[CG笔记]绘制图元:三角形

news2024/11/26 0:28:12

学习资料是Github的一个项目Tiny renderer or how OpenGL works: software rendering in 500 lines of code

本文对应原教程的第二课的部分内容

原教程重在思路,主要内容是以推导为主,所以这里还是记录思路和为代码做注释

知乎也有人给出了中译版:[从零构建光栅渲染器] 0.引言

三角形的线框绘制与区域填充

教程给出的代码中,geometry.h的引用处要加一行#include <ostream>,否则报错

最基础的方法固然是借用已经实现的画线函数line,对三个顶点两两一组依次使用即可,使用方法类似如下:

// defination
void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) { 
    line(t0, t1, image, color); 
    line(t1, t2, image, color); 
    line(t2, t0, image, color); 
}

// ...

// using in code
Vec2i t0[3] = {Vec2i(10, 70),   Vec2i(50, 160),  Vec2i(70, 80)}; 
Vec2i t1[3] = {Vec2i(180, 50),  Vec2i(150, 1),   Vec2i(70, 180)}; 
Vec2i t2[3] = {Vec2i(180, 150), Vec2i(120, 160), Vec2i(130, 180)}; 
triangle(t0[0], t0[1], t0[2], image, red); 
triangle(t1[0], t1[1], t1[2], image, white); 
triangle(t2[0], t2[1], t2[2], image, green);

一个好的绘制三角形的方法应该有以下几个特点:

  • 应该是简单和高效的
  • 对称的,图片不应该取决于传递给绘制函数的顶点顺序

作者给出的方法是:

  1. 按Y坐标对构成三角形的顶点进行排序
  2. 对三角形的左右两边同时进行光栅化
  3. 在左右两边之间的区域内使用水平线填充

这类似就是多边形区域填充中的扫描线方法

在这里插入图片描述

作者的意思是将一个三角形的分为左右两部分来看,其中一个部分是y轴最下方的顶点到最上方的顶点的连线(红色部分,y方向跨度最大),其余的构成另一部分,这一部分拥有两条线段,显然不是一次就能绘制完的

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) { 
    // sort the vertices, t0, t1, t2 lower−to−upper (bubblesort yay!) 
    if (t0.y>t1.y) std::swap(t0, t1); 
    if (t0.y>t2.y) std::swap(t0, t2); 
    if (t1.y>t2.y) std::swap(t1, t2); 
    int total_height = t2.y-t0.y; 
    for (int y=t0.y; y<=t1.y; y++) { 
        int segment_height = t1.y-t0.y+1; 
        float alpha = (float)(y-t0.y)/total_height; 
        float beta  = (float)(y-t0.y)/segment_height; // be careful with divisions by zero 
        Vec2i A = t0 + (t2-t0)*alpha; 
        Vec2i B = t0 + (t1-t0)*beta; 
        if (A.x>B.x) std::swap(A, B); 
        for (int j=A.x; j<=B.x; j++) { 
            image.set(j, y, color); // attention, due to int casts t0.y+i != A.y 
        } 
    } 
    for (int y=t1.y; y<=t2.y; y++) { 
        int segment_height =  t2.y-t1.y+1; 
        float alpha = (float)(y-t0.y)/total_height; 
        float beta  = (float)(y-t1.y)/segment_height; // be careful with divisions by zero 
        Vec2i A = t0 + (t2-t0)*alpha; 
        Vec2i B = t1 + (t2-t1)*beta; 
        if (A.x>B.x) std::swap(A, B); 
        for (int j=A.x; j<=B.x; j++) { 
            image.set(j, y, color); // attention, due to int casts t0.y+i != A.y 
        } 
    } 
}

在这里插入图片描述
根据上面的代码,两个for循环分别绘制一个三角形的上半部分和下半部分。由于图形是像素组成的,这里要对每部分的像素按行绘制(就是上面作者提到的三点中的第三点),如下图可以所示(这个图是我在PS作的,有抗锯齿,但是这里绘制的不应该有抗锯齿)。外层for循环的每一次,就绘制好了一行,内存for循环一次,是绘制这一行的其中一个像素点。

在这里插入图片描述

单拿出一个for循环看,其中外层循环先算出这条线的左右端点(即代码中的AB),而这个左右端点的计算,要依靠aplhabeta这两个变量,而这两个变量,以上半部分为例,就是当前绘制横线(红色)的y坐标到底端(t0的y坐标)的差,占整个三角形y方向长度的比例,用这个就可以推知下图A这个向量(即原点指向A点的向量),即A = t0 + (t2-t0)*alpha,B向量同理。知道向量,自然就知道A点在哪,以及B点在哪。

按照比例(等比三角形),A、B点连线本身也是水平于坐标轴的,故而可以使用两点的X坐标作为两端,在它们中间填充像素

float alpha = (float)(y-t0.y)/total_height; 
float beta  = (float)(y-t0.y)/segment_height; // be careful with divisions by zero 

在这里插入图片描述

并且这个代码有两个问题,一个是重复代码过多,另一个是某些情况无法绘制(注释中说了,segment_height有可能为0,此时不能做除数),故而进行优化:

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) { 
    if (t0.y==t1.y && t0.y==t2.y) return; // I dont care about degenerate triangles 
    // sort the vertices, t0, t1, t2 lower−to−upper (bubblesort yay!) 
    if (t0.y>t1.y) std::swap(t0, t1); 
    if (t0.y>t2.y) std::swap(t0, t2); 
    if (t1.y>t2.y) std::swap(t1, t2); 
    int total_height = t2.y-t0.y; 
    for (int i=0; i<total_height; i++) { 
        bool second_half = i>t1.y-t0.y || t1.y==t0.y; 
        int segment_height = second_half ? t2.y-t1.y : t1.y-t0.y; 
        float alpha = (float)i/total_height; 
        float beta  = (float)(i-(second_half ? t1.y-t0.y : 0))/segment_height; // be careful: with above conditions no division by zero here 
        Vec2i A = t0 + (t2-t0)*alpha; 
        Vec2i B = second_half ? t1 + (t2-t1)*beta : t0 + (t1-t0)*beta; 
        if (A.x>B.x) std::swap(A, B); 
        for (int j=A.x; j<=B.x; j++) { 
            image.set(j, t0.y+i, color); // attention, due to int casts t0.y+i != A.y 
        } 
    } 
}

观察以上代码,可以发现的有

如果segment_height为0,则三角形有一条边本身就是平行于扫描线的方向,即X轴方向。此时本身相当于绘制上面例子中三角形的下半部分(即所谓的second_half)。

因此代码中设置了second_half变量,绘制下半部分有两种情况,一种是确实在绘制下半部分(即i>t1.y-t0.y),第二种是绘制上面说的底边平行于扫描线方向(即t1.y==t0.y),也当做绘制下半部分。

second_half作为标志变量,有选择性地更改segment_heightbeta,实现了在一个for写完所有操作。

这就是绘制2D三角形的方法了

题外话:重心坐标系

这是另一种绘制三角形的方法

作者随即提到了一个叫做重心坐标系(barycentric coordinates)的东西。我直接拿一张维基百科的图展示一下:

在这里插入图片描述

补充链接:计算机图形学三(补充):重心坐标(barycentric coordinates)详解及其作用

不难发现该坐标系有3个维度,顶点处必有一个维度为1,其他为0。重心处是(1/3, 1/3, 1/3)。因此这是一种全新的思路:在重心坐标系检查某个坐标是否处于三角形内

很简单,如果这个坐标的三个分量有负值,就说明在三角形外

Tips:某点若在三角形内部则其在该三角形的重心坐标系下三个分量都为非负数

因此我们需要一个barycentric()函数,它计算给定三角形中点 P 的坐标。

至于triangle()函数,它计算边界盒。定义一个边界盒需要知道左下角和右上角。为了找到这些位置,我们迭代了三角形的所有顶点并且找到最小/最大的坐标。我们会在边界盒的范围内,逐点检查它是否在三角形内。

#include <vector> 
#include <iostream> 
#include "geometry.h"
#include "tgaimage.h" 
 
const int width  = 200; 
const int height = 200; 
 
Vec3f barycentric(Vec2i *pts, Vec2i P) { 
    Vec3f u = Vec3f(pts[2][0]-pts[0][0], pts[1][0]-pts[0][0], pts[0][0]-P[0])^Vec3f(pts[2][1]-pts[0][1], pts[1][1]-pts[0][1], pts[0][1]-P[1]);
    /* `pts` and `P` has integer value as coordinates
       so `abs(u[2])` < 1 means `u[2]` is 0, that means
       triangle is degenerate, in this case return something with negative coordinates */
    if (std::abs(u.z)<1) return Vec3f(-1,1,1);
    return Vec3f(1.f-(u.x+u.y)/u.z, u.y/u.z, u.x/u.z); 
} 
 
void triangle(Vec2i *pts, TGAImage &image, TGAColor color) { 
    Vec2i bboxmin(image.get_width()-1,  image.get_height()-1); 
    Vec2i bboxmax(0, 0); 
    Vec2i clamp(image.get_width()-1, image.get_height()-1); 
    for (int i=0; i<3; i++) { 
        bboxmin.x = std::max(0, std::min(bboxmin.x, pts[i].x));
	bboxmin.y = std::max(0, std::min(bboxmin.y, pts[i].y));

	bboxmax.x = std::min(clamp.x, std::max(bboxmax.x, pts[i].x));
	bboxmax.y = std::min(clamp.y, std::max(bboxmax.y, pts[i].y));
    } 
    Vec2i 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, P); 
            if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0) continue; 
            image.set(P.x, P.y, color); 
        } 
    } 
} 
 
int main(int argc, char** argv) { 
    TGAImage frame(200, 200, TGAImage::RGB); 
    Vec2i pts[3] = {Vec2i(10,10), Vec2i(100, 30), Vec2i(190, 160)}; 
    triangle(pts, frame, TGAColor(255, 0, 0)); 
    frame.flip_vertically(); // to place the origin in the bottom left corner of the image 
    frame.write_tga_file("framebuffer.tga");
    return 0; 
}

阅读完代码,就得去理解Vec3f barycentric(Vec2i*, Vec2i)这个函数的原理,关键之处就是下面的这行代码:

Vec3f u = Vec3f(pts[2][0]-pts[0][0], pts[1][0]-pts[0][0], pts[0][0]-P[0])^Vec3f(pts[2][1]-pts[0][1], pts[1][1]-pts[0][1], pts[0][1]-P[1]);

能够看到在triangle函数中调用了这个函数,调用的思路是对于边界盒内的每个点P,依次调用Vec3f bc_screen = barycentric(pts, P); ,检查返回的重心坐标系下的坐标点bc_screen,若某分量小于0则不做任何事情,若均不小于0,则认为在三角形内,进行指定操作(在这里则是绘制)。

其中需要知道的是pts是什么,从main函数处的调用可以知道它是三角形的三个点的坐标,类型是Vec2i[3](我知道可能在C++中这么说不严谨),即每个点是一个Vec2i类型变量。

能猜出来这个代码是将笛卡尔坐标系转化为重心坐标系的,但是实现 ( x , y ) (x,y) (x,y) ( α , β , γ ) (\alpha, \beta, \gamma) (α,β,γ)的转换的公式是什么?可以从上面的链接文章和后面这个链接里找到:重心坐标系

我直接拿上面链接的知乎中译版截个图:

在这里插入图片描述
到这里最后一个式子就是要解的,实际上是要 ( u , v , 1 ) (u,v,1) (u,v,1) ( A B x , A C x , P A x ) (AB_x,AC_x,PA_x) (ABx,ACx,PAx) ( A B y , A C y , P A y ) (AB_y,AC_y,PA_y) (ABy,ACy,PAy)这两个向量正交(点积为零)

正交的定义:对于向量 α \alpha α β \beta β,有 ( α , β ) = α T β = 0 (\alpha,\beta)=\alpha^T\beta=0 (α,β)=αTβ=0

对于点积 α ⸳ β \alpha ⸳ \beta αβ的几何意义是 α \alpha α β \beta β上的投影,点积为零意味着垂直或者说正交。点积忘了的可以看:向量点乘与叉乘的概念及几何意义

点积的公式是 α ⸳ β = ∣ α ∣ ∣ β ∣ c o s θ \alpha ⸳ \beta=|\alpha||\beta|cos\theta αβ=α∣∣βcosθ

那么与这两个向量都垂直的向量 ( u , v , 1 ) (u,v,1) (u,v,1),可以用叉乘得到该变量的方向,之后再修改一下长度就好了。叉乘的性质就是对于不同向的两个向量 α \alpha α β \beta β,叉乘 α × β \alpha \times \beta α×β得到的结果是个向量,且同时垂直于 α \alpha α β \beta β这两个向量。

总归这个原理是弄明白了,但是问题是为什么Vec3f的构造函数接受这么多参数,是我没想明白的。

此外就是代码中的Vec3f^运算,实际上就是叉乘,可以在给的头文件里面找到定义:

inline Vec3<t> operator ^(const Vec3<t> &v) const { return Vec3<t>(y*v.z-z*v.y, z*v.x-x*v.z, x*v.y-y*v.x); }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/114225.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ARM uart stdio 的移植

一、uart stdio的移植1 1. 什么是 stdio (1) #include <stdio.h> (2) stdio&#xff1a;standard input output&#xff0c;标准输入输出 (3) 标准输入输出就是操作系统定义的默认的输入和输出通道。一般在 PC 机的情况下&#xff0c;标准输入指的是键盘&#xff0c;标…

C语言——操作符详解(下)

C语言——操作符详解&#xff08;下&#xff09;一、赋值操作符二、复合赋值符三、单目操作符单目操作符介绍四、 关系操作符五、逻辑操作符六、条件操作符七、逗号表达式八、下标引用、函数调用和结构成员8.1 [ ] 下标引用操作符8.2 ( ) 函数调用操作符8.3访问一个结构的成员一…

【Linux】Linux指令串讲

大家好&#xff0c;今天要开启一个新的专题&#xff1a;Linux 今天的内容是指令还有一些基本的Linux知识补充 由于Linux的知识很难明确写出分类&#xff0c;所以目录就不会做的特别详细完全 喜欢的小伙伴点赞收藏一下不迷路哦 目录 1.目录 2.文件 3.路径 1.目录 1.创建目录…

初识Docker:(3)Docker架构

初识Docker&#xff1a;&#xff08;3&#xff09;Docker架构镜像和容器Docker和DockerHubDocker架构总结镜像和容器 镜像&#xff08;Image&#xff09;&#xff1a;Docker将应用程序及其所需的以来、函数库、环境、配置等文件打包在一起&#xff0c;成为镜像。 容器&#x…

力扣27.移除元素(双指针算法)

题目描述&#xff1a; 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素&#xff0c;并返回移除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须仅使用 O(1) 额外空间并 原地 修改输入数组。 元素的顺序可以改变。你不需要考…

编译openssl支持libcurl的https访问

如果编译时不带openssl库那么无法访问https的网页&#xff0c;从网页端什么也获取不到。 在调用post请求访问翔云OCR人脸识别时无法访问&#xff0c;而使用ssl&#xff0c;需要先在系统中安装OpenSSL。 如下图安装说明&#xff1a; 所以将原先安装的libcurl库删掉&#xff0…

MySQL事务的隔离级别

&#x1f3c6;今日学习目标&#xff1a; &#x1f340;MySQL事务的隔离级别 ✅创作者&#xff1a;林在闪闪发光 ⏰预计时间&#xff1a;30分钟 &#x1f389;个人主页&#xff1a;林在闪闪发光的个人主页 &#x1f341;林在闪闪发光的个人社区&#xff0c;欢迎你的加入: 林在闪…

【C与数据结构】——寒假提高每日练习Day2

一共16日的练习&#xff0c;分为选择题与编程题&#xff0c;涵盖了C语言所学以及数据结构的重点&#xff0c;以及一些秋招、春招面试的高频考点&#xff0c;难度会随着天数而上升。1-8day为C语言&#xff0c;9-16day为数据结构。 &#xff08;建议在电脑客户端进行&#xff0c…

docker中的数据卷

目录 1. 为什么使用数据卷 2. 数据卷基本操作 2.1 创建数据卷 2.2 查看数据卷 2.3 查看数据卷详细信息 2.4 数据卷删除 3. 数据卷的使用 3.1 先创建数据卷再挂载 3.2 直接挂载宿主机目录 3.3 只读数据卷 4. 数据卷容器 4.1 新建数据卷容器 4.2 新建一个容器来使用数…

Nacos学习笔记 (8)服务发现基础应用

1. 什么是服务发现 在微服务架构中&#xff0c;整个系统会按职责能力划分为多个服务&#xff0c;通过服务之间协作来实现业务目标。这样在我们的代码中免不了要进行服务间的远程调用&#xff0c;服务的消费方要调用服务的生产方&#xff0c;为了完成一次请求&#xff0c;消费方…

求空间曲线的切线,法平面

求空间曲线的切线&#xff0c;法平面&#xff1a;归结为求空间曲线的切向量 进而用点向式直线方程表示出切线&#xff0c;点法式方程表示出法平面 情况一&#xff1a;空间曲线以参数式给出&#xff0c;求切向量时直接求导即可&#xff0c;如下题 情况二&#xff1a;空间曲线…

ROS2 基础概念 话题

ROS2 基础概念 话题1. Topics2. rqt_graph3. 话题4. 话题类型5. 话题发布6. 话题频率1. Topics 话题是节点交换消息的总线 节点可以向任意数量的话题发布数据&#xff0c;并同时订阅任意数量的话题 2. rqt_graph 将使用rqt_graph来可视化不断变化的节点和话题&#xff0c;以…

云服务器ECS入门及云上网站部署

云服务器ECS入门及云上网站部署 一、什么是云服务器ECS 云服务器ECS (Elastic Compute Service) 是阿里云提供的性能卓越、稳定可靠、弹性扩展的laaS(Infrastructure as a Service) 级别云计算服务 云服务器ECS免去了您采购IT硬件的前期准备&#xff0c;让您像使用水、电、天…

JavaScript操作BOM对象

BOM&#xff1a;浏览器对象模型 window代表浏览器窗口 >window.alert(1) undefined >window.innerHeight //浏览器内部高度 242 >window.innerWidth 1229 >window.outerHeight //浏览器外部高度 824 >window.outerWidth 1536 Navigator&#xff0c;封装了浏…

【Linux】第六部分 远程登录

【Linux】第六部分 远程登录 文章目录【Linux】第六部分 远程登录6. 远程登录6.1 配置hosts映射文件6.2 xshell和xftp的使用总结6. 远程登录 6.1 配置hosts映射文件 为什么要配置呢?利于后续我们进行连接方便,就比如:我们没有办法记住很多手机号,但是我们可以记住人名,我们打…

【20221225】【剑指1】链表

1、从尾到头打印链表 可以用rbegin&#xff0c;rend&#xff1b;也可以用reverse翻转数组。 2、如果是翻转链表的话&#xff08;双指针法&#xff0c;用虚拟头节点依次翻转&#xff09;&#xff1a;https://blog.csdn.net/HYAIWYH/article/details/127118468?ops_request_mi…

java ssm 摄影约拍系统的设计

目录 第一章 绪论 5 1.1 研究背景 5 1.2系统研究现状 5 1.3 系统实现的功能 6 1.4系统实现的特点 6 1.5 本文的组织结构 6 第二章开发技术与环境配置 7 2.1 Java语言简介 7 2.2JSP技术 8 2.3 MySQL环境配置 8 2.4 MyEclipse环境配置 9 2.5 mysql数据库介绍 9 2.6 B/S架构 9 第三…

6、集合介绍

文章目录6、集合6.1 介绍6.2 常用接口和类6.3 ArrayList6.3.1 介绍6.3.2 基本操作6.3.3 常用方法6.4 LinkedList6.4.1 基本操作6.4.2 常用方法6.5 泛型6.5.1 介绍6.5.2 基本使用6.6 比较器6.7 ArrayList和LinkedList的比较6.8 HashSet6.8.1 介绍6.8.2 常用方法6.8.3 重复数据6.…

Attetion is all you need论文阅读笔记

Attetion is all you need 参考&#xff1a;沐神&#xff08; 沐神_论文精讲_Attention is all you need&#xff09; 1、Abstract 主流的序列转录模型&#xff08;给一个序列生成另一个序列&#xff0c;比如机器翻译&#xff0c;给一句英文&#xff0c;生成一句中文&#x…

数据可视化大屏应急管理综合指挥调度系统完整案例详解(PHP-API、Echarts、百度地图)

文章目录项目说明一、项目说明单位信息数据库字段&#xff1a;资源数据库字段项目需求二、项目开发1.项目分析2.引入库3.项目开发&#xff08;1&#xff09;地图容器构建&#xff08;2&#xff09;筛选和返回按钮事件&#xff08;3&#xff09;企业筛选功能&#xff08;4&#…