草庐IT

软光栅从零开始——绘制线框、背面剔除、zbuffer

chenglixue 2023-03-28 原文

在此篇中,我们将学习如何绘制一个三角形并对其进行着色,如何判断屏幕中同一个像素位置顶点的前后顺序

绘制三角形和平面着色

​ 绘制图形,我们需要画线也就需要学习画线算法,但图形种类多种多样,为什么我们选择学习三角形呢?

​ 因为三角形是最基本的多边形,其拥有许多特性:

  • 三角形可以分解其他多边形,也就是我们可以不断分解其他多边形,最终形成有多个三角形组成的多边形
  • 三角形保证是平面的
  • 三角形可以用叉乘判断内外
  • 三角形内部可以定义插值

​ 在讲明为什么我们选择三角形作为最基本的图形后,我们现在来学习如何绘制一个三角形

​ 按照之前的画线算法,我们可以很轻松的画出三角形来,如下图。但问题是我们平时在游戏中看到的丰富多彩的画面,很明显三角形内部是被填充了的,那么请想想我们如何对三角形进行填充呢?

​ 实际上一个填充了的图形也是由许多许多线组成的,也就是我们可以在三角形内部从下往上画许多许多的水平线将其填充。具体实现上我们会将三角形分成上下两部分,从下向上填充

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) { 
    
    // 将三角形三个顶点y这一维度按从小到大的顺序排列
    if (t0.y>t1.y) std::swap(t0, t1); 
    if (t0.y>t2.y) std::swap(t0, t2); 
    if (t1.y>t2.y) std::swap(t1, t2); 
    
    int total_height = t2.y-t0.y; 	//最大高度。三角形从最y维度上最大的点到最低的点的差
    
    //填充三角形下半部分
    for (int y=t0.y; y<=t1.y; y++) { 
        
        int segment_height = t1.y-t0.y+1; 	//y维度上,t1到t2的高度差
        float alpha = (float)(y-t0.y) / total_height; 
        float beta  = (float)(y-t0.y) / segment_height;
        Vec2i A = t0 + (t2-t0) * alpha; 
        Vec2i B = t0 + (t1-t0) * beta; 
        
        if (A.x>B.x) std::swap(A, B); 
        for (int j=A.x; j<=B.x; j++) { 
            image.set(j, y, color); // 填充
        } 
    } 
    
    //填充三角形上半部分,方法同上
    for (int y=t1.y; y<=t2.y; y++) { 
        int segment_height =  t2.y - t1.y + 1; 
        float alpha = (float)(y-t0.y) / total_height; 
        float beta  = (float)(y-t1.y) / segment_height;
        Vec2i A = t0 + (t2-t0) * alpha; 
        Vec2i B = t1 + (t2-t1) * beta; 
        if (A.x>B.x) std::swap(A, B); 
        for (int j=A.x; j<=B.x; j++) { 
            image.set(j, y, color);
        } 
    } 
}

​ 虽然以上方法离结果已经很近了,但想想这样绘制出的模型,终究是用单调的颜色填充出的,这跟我们平时看的模型相差甚远,如下,这明显着色不够充分,少了明暗,没有体积感

​ 来回想一下,在图形学中我们学到了着色,那什么是着色呢?引入明暗和不同颜色的过程叫做着色,颜色有了我们还差光照,接下来我们来实现光照

​ 在生活中,我们可以观察到当光线与平面垂直时,光照强度是最强的,也就是说光线与平面的夹角越大,光照越强,夹角越小,光照越弱。因此,我们用光线的负方向 * 法线方向 * cosθ来表示光照强度。对强度为负的情况,相当于夹角在[180,360],这种所求的平面在相机方向是看不到的,我们会忽略它,这也就是背面剔除

​ 那么一个三角形面的法向量如何求呢?我们可以使用叉乘,将三角形两条边上的向量进行叉乘,会得到一个垂直两向量的向量。在下图也就是$$\vec{AC} × \vec{AB}$$。这时存在一个问题,若调换C和B,也就是$$\vec{AB} × \vec{AC}$$此时法向量与之前方向截然相反,对于这种情况我们应该如何处理呢?在模型的制作过程中规定好顶点的顺序,保证每个平面的法向量朝向模型外面

注:如何判断两个相同向量互换位置后叉乘的正负?对于左手坐标系来说,伸出左手,右手坐标系则伸出右手,除开大拇指,我们并拢其余四指,指向前一个向量的方向,然后四指向后一个向量的位置弯曲,,此时大拇指的朝向若是向上则是正,向下为负

Vec3f light_dir(0,0,-1); // 光源相对于物体的位置,光照的负方向

for (int i=0; i<model->nfaces(); i++) { 
    std::vector<int> face = model->face(i); 
    Vec2i screen_coords[3]; 
    Vec3f world_coords[3]; 
    for (int j=0; j<3; j++) { 
        Vec3f v = model->vert(face[j]); 
        screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.); 
        world_coords[j]  = v; 
    } 
    Vec3f n = (world_coords[2]-world_coords[0])^(world_coords[1]-world_coords[0]); 
    n.normalize(); 
    float intensity = n*light_dir; 
    if (intensity>0) { 
        triangle(screen_coords[0], screen_coords[1], screen_coords[2], image, TGAColor(intensity*255, intensity*255, intensity*255, 255)); 
    } 
}

​ 结果如上图,是不是感觉有些违和感?没错,这个眼睛和嘴巴有些奇怪,原因是因为这两个地方都有一层内腔,我们渲染的时候也将内腔给渲染出来了,覆盖了本来显示的眼睛和嘴巴

zBuffer

​ 之所以会出现内腔覆盖眼睛和嘴巴,是因为我们渲染的时候并未确立这两个顶点的前后顺序,从而导致的覆盖,因此我们需要想个办法来区分前后顺序

​ 想想在我们生活中,我们画画如下图,我们会先画后面的山,再画前面的草地和树,那我们是不是可以将这种方法应用于此呢?答案是不行的,因为计算机无法分辨这些物体的前后关系,emm它只会按照顶点顺序来绘制

​ 转换思路,既然每个按照上面的方法无法确定顺序,那我们是否可以确立屏幕范围内上每个像素位置上的点的深度值,根据这个深度值来确定我们面前能够看到的,也就是说,这样既能确立应该将哪个顶点绘制出来,又可以不关心绘制顺序

​ 那么这个深度值如何求得呢?我们只知道顶点的z坐标,不知道三角形内部的啊

​ 这个时候就需要用到重心坐标求插值的方法了

​ 那么什么是重心坐标(我们只考虑三角形)呢?如下图,给定一个三角形ABC,该平面内任意一点都可以用三个顶点的线性组合进行表示:$$(x,y) = \alpha A + \beta B + \gamma C$$,其中$$α+β+γ = 1$$,我们将三个系数αβγ看作点A,B,C的权重,此时我们称这个点(α,β,γ)就是三角形的重心,这种坐标系又称重心坐标系。值得注意的是,当三个系数都为非负数时,这个点在三角形内部

利用面积来计算确定系数

利用坐标计算确定系数

在坐标系角度来求解。我们将重心坐标系中a点看作原点,$$\vec{ab}和\vec{ac}$$看作基向量,任意点p可以表示为$$p = a + \beta (b-a) + \gamma (c-a)$$,移项后可得$$p = (1-\beta - \gamma)a + \beta b + \gamma c$$,定义$$\alpha = 1 - \beta - \gamma$$,最终依旧可以得出$$ p = \alpha a + \beta b + \gamma c$$

看完这些,那么为什么重心坐标是三角形顶点属性的平滑过渡呢?因为这个三系数时线性变化,那么它们每经过一个位置都是均匀变化的

注意!重心坐标插值,投影前后重心坐标可能会发生变化,所以需要在对应的阶段计算重心坐标

​ 那我们我们为什么需要插值呢?由于很多操作都是基于顶点完成,我们希望在三角形内完成平滑的过渡,插值的内容很多,如纹理坐标,颜色,法向量等等;用处很大,不仅在软件光栅化,而且在如雷贯耳的光线追踪中同样会使用

​ 补充一点,重心坐标可以求一个点是否在三角形内部,但我们可以用另一种方法来快速判定。我们事先需要知道一个三角形三个点的顶点坐标,以及要求的点的坐标,分别计算$$\vec{P0P1} × \vec{P0Q},\vec{P1P2} × \vec{P1Q}, \vec{P2P0} × \vec{P2Q}$$,若三个值同为正或同为负,则在三角形内部,否则在外部。其原理也就是重心坐标

​ 那么接下来的实现就十分简单了。

//判断点是否在三角形内
bool isInsideTriangle( Vec3f* v, int x, int y )
{
    Vec2f side1 = { v[1].x - v[0].x, v[1].y - v[0].y };
    Vec2f side2 = { v[2].x - v[1].x, v[2].y - v[1].y };
    Vec2f side3 = { v[0].x - v[2].x, v[0].y - v[2].y };

    Vec2f v1 = { x - v[0].x, y - v[0].y };
    Vec2f v2 = { x - v[1].x, y - v[1].y };
    Vec2f v3 = { x - v[2].x, y - v[2].y };

    float z1 = side1.x * v1.y - v1.x * side1.y;
    float z2 = side2.x * v2.y - v2.x * side2.y;
    float z3 = side3.x * v3.y - v3.x * side3.y;

    if( ( z1 > 0 && z2 > 0 && z3 > 0) || ( z1 < 0 && z2 < 0 && z3 < 0 ) ) return true;
    return false;
}

//计算重心坐标
Vec3f computeBarycentric2D(float x, float y, Vec3f* v)
{
    float c1 = (x * (v[1].y - v[2].y) + (v[2].x - v[1].x) * y + v[1].x * v[2].y - v[2].x*v[1].y) / (v[0].x * (v[1].y - v[2].y) + (v[2].x - v[1].x) * v[0].y + v[1].x * v[2].y - v[2].x * v[1].y );
    float c2 = (x * (v[2].y - v[0].y) + (v[0].x - v[2].x) * y + v[2].x * v[0].y - v[0].x*v[2].y) / (v[1].x * (v[2].y - v[0].y) + (v[0].x - v[2].x) * v[1].y + v[2].x * v[0].y - v[0].x * v[2].y );
    float c3 = (x * (v[0].y - v[1].y) + (v[1].x - v[0].x) * y + v[0].x * v[1].y - v[1].x*v[0].y) / (v[2].x * (v[0].y - v[1].y) + (v[1].x - v[0].x) * v[2].y + v[0].x * v[1].y - v[1].x * v[0].y );
    return Vec3f(c1, c2, c3);
}

//绘制三角形
void drawTriangle( Vec3f* v, TGAImage& image, TGAColor color )
{
    
    float minX = std::min( v[0].x, std::min( v[1].x, v[2].x ) );
    float maxX = std::max( v[0].x, std::max( v[1].x, v[2].x ) );
    float minY = std::min( v[0].y, std::min( v[1].y, v[2].y ) );
    float maxY = std::max( v[0].y, std::max( v[1].y, v[2].y ) );

    for( int i = minX; i <= maxX; ++i )
    {
        for( int j = minY; j <= maxY; ++j )
        {
            if( isInsideTriangle( v, i, j ) == true ) 
            {
                //重心坐标插值求z值
                auto bc_screen = computeBarycentric2D( i, j, v );
                float tempZ = v[0].z * bc_screen.x / 1.0f + v[1].z * bc_screen.y / 1.0f + v[2].z * bc_screen.z / 1.0f;

                //更新z值
                if( tempZ > zBuffer[j][i] )
                {
                    zBuffer[j][i] = tempZ;
                    image.set( i, j, color );
                }

            }

        }
    }
}

//
int main(int argc, char** argv)
{
    //...
    for( int i = 0; i < model->nfaces(); ++i )
    {
        
        //取顶点索引值
        std::vector<int> face = model->face(i); 
        Vec3f screen_coords[3];     //屏幕坐标

        Vec3f world_coords[3];  //世界坐标

        for( int j = 0; j < 3; ++j )
        {

            //取当前索引值对应的顶点信息
            //这个顶点信息就是这个当前这个顶点在世界空间下的坐标
            world_coords[j] = model->vert( face[j] );   

            //将取到的顶点变换到屏幕坐标
            //obj文件中的顶点每个维度上取值范围都在[-1,1]间,我们需要将其变换至屏幕坐标[0,0]到[width,height]这个范围内
            screen_coords[j] = {  ( world_coords[j].x + 1 ) * width / 2 ,  ( world_coords[j].y + 1 ) * height / 2 , world_coords[j].z  };    

        }

        Vec3f n = (world_coords[2] - world_coords[0]) ^ ( world_coords[1] - world_coords[0]);
        n.normalize();

        float intensity = n * light_dir;
        
        if( intensity > 0 ) drawTriangle( screen_coords, image, TGAColor( intensity * 255, intensity * 255, intensity * 255, 255 ) );
    }
    //...
}

有关软光栅从零开始——绘制线框、背面剔除、zbuffer的更多相关文章

  1. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  2. ruby-on-rails - Ruby/Rails 中的夏令时开始和结束日期 - 2

    我正在开发一个Rails应用程序,我需要在其中找到给定特定偏移量或时区的夏令时开始和结束日期。我基本上在我的数据库中保存了从用户浏览器接收到的时区偏移量(“+3”,“-5”),我想在它出现时修改它由于夏令时的变化。我知道Time实例变量有dst?和isdst方法,如果存储在它们中的日期在夏令时与否。>Time.new.isdst=>true但是使用它来查找夏令时的开始和结束日期会占用太多资源,而且我还必须为我拥有的每个时区偏移量执行此操作。我想知道更好的方法。 最佳答案 好的,基于你所说的和@dhouty'sanswer:您希望能够

  3. ruby-on-rails - phusion passenger 和 ruby​​ 1.9.1 已经开始工作了吗? - 2

    我有一台生产机器和一台开发机器,都运行ubuntu8.10并且都运行最新的phusionpassenger。当我在osx上的本地开发机器上使用ruby​​1.9.1时,我想知道外面的人是否已经在使用带有ruby​​1.9.1甚至1.9.2的phusionpassenger?如果是这样,请告诉我们您的设置!此外,有没有办法在apache上使用phusionpassenger同时运行ruby​​1.8.7(ree)和1.9.1?感谢您的指点,我在任何地方都找不到任何提示... 最佳答案 是的,从某些2.2.x版本开始就正式支持它,我不记

  4. ruby - Rails 3 - 我可以将开始日期设置为 date_select 方法吗? - 2

    date_select方法只能设置:start_year,但我想设置开始日期(例如3个月前的日期)(但没有这样的选项)。那么,我可以将开始日期设置为date_select方法吗?或者,要制作这样的选择框,我应该使用select_tag和options_for_select吗?或者,有什么解决办法吗?谢谢, 最佳答案 有可能……例如:start_year–设置年份选择的开始年份。默认为Time.now.year-5参见thisresource. 关于ruby-Rails3-我可以将开始日期

  5. ruby - 从特定索引开始迭代数组 - 2

    我想从特定索引开始遍历数组。我该怎么做?myj.eachdo|temp|...end 最佳答案 执行以下操作:your_array[your_index..-1].eachdo|temp|###end 关于ruby-从特定索引开始迭代数组,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/44151758/

  6. ruby - Heroku - 如何开始工作人员(延迟工作)? - 2

    我有一些使用delayed_job的小程序。在我的本地主机上一切正常,但是当我将我的应用程序部署到Heroku并单击应该由delayed_job执行的链接时,没有任何反应,“任务”只是保存到表delayed_job中。Inthisarticleonherokublog写入时,执行delayed_job表中的任务,当运行此命令时rakejobs:work。但是我怎样才能运行这个命令呢?命令应该放在哪里?在代码中,还是从终端控制台? 最佳答案 如果您正在运行Cedar堆栈,请从终端控制台运行以下命令:herokurunrakejobs:

  7. node.js - 从未编写过任何自动化测试,我应该如何开始行为驱动开发? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。多年来,我一直在使用多种语言进行编程,并且认为自己总体上相当擅长。但是,我从未编写过任何自动化测试:没有单元测试,没有TDD,没有BDD,什么都没有。我已经尝试开始为我的项目编写适当的测试套件。我可以看到在进行任何更改后能够自动测试项目中所有代码的理论值(value)。我可以看到像RSpec和Mocha这样的测试框架应该如何使设置和运行所述测试变得相当容易

  8. ruby - 是否有任何命令可以使用 vim 转到 Ruby block 的末尾(或开始) - 2

    有没有办法使用vim结束Rubyblock?例如moduleSomeModule#defsome_methodendend我想用一个命令从光标所在的位置移动到block的末尾,这可能吗?我读过thisdocumentation,但它似乎不适用于.rb文件,我在某些地方读到它只适用于C(虽然还没有尝试过)。提前致谢。 最佳答案 rubyforge好像有官方包对此有一些支持:TheRubyftpluginnowincludesRubyspecificimplementationsforthe[[,]],[],][,[m,]m,[M,an

  9. ruby-on-rails - 从 Ruby 1.9.x、Rails 3 和/或 Merb 开始 - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。关闭7年前。Improvethisquestion我需要认真阅读Ruby1.9.1和即将推出的Rails3/Merb的变化。人们可以推荐任何文章阅读吗?并不是真的在寻找一个答案,只是在寻找人们正在使用的资源汇编,以跟上即将发生的事情和当前存在的事情,所以如果你路过,请告诉我你在看什么。谢谢!

  10. ruby-on-rails - 开始新的 Rails 3.0 项目时应该使用 ruby​​ 1.9.2 吗? - 2

    应用将在Heroku上运行依赖包括回形针哈姆指南针设计aws-s3支持或反对的理由?对其他版本的ruby​​有什么建议吗?更新Heroku目前不支持1.9.2,但预计很快会基于thispost.Rails3.0正式支持1.9.2(但不支持1.9.1),所以我决定继续使用它。更新Heroku在其beta堆栈上支持1.9.2。 最佳答案 我会说是的。当您准备好推出您的应用程序时(2-3个月?),应该解决越来越多的兼容性问题。此外,如果您遇到任何问题,您可以提交补丁并为更快的1.9.2兼容性做出贡献!;)但是为了回答您的问题,考虑到您要使

随机推荐