Bresenham’s line drawing(布雷森汉姆算法)

进行games101的光栅化作业时,对其渲染原理仍不甚了解,找到tinyrenderer软光栅项目。在此记录下试错的过程。

作者在最初为我们做好了framebuffer,读者入手的方向实际是从渲染的过程开始。对于如何渲染出像素显示在画面上,应该需要从其他博主那进行学习,或者从作者实现的文件中分析,这里就不做多余的解释。

友情提示:这里记录个人对tinyrenderer原理的理解,若需源代码请从原作者的github处下载。(作者的博客内蓝色高光处包含了不同阶段源代码的地址)

github地址在这里:https://ssloy.github.io/tinyrenderer

作者第一节从线的绘制开始,一个三角面有三个顶点,即绘制三条直线。

Bresenham’s line drawing(布雷森汉姆算法)

给出两个顶点a(ax,ay),b(bx,by),渲染出两点间的直线。

按照作者的说法,直接抛出算法会很难以理解,采取渐进的形式,逐渐演化算法的执行。

假设参数t \(\in\)[0,1],定义二维点(x(t),y(t))如下:


x(t) = ax + t * (bx - ax)

y(t) = ay + t * (by - by)
对于该公式的推导

可以想象直线上两点,在两点间再取一点(x(t),y(t)),(这里设为(x,y)或许更好一些,但是图画好了懒得改了)

很容易可以想到它们间存在的相似三角形,

(y(t) - ay) / (by - ay) = t

t为0时,y(t) = ay,t为1时,y(t) = by

之后,简单推导即可得到参数方程,x(t)同理可得。

代码实现

void line(int ax,int ay,int bx,int by, TGAImage &framebuffer,TGAColor color){
for(float t = 0; t < 1; t += 0.02){
int x = ax + std::round(t * (bx - ax));
int y = ay + std::round(t * (by - ay));
framebuffer.set(x,y,color);
}
}



可以注意到红线部分存在四个缺口,仔细观察一下可以发现

t的取值为0-1,每0.02取值进行运算,可取51次

cx - ax = 62 - 7 = 55 次

其gap = 55 - 1 = 4

t的取值不足导致了gap的出现:

一个直接的解决方法是:

可以将 t += .02改为 t += .01,这样确实可以解决当前的gap问题,但当cx - ax的差值更大时,t的取值仍然会不足;

作者给出的方法则是使用t的定义式,而不是直接赋值:

t = (x(t) - ax) / (bx - ax)
或者
t = (y(t) - ay) / (by - ay)
对于t定义式可行的个人见解 每一个(x(t),y(t))坐标表示一个像素点位置,两点间横坐标差值即缺少的横向像素点的数量,纵坐标同理,这样即可遍历每一个单位像素点的横坐标,得到对应的t值,从而求得相应的纵坐标。

函数实现

void line(int ax,int ay,int bx,int by, TGAImage &framebuffer,TGAColor color){
for(int x = ax; x <= bx; x ++){
//t需要float型,故须使用static_cast<float>来使计算返回浮点值
float t = (x - ax) / static_cast <float> (bx - ax);
int y = ay + std::round(t * (by - ay));
framebuffer.set(x,y,color);
}
}

好了,现在我们出现两个问题,黄线出现了大量gap,红线消失:

红线消失很好发现原因,在for循环中,若bx < ax,循环会被打破,也就不会有线产生。

在bx < ax时,我们交换两点的位置即可。


if(bx < ax){
std::swap(ax,bx);
std::swap(ay,by);
}

绿线的问题,观察下图的白框和黄点可以看出



由于取整的问题,一个单位横坐标跨越了多个单位纵坐标。也就是说一个纵坐标可以对应着不到一个单位的横坐标,取整后可以做到连续的纵坐标对应着同一个横坐标,这样就避免了gap的出现:


bool steep = std::abs(bx - ax) < std::abs(by - ay);
if(steep){
std::swap(ax,ay);
std::swap(bx,by);
} //在绘制直线时,须判断steep,换回x,y位置
if(steep)
framebuffer.set(y,x,color)
else
framebuffer.set(x,y,color);



绘制成功!

接下来是性能上的优化,逐渐的接近布雷森汉姆算法

既然要优化性能,需要先了解花费性能较高的部分,首先是framebuffer.set()的调用,可惜我们此次不涉及该函数的设计,不考虑。其次就是y值的计算:

可以将t(x)式带入y(t)可得


y(x) = ay + (by - ay) * (x(t) - ax) / (bx - ax)

这样我们在代码上可以进一步处理:

我们知道,在for循环中,x(t)的值从ax开始,每次增加1,

所以(x(t) - ax)的值则为0,1,2,……;



y(i + 1) - y(i) = (by - ay) / (bx - ax)

在该值为0时,上式子中y(ax) = ay;

在这种条件下,y(x)的代码可转化为:


//此处y由int改为float
//每次循环y增值(by - ay) /static_cast (bx - ax),该数为float,多次增加后其值对y的影响会越来越大,将y改为float,可以避免误差的发生
float y = ay;
for(int x = ax; x <= bx; x ++){
if(steep)
framebuffer.set(y,x,color);
else
framebuffer.set(x,y,color);
y += (by - ay) /static_cast <float> (bx - ax);
}

此次以整型转换为浮点型为代价,经过作者测试,由3.99s降至2.8s,有显著优化



避免浮点运算可以为我们提升很大的性能,布雷森汉姆算法即使如此。

我们将y由float改为int:

易知,增值(by - ay) /static_cast <float> (bx - ax)不会大于一,自动由float转为int时,会丢弃小数位,不进行四舍五入,这意味着y值将不会发生改变;

我们通过引入变量 float error = 0;每次循环error增值(by - ay) /static_cast (bx - ax);即error做了float y之前做的事;
超过+-0.5,便为y+-1,error +-1;

实现代码

//此处改为使用std::abs来使得每次error的增值为正,以省去判断正负的问题。
float error = 0;
int y = ay;
for(int x = ax; x < bx; x ++){
if(steep)
framebuffer.set(y,x,color);
else
framebuffer.set(x,y,color);
error += std::abs(by - ay) / static_cast <float> (bx - ax);
if(error > .5){
y += by > ay ? 1 : -1;
error -= 1;
}
}

不难猜出,这次引入float error会花费更多的性能,为进行接下来的消除浮点运算。



我在这里进行进一步的演算,以便可以更清晰的消除浮点运算(error由float转为int):


//这里的表现形式不太合规,但应该可以理解
[error += std::abs(by - ay)] / static_cast <float> (bx - ax) > 0.5
两边同乘static_cast <float> (bx - ax)
[error += std::abs(by - ay)] > 0.5 * static_cast <float> (bx - ax)
此时,不可变的浮点数仅有0.5,那么我们两边同乘2
[error += 2 * std::abs(by - ay)] > (bx - ax)

至此,我们成功消除了浮点运算,实现布雷森汉姆算法。

代码实现

int ierror = 0;
int y = ay;
for(int x = ax; x < bx; x ++){
if(steep)
framebuffer.set(y,x,color);
else
framebuffer.set(x,y,color);
ierror += 2 * std::abs(by - ay);
if(ierror > (bx - ax)){
y += by > ay ? 1 : -1;
ierror -= 2 * (bx - ax);
}
}

从作者的测试结果来看,布雷森汉姆算法的实际速度要慢于第一次的优化,原因则是现在的整数运算并不总是比浮点运算更高效。

至于无分支结构的优化,可以在作者的博客中进行了解,我实际进行的测试中,作者的无分支形式实际要花费更多的性能,可能是现在的CPU的对分支版本的优化更加高效,不充分的无分支形式无法在其中获得优势

记录:tinyrenderer的更多相关文章

  1. 记一次debug记录:Uncaught SyntaxError: Unexpected token ILLEGAL

    在使用FIS3搭建项目的时候,遇到了一些问题,这里记录下. 这里是发布搭建代码: // 代码发布时 fis.media('qa') .match('*.{js,css,png}', { useHash ...

  2. nginx配置反向代理或跳转出现400问题处理记录

    午休完上班后,同事说测试站点访问接口出现400 Bad Request  Request Header Or Cookie Too Large提示,心想还好是测试服务器出现问题,影响不大,不过也赶紧上 ...

  3. Kali对wifi的破解记录

    好记性不如烂笔头,记录一下. 我是在淘宝买的拓实N87,Kali可以识别,还行. 操作系统:Kali 开始吧. 查看一下网卡的接口.命令如下 airmon-ng 可以看出接口名称是wlan0mon. ...

  4. 2015 西雅图微软总部MVP峰会记录

    2015 西雅图微软总部MVP峰会记录 今年决定参加微软MVP全球峰会,在出发之前本人就已经写这篇博客,希望将本次会议原汁原味奉献给大家 因为这次是本人第一次写会议记录,写得不好的地方希望各位园友见谅 ...

  5. 分享一个SQLSERVER脚本(计算数据库中各个表的数据量和每行记录所占用空间)

    分享一个SQLSERVER脚本(计算数据库中各个表的数据量和每行记录所占用空间) 很多时候我们都需要计算数据库中各个表的数据量和每行记录所占用空间 这里共享一个脚本 CREATE TABLE #tab ...

  6. 我是如何在SQLServer中处理每天四亿三千万记录的

    首先声明,我只是个程序员,不是专业的DBA,以下这篇文章是从一个问题的解决过程去写的,而不是一开始就给大家一个正确的结果,如果文中有不对的地方,请各位数据库大牛给予指正,以便我能够更好的处理此次业务. ...

  7. 前端学HTTP之日志记录

    前面的话 几乎所有的服务器和代理都会记录下它们所处理的HTTP事务摘要.这么做出于一系列的原因:跟踪使用情况.安全性.计费.错误检测等等.本文将谥介绍日志记录 记录内容 大多数情况下,日志的记录出于两 ...

  8. ASP.NET Core应用中如何记录和查看日志

    日志记录不仅对于我们开发的应用,还是对于ASP.NET Core框架功能都是一项非常重要的功能特性.我们知道ASP.NET Core使用的是一个极具扩展性的日志系统,该系统由Logger.Logger ...

  9. python+uwsgi导致redis无法长链接引起性能下降问题记录

    今天在部署python代码到预生产环境时,web站老是出现redis链接未初始化,无法连接到服务的提示,比对了一下开发环境与测试环境代码,完全一致,然后就是查看各种日志,排查了半天也没有查明是什么原因 ...

  10. 记录我这一年的技术之路(nodejs纯干货)

    2015年12月28日23:19:54 更新koa应用.学习型网站和开发者工具等 coding伊始 开始认认真真的学习技术还是2015.10.21日开始的,记得很清楚,那天,是我在龙湖正式学习的第一天 ...

随机推荐

  1. 中电金信:四川农担X中电金信大数据智能风控平台 护航金融服务乡村振兴

    ​高质量金融服务是乡村振兴的重要支撑.四川省农业融资担保有限公司(以下简称"四川农担")持续探索融资担保服务,努力满足"三农"领域多样化.多层次融资担保需求的同 ...

  2. k8s pod重启 deployment重启

    1.15版本之后可通过kubectl rollout restart deployment -n 命令来实现滚动重启POD 该命令会先创建待用POD,待新POD运行成功后,再关闭原有POD.因此需要保 ...

  3. Mac安装thrift出现的问题总结

    https://www.cnblogs.com/fingerboy/p/6424248.html刚上手thrift,安装上面花了时间,我在上面的链接中照着安装的.下面记录发生的问题:当我正确安装到bi ...

  4. PostgreSQL 初始化配置设置

    title: PostgreSQL 初始化配置设置 date: 2024/12/27 updated: 2024/12/27 author: cmdragon excerpt: PostgreSQL是 ...

  5. 关于Qt高分屏缩放几个知识点

    在windows上经常遇到高分屏缩放的问题,很头疼,貌似这东西就是windows首发的. 在Qt4时代的程序遇到高分屏缩放,不作任何处理,毕竟Qt4时代(2010年以前)出来的时候几乎还没高分屏缩放这 ...

  6. Qt开源作品24-遮罩层窗体

    一.前言 在有些项目中,需要在弹框的窗体背后遮罩原有主窗体,使得突出显示弹窗窗体,突然想到之前写过一个全局截屏的东东,原理一致,拿来改改.只需要引入一个头文件和实现文件,然后在主窗体中设置下需要遮罩的 ...

  7. C# 读取本地的TXT文件内容

    using (StreamReader streamReader = new StreamReader("C:\\Users\\zhang\\Desktop\\新建文件夹\\远程开关.txt ...

  8. JMeter使用指南+实验报告

    JMeter使用指南 目录 JMeter使用指南 界面基本配置方法 1.选项里的放大与缩小--缩放字体 2.选项里的选择语言 3.命令行的调出 注意事项 一些指标介绍 1.TCP取样器 2.汇总/聚合 ...

  9. 老生常谈——分布式限流:部分Sentinal源码解读

    基础知识 HTTP CODE = 429 "请求过多" A. 限流的类型 服务端 客户端 限流的标的 IP 用户 ... 基本要求 准确限制过量的请求. 低延时.限流器不能拖慢HT ...

  10. torque提交作业

    PBS(Protable Batch System)是功能最为齐全,历史最悠久,支持最广泛的本地集群调度器之一. PBS的目前包括openPBS,PBS Pro和Torque三个主要分支.其中Open ...