图形学笔记: 中点画圆法

多线程大师
• 阅读 5158

正圆

首先我们先研究一下圆本身的性质.圆是高度对称的, 在我们实际画图的时候, 我们只需要计算其中八分之一个圆, 然后把另外八分之七由对称性推出来. 我们选择斜率在0到-1之间的一段. 有了之前画线段的思路, 我们便可以很显然地想到方法: 利用判别式决定画哪个点. 我们希望判别式的作用是这样的:

f(x, y) > 0:     (x, y) is out of the circle
f(x, y) < 0:     (x, y) is inside the circle
f(x, y) = 0:     (x, y) is on the circle

   ,--- (x, y)
A  V
|  _ _
| |_|_| ,___    What if we test where (x+1, y-1/2) is?
|   |_| `
|
+-------->

测试两个候选点(x+1, y)和(x+1, y-1)的中点, 如果中点在圆内, 则说明圆弧经过上面的点更多, 反则经过下面的点更多. 现在我们希望得到这样的判别式. 大多数人都应该已经想到了, 这个判别式就是 f(x, y) = x^2 + y^2 - r^2. 这样, 我们就得到了第一条程序:

void circle_1(int x0, int y0, int r, const color& c, image& img)
{
        auto f = [&](float x, float y) {
                x -= x0; y -= y0;
                return x * x + y * y - r * r;
        };

        for(int x = x0, y = y0 + r; x - x0 <= y - y0; x++) {
                img(x, y) = c;
                img(y - y0 + x0, x - x0 + y0) = c;
                if(f(x + 1, y - 0.5) > 0) y--;
        }
}

正圆优化时间

上面的程序是正确的. 但是我们希望能用加减法來减少运算. 思路跟优化画线判别式是非常相似的, 就是找出f(x+1, y-1/2)与f(x, y)之间的递推关系. 现在, 我们静下心來做些数学推导:

 f(x + 1, y - 1/2) = x^2 + 2x + 1 + y^2 - y + 1/4 - r^2
                   = f(x, y) + 2x + 1 - y + 1/4

这样就可以由 上一个点 f(x, y)得出当前的判别式了. 但是, 当我希望继续递推下去的时候, 我发现我们并没有得到判别式所需的 当前的点 的判别式f(x+1, y)或f(x+1, y-1). 所以, 我们要把它求出来:

 f(x + 1, y)     = f(x + 1, y - 1/2) + y - 1/4  <------- Not integral
                 = f(x, y) + 2x + 1
 f(x + 1, y - 1) = f(x + 1, y - 1/2) - y - 1 - 1/4
                 = f(x, y) + 2x - 2y

有了这些式子, 于是我们的算法就出来了:

  1. f(0, r) = 0 是第一个值.
  2. f(1, r - 1/2), 是大于0还是小于0.
  3. 如果大于0, 那么画下面的点, 否则画上面的点.
  4. 如果画的是下面的点, 我们就应该将ff(1, r - 1/2)修改成f(x+1, y-1), 否则修改成f(x+1, y)
  5. 现在可以返回第二步, 來处理下一个点的事情了

等等! 在我们开始写代码之前, 还有一个问题要处理: 1/4 不是整数. 所幸, 这几乎影响不到什么. 1/4是一个不足1的数, 也就是说, 它丝毫影响不到f的正负性(如果f是-1, 加上1/4仍然小于0, 如果f是0, 加上1/4就大于0了). 到了后面, 1/4 恰好都是要被减去的. 所以, 直接当它不存在就好了:

void circle_2(int x0, int y0, int r, const color& c, image& img)
{
        for(int x = x0, y = y0 + r, f = 0; x - x0 <= y - y0; x++) {
                int dx = x - x0, dy = y - y0;
                img(x, y) = c;
                img(dy + x0, dx + y0) = c;

                f += dx + dx - dy + 1;
                if(f > 0) {
                        f += -dy - 1;
                        y--;
                } else f += dy;
        }
}

正圆渲染结果

int main()
{
        image img(800, 600);

        circle_1(300, 300, 250, color(uint32_t(0x00000000)), img);
        circle_2(300, 300, 220, color(uint32_t(0x00000000)), img);

        ofstream f("3-5-circle.ppm");
        f << img;
        f.close();
}

图形学笔记: 中点画圆法

椭圆

顺着这个思路, 我们可以想想椭圆该怎么画了. 圆可以八分对称, 但是椭圆只可以四分对称. 所以我们必须将4个象限的内容都算出来. 但是同时, 我们发现原先的中点测试不管用了, 因为它只能应对斜率在[0, -1]部分. 因此, 我们需要另一个迭代, 來应对斜率在[-1, -inf]的部分. 在此之前, 我们需要先知道椭圆的公式和判别式f(x, y)是怎样的.

       2     2
      x     y
 F = --- + --- - 1 = 0
       2     2
      a     b

 f(x, y) = b^2 x^2 + a^2 y^2 - a^2 b^2,

跟圆几乎没有差别, f(x, y)的性质也是一样的. 现在我们來讨论椭圆在一个象限里面的上半支和下半支. 比如说上半支, 我希望它从(0, y0)出发, 到某一点停止, 而下半支, 则从(x0. 0)出发, 到同一点停止. 这时, 求出这一点就变得相当关键. 考虑这一点是什么 -- 没错, 这一点就是斜率为-1的那点.

 A
 |(0, y0)          ,----- dy/dx = -1
 +--------____     |
 |            ``--.V
 |                 #
 |                  `\
 |                    \
 |                     |
 +---------------------+------->
                       (x0, 0)


   ,--- (x, y)
A  V                            A   ,---- test this while
|  _ _                          |  _V_    dy/dx < -1
| |_|_| ,_ test this while      | |_|_|
|   |_| `  dy/dx > -1           |   |_| <--- (x, y)
|                               |
+-------->                      +-------->

为了把这一点求出來, 我们需要做一点偏导:

  dy     ∂F/∂y       b^2 x
 ---- = ------- = - ------- <= -1
  dx     ∂F/∂x       a^2 y

  x <= a^2/b^2 y
  y <= b^2/a^2 x

如此以来, 我们便有了起始点的凭据, 可以书写一段代码了:

void eclipse_1(int x0, int y0, int a, int b, const color& c, image& img)
{
        const double bsq = b*b, asq = a*a, absq = bsq * asq;
        const double sepx = asq/bsq, sepy = bsq/asq;

        auto f = [&](float x, float y) {
                x -= x0; y -= y0;
                return bsq * x * x + asq * y * y - absq;
        };

        for(int x = x0, y = y0 + b; x - x0 <= sepx * (y - y0); x++) {
                img(x, y) = c;
                if(f(x + 1, y - 0.5) > 0) y--;
        }

        for(int y = y0, x = x0 + a; y - y0 <= sepy * (x - x0); y++) {
                img(x, y) = c;
                if(f(x - 0.5, y + 1) > 0) x--;
        }
}

椭圆优化时间

相似地, 我们优化时间需要有下列这些式子: 上一点到中点的关系式, 中点到当前点的关系式(这里有两条), 所以总共是6条:

f(x + 1, y - 1/2) = f(x, y) + 2 b^2 x + b^2 - a^2 y + a^2 /4     (i
f(x - 1/2, y + 1) = f(x, y) - b^2 x - b^2 /4 + 2 a^2 y + a^2     (ii

  f(x + 1, y - 1) = f(x, y) + 2 b^2 x + b^2 - 2 a^2 y + a^2
                  = f(x + 1, y - 1/2) - a^2 y - a^2 / 4 + a^2;   (iii
  f(x - 1, y + 1) = f(x, y) - 2 b^2 x + b^2 + 2 a^2 y + a^2
                  = f(x - 1/2, y + 1) - b^2 x - b^2 / 4 + b^2;   (iv

  f(x + 1, y) = f(x, y) + 2 b^2 x + b^2
              = f(x + 1, y - 1/2) + a^2 y - a^2 / 4;             (v
  f(x, y + 1) = f(x, y) + 2 a^2 y + a^2
              = f(x - 1/2, y + 1) + b^2 x + b^2 / 4;             (vi

同样地, a^2/4之类的部分, 因为小数部分对f(x+1, y-1/2)的正负性没有影响, 并且在后继的当前点的判别值的时候会被减去抵消, 所以并不必特意去校正它. 这样, 我们可以得到最终的程序是:

void eclipse_2(int x0, int y0, int a, int b, const color& c, image& img)
{
        long long int asq = a * a, bsq = b * b,
                 asq_4 = asq / 4, bsq_4 = bsq / 4,
                 asq_y, bsq_x, f;

        f = 0, asq_y = asq * b, bsq_x = 0;

        for(int x = 0, y = b; bsq_x <= asq_y; x++, bsq_x += bsq) {
                img(x + x0, y + y0) = c;
                f += bsq_x + bsq_x + bsq - asq_y + asq_4; // (i
                if(f < 0) f += asq_y - asq_4; // (v
                else {
                        f += - asq_y - asq_4 + asq; // (iii
                        y--;
                        asq_y -= asq;
                }
        }

        f = 0, asq_y = 0, bsq_x = bsq * a;

        for(int x = a, y = 0; bsq_x >= asq_y; y++, asq_y += asq) {
                img(x + x0, y + y0) = c;
                f += asq_y + asq_y + asq - bsq_x + bsq_4; // (ii
                if(f < 0) f += bsq_x - bsq_4; // (vi
                else {
                        f += - bsq_x - bsq_4 + bsq; // (iv
                        x--;
                        bsq_x -= bsq;
                }
        }
}

椭圆渲染结果

int main()
{
        image img(800, 600);
        eclipse_1(300, 300, 250, 200, color(uint32_t(0)), img);
        eclipse_2(300, 300, 270, 220, color(uint32_t(0)), img);
        ofstream file("3-6-eclipse.ppm");
        file << img;
        file.close();

        return 0;
}

图形学笔记: 中点画圆法

其实, 文章所描述的算法仍然没有彻底优化, 在很多细节方面还有很多可以修补的地方, 读者可以慢慢思考一下.

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
艾木酱 艾木酱
4年前
我们与MemFire Cloud的缘起1--MemFireDB
​如果你是一个大学生、程序员、创业者,当你脑子里不时冒出一些千奇百怪的想法,想要自己动手去实现它,却受限于资源,无法开展你的工作,不妨关注一下,我们正在尝试为您解决这些困扰。缘起/当前的服务模式限制了创造力的发挥传统云厂商的收费方式对于个人开发者不友好经常面临这样的困境:当我有了一个很好的idea时,作为程序员,能快速着手去实现它是我最大的优势。但是
代码还原的技术: Unidbg hook_add_new实现条件断点(二)
一、目标在做代码还原的时候,有时候会分析一组结果,希望在中途下个条件断点,比如在代码行0x1234,R00x5678的时候触发断点。今天我们就来试着搞一下。TIP:Unidbg代码同步到官方最新版,最新版已经支持浮点寄存器的显示了。二、步骤先写个floatdemotwo把祖传算法升个级extern"C"JNIEXPORTjstringJNIC
Easter79 Easter79
4年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Easter79 Easter79
4年前
TurnipBit开发板DIY呼吸的吃豆人教程实例
  转载请以链接形式注明文章来源(MicroPythonQQ技术交流群:157816561,公众号:MicroPython玩家汇)  0x00前言  吃豆人是耳熟能详的可爱形象,如今我们的TurnipBit也集成了这可爱的图形,我们这就让他来呼吸了~。  0x01效果展示  先一起看下最终的成品演示视频:  http:/
Stella981 Stella981
4年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
贾蔷 贾蔷
9个月前
力扣1137题 解题思路和步骤 C++代码实现,力扣一共多少题
一、题目分析力扣1137题要求我们找到第N个泰波那契数。泰波那契数的定义是:T00,T11,T21,且在n0的条件下Tn3TnTn1Tn2。,当n4时,T4T3T2T14。这道题主要考查我们对递归或动态规划的理解和运用。在思考解题方法时,我们