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

2023-10-11 02:30
文章标签 笔记 三角形 cg 绘制图

本文主要是介绍[CG笔记]绘制图元:三角形,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

学习资料是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 coordinatesso `abs(u[2])` < 1 means `u[2]` is 0, that meanstriangle 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 ) {( \overrightarrow{AB}_x , \overrightarrow{AC}_x , \overrightarrow{PA}_x) } (AB x,AC x,PA x) ( A B → y , A C → y , P A → y ) {( \overrightarrow{AB}_y , \overrightarrow{AC}_y , \overrightarrow{PA}_y) } (AB y,AC y,PA y)这两个向量正交(点积为零)。这里的意思是单拿出 ( A B → , A C → , P A → ) {( \overrightarrow{AB} , \overrightarrow{AC} , \overrightarrow{PA}) } (AB ,AC ,PA ) x x x y y y分量来运算,因为 A B → \overrightarrow{AB} AB 仍是一个向量,具有 x x x y y y两个维度,这实际上是个 2 × 3 2 \times3 2×3的矩阵。

正交的定义:对于向量 α \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); }

这篇关于[CG笔记]绘制图元:三角形的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/184880

相关文章

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

【WebGPU Unleashed】1.1 绘制三角形

一部2024新的WebGPU教程,作者Shi Yan。内容很好,翻译过来与大家共享,内容上会有改动,加上自己的理解。更多精彩内容尽在 dt.sim3d.cn ,关注公众号【sky的数孪技术】,技术交流、源码下载请添加微信号:digital_twin123 在 3D 渲染领域,三角形是最基本的绘制元素。在这里,我们将学习如何绘制单个三角形。接下来我们将制作一个简单的着色器来定义三角形内的像素

论文阅读笔记: Segment Anything

文章目录 Segment Anything摘要引言任务模型数据引擎数据集负责任的人工智能 Segment Anything Model图像编码器提示编码器mask解码器解决歧义损失和训练 Segment Anything 论文地址: https://arxiv.org/abs/2304.02643 代码地址:https://github.com/facebookresear

数学建模笔记—— 非线性规划

数学建模笔记—— 非线性规划 非线性规划1. 模型原理1.1 非线性规划的标准型1.2 非线性规划求解的Matlab函数 2. 典型例题3. matlab代码求解3.1 例1 一个简单示例3.2 例2 选址问题1. 第一问 线性规划2. 第二问 非线性规划 非线性规划 非线性规划是一种求解目标函数或约束条件中有一个或几个非线性函数的最优化问题的方法。运筹学的一个重要分支。2

【C++学习笔记 20】C++中的智能指针

智能指针的功能 在上一篇笔记提到了在栈和堆上创建变量的区别,使用new关键字创建变量时,需要搭配delete关键字销毁变量。而智能指针的作用就是调用new分配内存时,不必自己去调用delete,甚至不用调用new。 智能指针实际上就是对原始指针的包装。 unique_ptr 最简单的智能指针,是一种作用域指针,意思是当指针超出该作用域时,会自动调用delete。它名为unique的原因是这个

查看提交历史 —— Git 学习笔记 11

查看提交历史 查看提交历史 不带任何选项的git log-p选项--stat 选项--pretty=oneline选项--pretty=format选项git log常用选项列表参考资料 在提交了若干更新,又或者克隆了某个项目之后,你也许想回顾下提交历史。 完成这个任务最简单而又有效的 工具是 git log 命令。 接下来的例子会用一个用于演示的 simplegit

记录每次更新到仓库 —— Git 学习笔记 10

记录每次更新到仓库 文章目录 文件的状态三个区域检查当前文件状态跟踪新文件取消跟踪(un-tracking)文件重新跟踪(re-tracking)文件暂存已修改文件忽略某些文件查看已暂存和未暂存的修改提交更新跳过暂存区删除文件移动文件参考资料 咱们接着很多天以前的 取得Git仓库 这篇文章继续说。 文件的状态 不管是通过哪种方法,现在我们已经有了一个仓库,并从这个仓

忽略某些文件 —— Git 学习笔记 05

忽略某些文件 忽略某些文件 通过.gitignore文件其他规则源如何选择规则源参考资料 对于某些文件,我们不希望把它们纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。通常它们都是些自动生成的文件,比如日志文件、编译过程中创建的临时文件等。 通过.gitignore文件 假设我们要忽略 lib.a 文件,那我们可以在 lib.a 所在目录下创建一个名为 .gi

取得 Git 仓库 —— Git 学习笔记 04

取得 Git 仓库 —— Git 学习笔记 04 我认为, Git 的学习分为两大块:一是工作区、索引、本地版本库之间的交互;二是本地版本库和远程版本库之间的交互。第一块是基础,第二块是难点。 下面,我们就围绕着第一部分内容来学习,先不考虑远程仓库,只考虑本地仓库。 怎样取得项目的 Git 仓库? 有两种取得 Git 项目仓库的方法。第一种是在本地创建一个新的仓库,第二种是把其他地方的某个