基于Unity的软光栅实现(2):CPU单线程软光栅

About

本系列文章是关于本人的开源项目 URasterizer: A software rasterizer on top of Unity, accelerated by Job system & Compute Shader的总结和介绍。
第一篇:基于Unity的软光栅实现(1):框架搭建和矩阵构造
第二篇:基于Unity的软光栅实现(2):CPU单线程软光栅 (本篇)

CPURasterizer

在上篇文章中,我们搭建了整个框架,使用RendringObject组件从Mesh上获取了模型数据并转换成我们需要的格式,获取场景中的Camera和灯光的参数,计算所需要的各个矩阵,以及其他场景信息Uniform。
本篇中,将介绍基于CPU单线程的软光栅渲染器 CPURasterizer。包括数据准备,缓冲区构建,顶点变换,图元集成,光栅化和片段着色,MSAA等内容,基本就是一个完整的光栅化流水线了。

渲染数据: CPURenderObjectData

每种渲染器使用的渲染数据,都是包含在RenderingObject中的,在RenderingObject激活时创建。这些数据是和Mesh一一对应的,根据不同渲染器的需求从Mesh中提取相应数据并处理。

输入数据

对于软光栅来说,从Unity模型获取的顶点坐标、法线、UV、索引等数据是可以直接使用的,虽然有手向性差异需要转换,但可以在使用数据时再转换。但是我们仍然需要注意一个问题,就是GC。如果直接在每帧渲染时通过mesh.vertices等方法获取数据,就会造成GC Alloc。以mesh.vertices为例,这是一个属性,看Unity的注释可以知道这个属性返回的是顶点位置的拷贝。

Returns a copy of the vertex positions or assigns a new vertex positions array.

首先copy顶点本身就是一个耗时的操作,然后返回的是一个数组,虽然顶点位置是Vector3本身是值类型,但是数组是引用类型。创建数组时,其成员也会在托管堆上分配,这就会造成每帧分配大量的托管堆。加上其他顶点属性,会造成非常重的GC Alloc。因此我们需要将这些数据缓存,这就是CPURenderObjectData做的事情。

中间输出数据

在流水线的开始,顶点属性会经过变换(vertex shading),变换结果也要保存,保存为一个 VSOutBuf 结构的数组,也放在CPURenderObjectData中。VSOutBuf的定义如下:

	public struct VSOutBuf
    {
        public Vector4 clipPos; //clip space vertices
        public Vector3 worldPos; //world space vertices
        public Vector3 objectNormal; //obj space normals
        public Vector3 worldNormal; //world space normals
    }

由于下面会在世界空间计算光照,因此会输出世界空间的位置和法线,另外由于我们有法线可视化的功能,因此也输出了本地空间的法线。裁剪空间的位置也是默认要输出的,用于后面的clipping操作。

缓冲区

缓冲区定义

上篇提到,CPURasterizer使用一个Texture2D作为最终的输出目标。但是直接操作Texture2D并不方便,且除了颜色缓存,还需要可视化深度缓存。因此CPURasterizer中使用数组保存这几个缓冲区:

		Color[] frame_buf;
        float[] depth_buf;
        Color[] temp_buf;

其中temp_buf的作用是深度缓冲可视化时,用于填充颜色。
颜色缓冲区frame_buf直接使用Color类型,深度缓冲使用float类型。
另外由于要支持MSAA,需要一些额外的缓冲区,具体在MSAA时介绍。

清除缓冲区

CPURasterizer实现IRasterizer接口的方法Clear,来清除缓冲区。其实就是简单的将数组设置为某个值。但是我们不能直接遍历数组,用一个大循环去设置,因为这样太慢了。如果是c/c++可以直接使用memcpy或memset,但是C#数组不支持这个。CPURasterizer使用了一个快速的数组填充方法:

		public static void FillArray<T>(T[] arr, T value)
        {
            int length = arr.Length;            
            if(length == 0){
                return;
            }
            arr[0] = value;

            int arrayHalfLen = length/2;
            int copyLength;
            for(copyLength = 1; copyLength <= arrayHalfLen; copyLength<<=1){
                Array.Copy(arr, 0, arr, copyLength, copyLength);
            }
            Array.Copy(arr, 0, arr, copyLength, length-copyLength);            
        }

基本原理就是使用Array.Copy循环成倍copy。经过简单测试,使用这个方法填充数组,clear函数的时间消耗从2.5ms左右降低到不到0.5ms,提升还是很明显的。

视锥剔除

Frustum culling其实是应用阶段的工作,不属于图形流水线。但因为我们是自己渲染的,不能利用Unity的culling,所以自己实现了一下。关于Frustum culling的实现,网上有很多介绍,大多数都是获取6个裁剪面的平面,然后针对这6个平面,分别判断AABB的8个点有同时在该平面外面的情况,如果有则被剔除。这种方法是保守剔除,这是正确的,不会错误剔除掉不该剔除的AABB。但是可能有一些特殊位置的AABB剔除不掉。视锥剔除看似简单,其实有很多可研究的地方,本项目就没有展开了。不过本项目也使用了一个不常用的方法,即clip space下的OBB视锥剔除。和AABB的保存剔除方法其实一样。只是是在clip space中进行剔除,将Rendering Object的local AABB使用MVP矩阵变换到clip space中,得到clip space中的OBB。然后利用clip space中顶点x,y,z分量在[-w,w]中的性质进行剔除的检查。这个方法省去了提取6个剪裁面的麻烦,且点和面的比较从点积比较简化成直接比较坐标,虽然没有详细分析,但我觉得这个方法应该是优于常规的视图空间的剔除。特别是对于视锥剔除,我们只要知道是否在视锥体外面即可,不需要判断是否则完全在内部以及是否有相交的情况。至于使用矩阵变换AABB的消耗,我觉得虽然比直接用某些特殊方法转换AABB要费一些,但是他得到的是OBB更加精确,更不用提网上的很多方法也是直接变换AABB。

顶点变换

计算包括clip space pos在内的所有需要的顶点属性,保存在 vsOutput中。矩阵乘法直接使用Matrix4x4的operator *即可。由于是单线程计算,直接一个大循环处理所有顶点。

图元集成

Clipping

通过索引获取到三角形三个顶点之后,首先进行Clipping操作。三角形Clipping操作,对于部分在clipping volume中的图元,硬件实现时一般只对部分顶点z值在near,far之间的图元进行clipping操作,而部分顶点x,y值在x,y裁剪平面之间的图元则不进行裁剪,只是通过一个比viewport更大一些的guard-band区域进行整体剔除(相当于放大x,y的测试范围)。这样x,y裁剪平面之间的图元最终在frame buffer上进行Scissor测试。URasterizer的实现简化为只整体的视锥剔除,不做任何clipping操作。对于x,y裁剪没问题,虽然没扩大region,也可以最后在frame buffer上裁剪掉。对于z的裁剪由于没有处理,会看到整个三角形消失导致的边缘不齐整。clipping直接使用了视锥剔除的算法,只是判断的是三角形的3个顶点,而不是OBB的8个顶点。需要注意的是(包括视锥剔除也是),和 w 比较时,要知道clip pos的w值不一定是正数。由于NDC中总是满足-1<=Zndc<=1, 而当 w < 0 时,-w >= Zclip = Zndc*w >= w。所以此时clip space的坐标范围是[w,-w], 为了比较时更明确,将w取正,这样始终比较 -w <= x,y,z <= w。

Backface culling

这个简单,直接使用三角形两个相邻边的叉积,判断叉积的z值小于0则被剔除。

Viewport transform

将NDC的坐标变换到屏幕坐标,对于x,y坐标就是正常从[-1,1]变换到[0,screenWidth-1]和[0,screenHeight-1]。在硬件渲染中,NDC的z值经过硬件的透视除法之后就直接写入到depth buffer了,如果要调整需要在投影矩阵中调整。由于我们是软件渲染,所以可以在这里调整z值。GAMES101约定的NDC是右手坐标系,z值范围是[-1,1],但n为1,f为-1,因此值越大越靠近n。为了可视化Depth buffer,将最终的z值从[-1,1]映射到[0,1]的范围,因此最终n为1, f为0。离n越近,深度值越大。由于远处的z值为0,因此clear时深度要清除为0,然后深度测试时,使用GREATER测试。当然我们也可以在这儿反转z值,然后clear时使用float.MaxValue清除,并且深度测试时使用LESS_EQUAL测试。注意:这儿的z值调整并不是必要的,只是为了可视化时便于映射为颜色值。其实也可以在可视化的地方调整。但是这么调整后,正好和Unity在DirectX平台的Reverse z一样,让near plane附近的z值的浮点数精度提高。

光栅化三角形

光栅化使用的是重心坐标法,即首先计算三角形的AABB,对在AABB范围内的像素计算重心坐标,当重心坐标alpha,beta,gamma均大于0时,则该像素在三角形内,需要计算颜色并进行深度测试最终将颜色填充到color buffer中。我之前是使用IsInsideTriangle方法,通过叉积判断点是否在三角形内,但后来通过profile发现,使用重心坐标判断效率更高。

透视校正插值

GAMES101的作业框架由于存在错误,导致透视校正插值失效了。URasterizer中修复了这点,对所有顶点属性都进行了透视校正插值。
在这里插入图片描述
上图中所有格子都是相同大小,通过透视校正,可以做到近大远小。

片段着色

着色方法没啥好说,说一下获取贴图数据。由于为了兼容Job,这儿给CPURasterizer使用的贴图数据也直接使用了NatvieArray。获取方法如下:

input.TextureData = ro.texture.GetPixelData<URColor24>(0);

其中TextureData的定义如下:

public NativeArray<URColor24> TextureData; //因为我们的纹理都是RGB格式的(24位),所以不能用Color32(32位)

这里我们获取的是mip0的贴图数据,即原始贴图数据。因为使用的贴图都RGB24位的,所以这里的颜色结构体不能使用Color32,只能自己定义了一个URColor24的结构体。另外为了可以读取贴图数据,需要在贴图的导入设置里面将read/write enabled打开。

MSAA

MSAA是在子像素级别进行深度测试,且color buffer也扩大为可以存储所有可见的子像素的颜色。另外还需要记录哪些子像素可见。URasterizer使用了如下的几个buffer:

		Color[] samplers_color_MSAA;
        bool[] samplers_mask_MSAA;
        float[] samplers_depth_MSAA;

光栅化之后,需要进行Resolve操作,根据可见子像素的颜色和数量,计算最终像素的颜色。

输出到贴图

渲染完成后,在 UpdateFrame 中,需要将颜色缓冲的数据设置到贴图上。由于URasterizer支持可视化深度缓冲,所以会根据使用的模式,选择不同的缓冲区数据,设置给贴图。以下是深度可视化效果,分别使用R通道以及RGB通道:
在这里插入图片描述
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值