回顾games101中的SSAA和MSAA
回顾games101中的AA(抗锯齿)
前言
善于进行课后总结,可以更加巩固自己的知识和具体细节
锯齿(走样)产生的原因
本质上,在光栅化阶段中,用有限离散的数据想表示连续的(类似三角形的某一边),就可能存在采样点不够的问题,也就引申出了锯齿(走样 Aliasing)的这个概念,在信号处理以及相关领域中,走样(混叠)在对不同的信号进行采样时,导致得出的信号相同的现象。它也可以指信号从采样点重新信号导致的跟原始信号不匹配的瑕疵
具体到实时渲染领域中,走样有以下三种:[3]
- 几何体走样(几何物体的边缘有锯齿),几何走样由于对几何边缘采样不足导致。

- 着色走样,由于对着色器中着色公式(渲染方程)采样不足导致。比较明显的现象就是高光闪烁。


上面一张图显示了由于对使用了高频法线贴图的高频高光BRDF采样不足时产生的着色走样。下面这张图显示了使用4倍超采样产生的效果。
- 时间走样,主要是对高速运动的物体采样不足导致。比如游戏中播放的动画发生跳变等。
SSAA(超采样反走样)
产生锯齿的原因本质上是因为采样点个数不够,少了,那我给你多一倍的采样点不就可以弥补了吗,比如一张800x600分辨率的图,我先长宽都加倍采样,变成1600x1200,那我在把它缩放回800x600不就可以了吗
过程:
对每个像素取n个子采样点,然后针对每个子像素点进行着色计算。最后根据每个子像素的值来合成最终的图像
MSAA(多重采样反走样)
在前面提到的SSAA中,每个子采样点都要进行单独的着色,这样在片断(像素)着色器比较复杂的情况下还是很费的。那么能不能只计算每个像素的颜色,而对于那些子采样点只计算一个覆盖信息(coverage)和遮挡信息(occlusion)来把像素的颜色信息写到每个子采样点里面呢?最终根据子采样点里面的颜色值来通过某个重建过滤器来降采样生成目标图像。这就是MSAA的原理。注意这里有一个很重要的点,就是每个子像素都有自己的颜色、深度模板信息,并且每一个子采样点都是需要经过深度和模板测试才能决定最终是不是把像素的颜色得到到这个子采样点所在的位置,而不是简单的作一个覆盖测试就写入颜色
代码实现
没有SSAA和MSAA之前,可以看到边缘锯齿化特别明显

SSAA



#define ssaa_sample 2
float sampling_period = 1.0f / ssaa_sample;
// 2x2SSAA
for (int x = xmin; x <= xmax; x++) {
for (int y = ymin; y <= ymax; y++) {
int in_num = 0;
Eigen::Vector3f color_sum;
for (int i = 0; i < ssaa_sample; ++i) {
for (int j = 0; j < ssaa_sample; ++j) {
// 中心点
float new_x = x + (i + 0.5) * sampling_period;
float new_y = y + (j + 0.5) * sampling_period;
if (insideTriangle(new_x, new_y, t.v)) {
auto [alpha, beta, gamma] = computeBarycentric2D(new_x, new_y, t.v);
float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated =
alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
// 左下角的点
int depth_buf_x, depth_buf_y;
depth_buf_x = x * ssaa_sample + i;
depth_buf_y = y * ssaa_sample + j;
if (z_interpolated < depth_buf[get_index(depth_buf_x, depth_buf_y)]) {
depth_buf[get_index(depth_buf_x, depth_buf_y)] = z_interpolated;
Vector3f temp_point = { depth_buf_x * 1.0f,depth_buf_y * 1.0f,0.0f };
Vector3f color = t.getColor();
set_temp_pixel(temp_point, color);
}
}
}
}
}
}
// Down Sample
for (int x = xmin; x <= xmax; x++)
{
for (int y = ymin; y <= ymax; y++)
{
Eigen::Vector3f color{ 0,0,0 };
Eigen::Vector3f point{ x * 1.0f, y * 1.0f, 0 };
for (int i = 0; i < ssaa_sample; ++i)
{
for (int j = 0; j < ssaa_sample; ++j)
{
int depth_buf_x, depth_buf_y;
depth_buf_x = x * ssaa_sample + i;
depth_buf_y = y * ssaa_sample + j;
color += temp_frame_buf[get_index(depth_buf_x, depth_buf_y)];
}
}
color /= (ssaa_sample * ssaa_sample);
set_pixel(point, color);
}
}
通过放大对比,其实可以看到SSAA的效果比MSAA好很多,解决了出现黑边的问题,整体也是接近于完美的效果
MSAA
以下例子为8x8的MSAA,MSAA其实就是求出一个面积的覆盖率,然后通过覆盖率乘以rgb,然后更新深度缓冲区和颜色缓冲区
auto v = t.toVector4();
int xmin = MIN(MIN(floor(v[0].x()), floor(v[0].x())), floor(v[2].x()));
int xmax = MAX(MAX(floor(v[0].x()), floor(v[1].x())), floor(v[2].x()));
int ymin = MIN(MIN(floor(v[0].y()), floor(v[1].y())), floor(v[2].y()));
int ymax = MAX(MAX(floor(v[0].y()), floor(v[1].y())), floor(v[2].y()));
int sample_num = 8;
std::vector<float> offset;
for (int i = 0; i < sample_num; ++i)
{
offset.push_back((0.5 + i) * 1.0 / static_cast<float>(sample_num));
}
int index;
// MSAA
for (int x = xmin; x <= xmax; x++) {
for (int y = ymin; y <= ymax; y++) {
int in_num = 0;
for (int i = 0; i < sample_num; ++i) {
for (int j = 0; j < sample_num; ++j) {
if (insideTriangle(x + offset[i], y + offset[j], t.v)) {
++in_num;
}
}
}
if (in_num > 0 &&insideTriangle(x + 0.5, y + 0.5, t.v)) {
auto [alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated =
alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
index = get_index(x, y);
if (index < frame_buf.size() && depth_buf[index] > z_interpolated) {
depth_buf[index] = z_interpolated;
Eigen::Vector3f point;
point << static_cast<float>(x), static_cast<float>(y), z_interpolated;
set_pixel(point, t.getColor() * in_num / (sample_num * sample_num));
}
}
}
}
其中会遇到几个问题,一是三角形的边可能会呈现黑色,这是因为当覆盖率过低的时候,乘上rgb基于接近于0,也就是黑色,然后蓝色三角形在绿色三角形的后面,计算深度的时候大于绿色三角形的深度,所以无法写入,就会呈现黑边的情况。


不过总体上效果也还算看得过去
SSAA和MSAA的优缺点
通过实践中可以看出,SSAA需要额外用到扩大的缓冲空间,以及在计算所有像素点后,还会经过downSample的过程,可以说从时间还是空间上消耗都比MSAA要大,但是他的效果也是显著的好
MSAA性能上优于SSAA,不需要扩展额外的深度缓存空间,但是效果不是特别好,可能需要后续的其他改进方法吧
参考博文
回顾games101中的SSAA和MSAA的更多相关文章
- 回顾Games101图形学(一)几何变换中一些公式的推导
回顾Games101 chatper1 - 6 前言 本文只写回顾后重新加深认识的知识 透视除法的意义 经过MVP矩阵之后,将模型空间下某点的坐标,转换成了裁剪空间下的坐标,此时因为裁剪空间的范围是x ...
- 25.redux回顾,redux中的action函数异步
回顾:Redux: 类似于 Vuex 概念:store/reducer/action action:动作 {type,.....} 一定要有type 其他属性不做限制 reducer:通过计算产生st ...
- 简单回顾C++中的字符串
C++中有两种字符串形式,一种是C语言字符数组,一般可以使用 char*指针来操作它:另一种是C++中基于标准库的string类型,这算是更高层次的抽象数据类型. 主要讨论一下string类型,既然是 ...
- 回顾MySQL中的事务特征
一.事务定义Transaction事务:一个最小的不可再分的工作单元:通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务就是一个最小的工作单元)一个完整的业务需要批量的DML(insert. ...
- 回顾js中的cookie/localstorage
1.首先简单总结下cookie cookie:可以做会话跟踪 特点: 1.大小限制(不能超过4k) 2.每个域下cookie不能超过50个 3.有效期(和设定时间有关), ...
- 专攻知识小点——回顾JavaWeb中的servlet(二)
续前篇... ServletConfig对象 Servlet的配置对象,ServletConfig对象作用域只能在一个Servlet类中使用.每个Servlet类都维护一个ServletConfig对 ...
- 专攻知识小点——回顾JavaWeb中的servlet(三)
HttpSession基本概述 ** ** 1.HttpSession:是服务器端的技术.和Cookie一样也是服务器和客户端的会话.获得该对象是通过HTTPServletRequest的方法getS ...
- 讲解Canvas中的一些重要方法
Canvas所提供的各种方法根据功能来看大致可以分为几类: 第一是以drawXXX为主的绘制方法: 第二是以clipXXX为主的裁剪方法: 第三是以scale.skew.translate和rotat ...
- 思考 Swift 中的 MirrorType 协议
Swift中的反射非常有限,仅允许以只读方式访问元数据的类型子集.或许 Swift 因有严格的类型检验而不需要反射.编译时已知各种类型,便不再需要进行进一步检查或区分.然后大量的 Cocoa API ...
随机推荐
- 理解并掌握Promise的用法
前沿: Promise在处理异步操作非常有用.项目中,与后端进行数据请求的时候经常要用到Promise.我们可以用promise + xhr进行ajax的封装.也可以使用基于promise封装的请求 ...
- python之数据驱动ddt操作(方法三)
import unittestfrom selenium import webdriverfrom selenium.webdriver.common.by import Byimport unitt ...
- Android Studio(或IntelliJ IDEA )把Android程序运行到由VirtualBox创建 Android x86虚拟机中
一.运行前相关配置 1.把Android sdk platform-tools目录下的adb程序加入到path环境变量,默认情况下是其路径是: C:/Users/ [User]/AppData/Loc ...
- Spring 学习笔记(2) Spring Bean
一.IoC 容器 IoC 容器是 Spring 的核心,Spring 通过 IoC 容器来管理对象的实例化和初始化(这些对象就是 Spring Bean),以及对象从创建到销毁的整个生命周期.也就是管 ...
- CF1032G Chattering
CF1032G Chattering 题意 思路 对于每一个位置,它转移的范围是确定的. 对于一段可以走到的区间,我们可以求出区间中所有点再能走到区间范围. 于是这个就可以倍增进行转移. 如何快速求出 ...
- Springboot通过过滤器实现对请求头的修改
之前在一个项目中有一个API服务需要重构,尤其是接口的用户身份校验,原先的实现是将用户token放在URL请求参数中,然后通过AOP进行校验,现在要统一将token放在header中,但是这样修改会让 ...
- 选择排序(selection_sort)——Python实现
# 选择排序 # 作用:对给出的n个顺序不定的数进行排序 # 输入:任意数组A # 输出:按顺序排列的数组A # 时间复杂度 (n(n-1))/2 # 选择排序 # 第一趟:选择第一个元素,依次与 ...
- Git,Linux,Ubuntu,Tmux的常用命令
常用的连接 Git命令 廖雪峰的Git教程 Git常用命令 ubuntu16.04之GitHub入门教程 Linux相关 tmux命令 Ubuntu常用命令速查手册 Linux 命令大全 其它工具 M ...
- XCTF-open-source
下载附件拿到源码. #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { if ( ...
- Linux进程理解与实践(五)细谈守护进程
一. 守护进程及其特性 守护进程最重要的特性是后台运行.在这一点上DOS下的常驻内存程序TSR与之相似.其次,守护进程必须与其运行前的环境隔离开来.这些环境包括未关闭的文件描述符,控制终端, ...