光栅化渲染器

渲染器源代码

基本管线概念

http://claireswallet.farbox.com/post/directx/shi-shi-xuan-ran-tu-xing-guan-xian
http://claireswallet.farbox.com/post/directx/d3d11de-tu-xing-guan-xian#main

性能

纠结于到底是使用内存池进行动态分配还是直接使用vector进行预分配,有多的需求再动态拓展。直接使用new进行分配是断然不行的。

三角光栅化还遇到一个精度问题,拜我的设计的问题,我在光栅化的时候需要同时插值属性,于是int类型的变量有时候会赋值给float,这时候会出现扰动,2.00000会变成1.99999,如果这个不修正,在后面的某一个步骤,会直接截尾。这会导致扫描缺行,或者扫描出“洞”。
所以我使用了round函数,然而这个函数的性能我目前尚不知晓。这个问题也许需要彻底更改一个方案。

【2015.11.21】最近写了个Profiler来看了一下自己的程序,发现瓶颈竟然在PS阶段,整个程序有50%以上的时间花在了PS的计算上,进一步查看发现原来的各种buffer为了方便都是用map来使用字符串索引的,于是直接改成了数组就解决了。
但是PS的效率其实还是很低,然后我多次查看了一下占用时间多的几个函数,发现都是很简单的函数,排除了多态引起的因素,实在没有办法要加速只能靠多线程了。

多线程

多线程的设计,根据Profiler的结果我是把最消耗时间的PS和OM阶段拿到了另外一个线程。因为以前没做过多线程,第一次使用C++11的多线程库搞了一下,发现性能并没有多大提升,但是谢天谢地没有下降。
我的实现方案是,使用一个简单的queue来储存主线程产生的片元数据,然后由PS-OM线程来逐个消化这些数据。在访问queue的时候,我使用了锁来防止竞争(但是我并不是很确定这个函数到底有多大消耗,最佳的方案又是什么?):

    void access(bool read, VertexP3N3T2& data)
    {
        lock_queue.lock();
        if (read)
        {
            data = queue.front();
            queue.pop();
        }
        else
        {
            queue.push(data);
        }
        lock_queue.unlock();
    }

等到主线程产生完成了所有的片元生产,就发送一个终结信息到queue中,然后等待PS-OM线程完成处理。PS-OM线程检测到queue中的终结信号就意味着本次Pass完成,于是发送一个信号给主线程终止等待。但是直觉上讲这么做其实设计的并不太合理,因为主线程相邻两次写入queue的频率非常之高,会导致不平衡。但是现在能快速实现这种方案,所以抱着试一试的心理实验了一下。
果不其然,经过Profile发现,主线程等待PS-OM线程消耗了原来单线程PS-OM阶段时间60%以上的时间。进一步看了下,发现queue最长涨到了1万以上,这导致了内存和性能的双重灾难,所以现在考虑还是等空下了要仔细重新搞一下了。
计划看一下其他的多线程系统是怎么实现的,深入学习一下多线程API。

SIMD指令优化

三角光栅化

三角光栅化目前用的线扫。先前是把三角分成了平顶三角和平底三角两种,如果遇到斜三角形就把三角形分成一个平顶一个平底三角。对齐规则因为我的坐标原点在左下角,所以是左下对齐。在实现的时候,使用下列步骤来光栅化一个三角形:

  • 把所有坐标xy转换到viewport的浮点坐标
  • 检测退化为直线的三角形
  • 对三角形顶点y排序,从0~2号顶点,y值逐渐增大
  • 线扫平顶三角或者平底三角或者斜三角

线扫平顶算法:

  • 创建新的片元
  • 转换y到像素点
  • 从下到上按照y一排一排线扫x
  • 增加x始终点一个斜率倒数
  • 插入新片元

平底也差不多。线扫算法就简单了,但是有一种是要在线扫的时候进行裁剪,我并没有采用这种方案。

上面的算法注意不要去修改原来的顶点值。

对于斜三角形,有种算法是把它分成一个平顶和一个平底三角形来处理,但是在我的算法中,如果这样处理会出现问题导致出现很多“洞”。问题记录如下:


如上图所示,如果按照我的算法,一个四边形的精确位置是上图的黑线,那么左边的平底三角形和右边平底三角形的实际光栅化位置可能是红色位置。因为我总是从下到上扫描,增量按照斜率计算与最上面的点位置无关(也就是说三角形最上面的点并不是通过浮点数光栅化出来的,而是在光栅上扫描出来的)。所以光栅化出来的中间的斜边总是和实际的边平行,但是A1所在的光栅化边却并不一定和B1所在的边重合。这就有可能导致斜边结合处少填或者多填像素,多填看不出来(实际上也有影响),少填表现就是有“洞”。
所以,我最后决定单独填充一个斜三角,而不是把它剖开。
新的算法和平顶、底三角形扫描差不多,只是到了一边转折点要修改斜率。
还有就是要判断y第二大的点在三角形的左边还是右边,我当前使用的是暴力方法,直接计算出切分点的x坐标然后和y第二大点的x来比较的方法,感觉还有更简单的方案,一时没想出来。
斜三角形扫描算法主要是先把三角形的顶点的所有y值全部按照对齐规则更新一次,得到一个光栅好y的三角形,然后计算光栅后三角形的edge equation作为光栅化边。
为了清晰精确(实在是被各种“洞”搞怕了,生怕有任何数据误差之类的),我多求出了5个顶点,都是使用插值求的,在这里我并没有考虑太多的效率。

使用插值方法求出五个蓝色点顶点。
在线扫三角形的时候,我从B所在直线把整个三角形分为上下两个部分。
从下到上扫描——扫描下半三角的时候,y的范围是[y0,y1-1],起点为y0所在顶点,上半是[y1,y2-1],起点是三角形的一个顶点和B,不扫y2是因为左下规则
最后,这种扫描方案还有一个重要的细节,如果三角形y被光栅化之后,成了一条直线,不能直接丢弃直线,而应该把这条直线画出来,这和前面的退化三角形检测不一样。

最终由于新的斜三角型扫描函数和平顶、底扫描函数不能良好的结合,所以最终所有的三角扫描的任务全部落在斜三角扫描函数上。

对齐规则

左、上填充规则:水平边要们是上边,要么是底边;斜边要么是左边,要么是右边.
这个问题的本质是,我们填充两个点(这两个点恰好都是0.5小数位)间的像素,被填充的位置中心恰好会和像素的边对齐,而这样是无法填充的,要填充必须要把填充中心和像素中心对齐,“左上”的意思就是——填充中心和左边、上面的像素中心对齐。
这个定义是根据代码来定义的,从上到下扫描,从左到右扫描,左上规则是

for(int y = ymin; y<ymax; y++)
{
    for(int x = xmin; x<xmax; x++){}
}

也就是从最小的开始,最大的一个不填。
对应的有,右下填充就是

for(int y = ymin+1; y<=ymax; y++)
{
    for(int x = xmin+1; x<=xmax; x++){}
}

左下:

for(int y = ymin+1; y<=ymax; y++)
{
    for(int x = xmin; x<xmax; x++){}
}

右上:

for(int y = ymin; y<ymax; y++)
{
    for(int x = xmin+1; x<=xmax; x++){}
}

代码实现上就是循环的端点取值范围不一样。不过仅供参考。
如果,端点的取值范围不同,要保证填充对应的像素,就必须要确定起止像素。
起止像素对于0.5的要成立,对于一般情况也一样要成立。所以首先要确定好,像素的中心在像素中心还是在像素左上角,然后开始对x,y轴分别分析即可。
总之这个问题不是一个什么问题,按照需求分析,再按照最前面的粗体字处理就好了。
最终根据我的坐标系统我采取了左下规则。

1/Z插值

直观的说,如果不使用1/z插值投影的话,纹理会是这样的 :

使用后是这样的:

用棋盘格看的非常清楚,使用其他纹理不一定看的分明。差距非常明显。
理论上的原因,看下图:

从Eye往3D Line划线,初中学习的三角学几何知识告诉我们,Near面上的3D line投影的变化比例跟3D空间里面变化比例是不一样的。所以如果直接在屏幕空间线性插值屏幕空间量(如uv),会导致扭曲。

然而这种扭曲可以接受,一般在viewport精度高、三角形距离近裁剪面远、三角形没有很水平的时候不会太显现出来。因为他是关于z值的,但是这种屏幕空间的线性插值是错误的。

这一切都是透视惹的祸。
这里说的都是屏幕空间量,现在拿uv说事。uv在3d空间中是按照空间坐标(xyz)线性插值计算的,透视后投影到投影平面,空间量变成了屏幕空间量,所以在屏幕空间插值要按照1/z来插值才成。因为恰好1/z在屏幕空间是线性的。
推导1/z的过程,可以参考:http://imgtec.eetrend.com/article/1940
另外,要提一下D3D中的矩阵变换,其中透视矩阵,保留了z值在w处,前面的xyz都是乘以了z值的。所以在传入管线之后,要把每一个xyz值都除以一个w。这也解释了xyz为什么可以按照屏幕空间直接线性插值。uv没有经过任何变换,因此也不在屏幕空间,前面也说了其实他们是在原xyz的3D空间里面的。
而w!(也就是空间中的z)不能直接线性插值。因为这个量不在屏幕空间。我当时在插值uv的时候,只把uv按照1/z插值,而把z依然在屏幕空间直接线性插值,这导致我画出来这样的图样:

差距并不明显,如果很远的话根本看不出来。但是在这个距离,可以发现对角线明显走样严重,右下角也有扭曲,产生这个现象的原因是斜三角形我是划分为上下来扫描的。
总之,一句话,线性插值的时候,在哪个空间的量就应该在哪个空间插值,也就是插值的参考应该和目标量在同一个空间。

最后说一句,这里插值的法线不是准确的,因为法线是空间量,但是插值参数是在透视空间算出来的。但是误差不太大。

使用

API

SoftRApp中可以设置渲染的模型(目前只支持obj格式)和纹理。

  //创建渲染管线并初始化输出设备
  SrPipeline* pip = new SrPipeline();
  pip->set_out_tex(out_tex);
  //读取obj模型数据并初始化顶点buffer
  RBObject* obj = RBObject::create_object();
  obj->load_mesh("objs/tri.obj");
  obj->generate_softr_buffer<VertexFormats::Vertex_PCNT>();
  obj->_node->set_position(0, -1,0);
  obj->_node->rotate(-30, 195, 0);
  //创建着色器
  BaseShaderPS* ps = new BaseShaderPS();
  BaseShaderPS* vs = new BaseShaderVS();
  //创建常量结构体
  ShaderMatrixBuffer* mb = new ShaderMatrixBuffer();
  //创建常量buffer,并写入数据
  SrBufferConstant* bf = new SrBufferConstant();
  bf->init(mb, sizeof(ShaderMatrixBuffer));

  //创建纹理
  SrTexture2D* tf = SrTexture2D::creat("Res/pp.jpg");

  //...
  //draw call准备
  obj->_node->get_mat(mb->m);
  cam->get_view_matrix(mb->v);
  cam->get_perspective_matrix(mb->p);
  //出于方便使用名字索引shader uniform变量,但是效率及其低下
  //vs->set_constant_buffer("matrix", bf);
  //ps->set_texture("texture", tf);
  //使用数组直接用索引来处理提升50%性能
  vs->set_constant_buffer_index(0,bf);
  ps->set_texture_index(0,tf);
  pip->set_ps(ps);
  pip->set_vs(vs);
  //draw call
  pip->draw(*obj->get_softr_vertex_buffer(), *obj->get_softr_index_buffer(),obj->get_index_count()/3);

Shader例程

目前shader使用C++写成,只需要继承shader父类即可。

VS例子:

#include "ShaderVertex.h"
#include "..\\RBMath\\Inc\\Matrix.h"
#include "BufferConstant.h"
struct ShaderMatrixBuffer
{
    RBMatrix m;
    RBMatrix v;
    RBMatrix p;
};
class BaseShaderVS : public SrShaderVertex
{
public:
    void shade(VertexP3N3T2& v)
    {
        VertexP3N3T2 iv = v;
        //SrBufferConstant* mb = get_cbuffer("matrix");
    SrBufferConstant* mb = get_cbuffer_index(0);
        ShaderMatrixBuffer* cmb = (ShaderMatrixBuffer*)(mb->_data);
        v.position = iv.position * cmb->m;
        v.position = v.position * cmb->v;
        v.position = v.position * cmb->p;
        RBMatrix rot = cmb->m.get_rotation();
        v.normal = RBVector4(v.normal,1) * rot;
    }
};

PS例子:

#include "ShaderPixel.h"
#include "Texture2D.h"
#include "SamplePoint.h"
class BaseShaderPS : public SrShaderPixel
{
public:
    RBColorf shade(VertexP3N3T2& vert_lerp)
    {
        //SrTexture2D* tex = get_texture2d("texture");
    SrTexture2D* tex = get_texture2d_index(0);
        SrSamplerPoint sp;
        RBColorf tc = sp.sample(tex, vert_lerp.text_coord.x, vert_lerp.text_coord.y);
        vert_lerp.normal.normalize();
        RBVector3 v = vert_lerp.position;
        v = v.get_abs();
        v.normalize();
        RBVector3 light(-1,-1,-1);
        float cost = RBVector3::dot_product(vert_lerp.normal, light);
        RBColorf oc = RBColorf::white;
        if (cost <= 0)
            oc = RBColorf::black;
        RBVector4 outc = oc;
        outc = outc * cost;
        outc.w = 1.0;
        if (outc.x > 1) outc.x = 1.f;
        if (outc.y > 1) outc.y = 1.f;
        if (outc.z > 1) outc.z = 1.f;
        if (tc.a < 0.0001)
            RBColorf f = RBColorf::black;
        RBColorf c =outc* tc;
        return c;
    }
};

渲染图样

目前还只是个原型。Updating……

纹理采样只写了点采样,有点烂。

渲染器源代码

你可能感兴趣的:(光栅化渲染器)