惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
G
GRAHAM CLULEY
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V2EX - 技术
V2EX - 技术
The Last Watchdog
The Last Watchdog
S
Secure Thoughts
Webroot Blog
Webroot Blog
PCI Perspectives
PCI Perspectives
L
LINUX DO - 最新话题
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
H
Heimdal Security Blog
H
Help Net Security
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
The GitHub Blog
The GitHub Blog
Jina AI
Jina AI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
F
Full Disclosure
小众软件
小众软件
S
Securelist
罗磊的独立博客
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
云风的 BLOG
云风的 BLOG
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
Know Your Adversary
Know Your Adversary
S
Schneier on Security
D
DataBreaches.Net
M
MIT News - Artificial intelligence
V
Vulnerabilities – Threatpost
N
News and Events Feed by Topic
有赞技术团队
有赞技术团队
F
Fortinet All Blogs
T
Tenable Blog
The Register - Security
The Register - Security
C
Check Point Blog
AWS News Blog
AWS News Blog
Cloudbric
Cloudbric
C
CXSECURITY Database RSS Feed - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Google Online Security Blog
Google Online Security Blog
博客园 - 叶小钗
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 司徒正美

重归混沌的BLOG

给silly实现了一个ernro模块 | 重归混沌的BLOG 给silly实现了一个ernro模块 | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 如何基于LanguageServerProtocol来编写lint工具 谈谈游戏服务器中RPC模块的设计 谈谈游戏服务器代码抽象 最近碰到的一个分布式一致性问题 谈谈游戏服务器的自动化测试 对Raft协议的一点理解 使用mmap来学习/proc/pid/smaps 2023(完) 再次实现了一个Lua性能分析器 终于给Silly的定时器增加了取消功能 一次虚拟内存排查经历 游戏服务器分布式数据的一种同步的思路 为silly增加了互斥锁 2022(完) Go语言之闭包篇 一例误用unsafe包引起的内存问题 Go语言之内存篇 初识Go语言 重新抽象图形API 给Lua实现了一个数学库 谈谈跨平台图形API的抽象 寻路和Flocking算法的结合 行为树的一种高效实现 内测过程中Shader出现的问题 彻底解决多国语言 谈谈数据库的选型 再谈Lua热更新(终) 初窥Rust 关于游戏服务器的服务拆分 ECS的初步实现 ECS初探 屏幕空间(SreenSpace)的想象力 一些对辐射度量学的理解 深度缓冲和半透明渲染 Mysql的间隙锁 更新一些GPU相关知识 2020 地形渲染之爬过的坑 Lua5.3 GC源码阅读(5) 实现一个数据库存储队列 再学计算机图形学入门 再谈分布式服务架构 游戏上线一个月后的反思 一次并发Bug 双向链表的三种实现 谈谈随机数的使用 再谈性能优化 2019 Lua中的函数式编程 重构登录逻辑 Unity资源管理(续) 谈谈Unity的资源管理 一次关于Cache的性能分析 历史之2018 DC3算法 移动平台native代码遭遇的坑 从CPU层面谈谈优化 开卷有益(UNIX编程艺术篇) GC竞争问题 通过Mesh投影来实现贴花系统 谈谈我对数据同步的理解 又一个类型提升引起的Bug Lua5.3 GC源码阅读(4) Lua5.3 GC源码阅读(3) Lua5.3 GC源码阅读(2) Lua5.3 GC源码阅读(1) 一次git事故 再见2017 又一个lua调试器 客户端缓存落地方案 Paxos算法 HTTP服务器的特点 一次性能优化经历 关于CPU分支预测 C程序中让两个不同版本的库共存 实现了一个AOI模块 一个高可伸缩的游戏服务器架构 关于网络协议封装的一些新想法
三角形光栅化时遇到的坑
重归混沌 · 2018-03-18 · via 重归混沌的BLOG

前一段时间打算写一个完整的游戏, 客户采用Unity3D引擎, 服务端则采用我自己的Silly网络框架

然而,最终这个项目烂尾了。烂尾的原因有很多,比如缺少资源,在不断寻找资源过程中使自己开发的热情消失殆尽等。但更为重要的是,我发现在使用Unity3D过程中,除了拼接UI逻辑时,没有碰到太大困难外。在实现一些3D效果时竟处处掣肘,甚至连最简单的贴花系统都实现不了。

而此时我的图形学背景是,《3D数学基础:图形与游戏开发》,《DirectX9.9 3D游戏开发编程基础》,《Unity Shader入门精要》和其他一些Unity操作手册。

这使我意识到,我的图形学知识结构出现了根本性问题。之后偶然的一个机会,我在网上接触到了“光栅化软件渲染器”的概念。深挖之下,发现这正是我目前所缺少的知识。

《3D数学基础:图形与游戏开发》介绍了“物体坐标系”,“世界坐标系”,“摄相机坐标系”等各种坐标系,以及如何实现一个数学引擎来实现各坐标系之间的转换。

《DirectX9.9 3D游戏开发编程基础》则介绍的是我们如何使用DirectX提供的各种API来使用相机,光照等各种抽象好的模型。

但是,这两本书之间存在着明显的断层,很难明白在调用DirectX提供的API之后,底层渲染是如何工作。例如,一个物体到底是如何一步一步画到屏幕上的,纹理是如何映射到3D物体上的,Shader到底是如何工作等。虽然这几本书,每一本都会讲一遍渲染流水线,但是这些细节很难被提到。

从零实现一个“光栅化软件渲染器”可以完美的填充这个空白,哪是一个效率很低的”软件渲染器”,这对理解3D渲染是如何工作起着至关重要的作用。幸运的是刚好有一本《3D游戏编程大师技巧》讲的就是这些内容(虽然我不是很认可他的章节安排:D)。


按照《3D游戏编程大师技巧》实现自己的软件渲染器的过程中,数学引擎,坐标系转换,甚至连光照都没碰到什么大问题,在最后一步将屏幕坐标系下的2D三角形光栅化时,踩了一个3连坑,导致一下耽搁了半个月才终于在昨天找到问题所在。

在光栅化的过程中,为了防止重复绘制相素,一般会采用左上(top-left)填充规则。例如绘制一个对角定点为(0, 0), (6,6)的正方形时,会避免为第6行和第6列绘制相素。大概实现伪码如下:

//void draw_pixel(int x, int y, int color);
int x, y;
for (y = 0; y < 6; y++) {
        for (x = 0; x < 6; x++) {
                draw_pixel(x, y, color);
        }
        ... do something ...
}

然而,看实简单的代码背后,却隐藏着各种坑。

在经过渲染流水线计算之后的图元(一般为三角形)坐标一般为float类型,而且大部分情况下都会有小数部分,比如(94.65, 331.64)。这时就需要制定一个规则,是向上取整(即将坐标变成(95,332))或者向下取整(即将坐标变成(94,331))。

为了省事,我的第一版直接采用了相下取整进行坐标映射,渲染伪码如下(这里仅仅将x,y的类型从int变成了float,因为向draw_pixel传参时,将float转成int采用的就是去1法):

//这里仅假设光栅化一个平顶三角形【(x0,y0), (x1, y1), (x2, y2)】并且(x0 < x1 && y0 == y1 && y0 < y2)。
//void draw_pixel(int x, int y, int color);
void draw(float x0, float y0, float x1, float y1, float x2, float y2)
{
        float x, y;
        float h = y2 - y1;
        float xleft_step = (x2 - x0) / h;
        float xright_step = (x2 - x1) / h;
        xleft = x0;
        xright = x1;
        for (y = y0; y < y2; y++) {
                for (x = xleft; x < xright; x++) {
                        draw_pixel(x, y, color);
                }
                xleft += xleft_step;
                xright += xright_step;
        }
}

然而这是不对的,一个最简单的例子,当【xstart=9.1,xend = 18.5】时,由于采用去1法映射相素坐标,因此理论上x方向绘制的相素坐标最大值应该小于18。但是当x=18.1上仍然会执行draw_pixel函数,此时经过传参转换后,x的值为18,这违反了左上规则(y轴同样存在这个问题)。

改良后第二版如下:

//这里仅假设光栅化一个平顶三角形【(x0,y0), (x1, y1), (x2, y2)】并且(x0 < x1 && y0 == y1 && y0 < y2)。
//void draw_pixel(int x, int y, int color);
void draw(float x0, float y0, float x1, float y1, float x2, float y2)
{
        int x, y;
        float h = y2 - y1;
        float xleft_step = (x2 - x0) / h;
        float xright_step = (x2 - x1) / h;
        xleft = x0;
        xright = x1;
        for (y = (int)y0; y < (int)y2; y++) {
                for (x = (int)xleft; x < (int)xright; x++) {
                        draw_pixel(x, y, color);
                }
                xleft += xleft_step;
                xright += xright_step;
        }
}

但是这依然不对, 当(x0, y0) = (2.1, 0.5)并且(x2, y2) = (7.1, 5.5) 时,代码实际绘制的第一个相素坐标为(2,0)。

但是根据直线【(x0, y0),(x2,y2)】的斜率可以算出当y=0时,x的值应该为1.1,换句话说理论上应该绘制的第一个相素坐标为(1, 0)。因此上述代码完美的将相素坐标(1,0)给避过去了,大部分情况下这种现象表现为,两个相邻的图元(这里指三角形)之间会有一条空白线(即两个三角形均没有绘制这条线)。

因此,当我们将y向下取整时,需要根据舍去去的小数修正x的值(因为斜率可能会非常大,有可能y只舍去了0.1,但是x会偏差好几个像素)。

修正版的代码如下:

//这里仅假设光栅化一个平顶三角形【(x0,y0), (x1, y1), (x2, y2)】并且(x0 < x1 && y0 == y1 && y0 < y2)。
//void draw_pixel(int x, int y, int color);
void draw(float x0, float y0, float x1, float y1, float x2, float y2)
{
        int x, y;
        float h = y2 - y1;
        float xleft_step = (x2 - x0) / h;
        float xright_step = (x2 - x1) / h;
        xleft = x0 - (y0 - (int)y0) * xleft_step;
        xright = x1 - (y0 - (int)y0) * xright_step;
        for (y = (int)y0; y < (int)y2; y++) {
                for (x = (int)xleft; x < (int)xright; x++) {
                        draw_pixel(x, y, color);
                }
                xleft += xleft_step;
                xright += xright_step;
        }
}

你以为事情就结束了么= =!

当直线【(x0, y0),(x2,y2)】的斜率为20时,y坐标向上取整时舍去y坐标0.5,x坐标会向左偏移10个坐标。这样原本(x0, y0) = (100, 10.5)的坐标经过取整后就会变成(80, 10)。这种错误大部分情况下表现为,三角形的顶部或底部突然多出一条直线。将所有逻辑改为向上取整,即可解决此问题。因为向上取整会保证所有坐标点都会落在原始三角形内,而向下取整会导致某些不在三角形内部。

修正版代码如下:

//这里仅假设光栅化一个平顶三角形【(x0,y0), (x1, y1), (x2, y2)】并且(x0 < x1 && y0 == y1 && y0 < y2)。
//void draw_pixel(int x, int y, int color);
void draw(float x0, float y0, float x1, float y1, float x2, float y2)
{
        int x, y;
        float h = y2 - y1;
        float xleft_step = (x2 - x0) / h;
        float xright_step = (x2 - x1) / h;
        xleft = x0 +(ceil(y0) - y0) * xleft_step;
        xright = x1 + (ceil(y0) - y0) * xright_step;
        for (y = ceil(y0); y < ceil(y2); y++) {
                for (x = ceil(xleft); x < ceil(xright); x++) {
                        draw_pixel(x, y, color);
                }
                xleft += xleft_step;
                xright += xright_step;
        }
}

至此,三连坑才算被踩完.


3月25日补充:

在进行仿射纹理映射时, 同样踩了两个坑。

1. 当y坐标向上取整时,相应的uv坐标需要采用与xleft和lright一样的算法进行修正。
2. 所有的纹理坐标范围是(0.0~1.0),在向位图坐标转换时(假设位图宽度为64×64)需要转换为(0, 63)

完整的修复代码在这里