绘制圆形,OpenGL样式

Drawing circle, OpenGL style

本文关键字:OpenGL 样式 绘制      更新时间:2023-10-16

我有一个13 x 13的像素阵列,我正在使用一个函数在上面画一个圆。(屏幕是13*13,这可能看起来很奇怪,但它是一个LED阵列,所以可以解释这一点。)

unsigned char matrix[13][13];
const unsigned char ON = 0x01;
const unsigned char OFF = 0x00;

这是我想到的第一个实现。(它效率低下,这是一个特殊的问题,因为这是一项嵌入式系统项目,80 MHz处理器。)

// Draw a circle
// mode is 'ON' or 'OFF'
inline void drawCircle(float rad, unsigned char mode)
{
    for(int ix = 0; ix < 13; ++ ix)
    {
        for(int jx = 0; jx < 13; ++ jx)
        {
            float r; // Radial
            float s; // Angular ("theta")
            matrix_to_polar(ix, jx, &r, &s); // Converts polar coordinates
                                             // specified by r and s, where
                                             // s is the angle, to index coordinates
                                             // specified by ix and jx.
                                             // This function just converts to
                                             // cartesian and then translates by 6.0.
            if(r < rad)
            {
                matrix[ix][jx] = mode; // Turn pixel in matrix 'ON' or 'OFF'
            }
        }
    }
}

我希望这是清楚的。这很简单,但后来我对它进行了编程,这样我就知道它应该如何工作了。如果你想要更多的信息/解释,那么我可以添加更多的代码/评论。

可以认为画几个圆(如4到6)是很慢的。。。因此,我正在寻求一种更有效的画圆算法的建议。

编辑:通过进行以下修改,成功地将性能提高了一倍:

调用绘图的函数过去是这样的:

for(;;)
{
    clearAll(); // Clear matrix
    for(int ix = 0; ix < 6; ++ ix)
    {
        rad[ix] += rad_incr_step;
        drawRing(rad[ix], rad[ix] - rad_width);
    }
    if(rad[5] >= 7.0)
    {
        for(int ix = 0; ix < 6; ++ ix)
        {
            rad[ix] = rad_space_step * (float)(-ix);
        }
    }
    writeAll(); // Write 
}

我添加了以下检查:

if(rad[ix] - rad_width < 7.0)
    drawRing(rad[ix], rad[ix] - rad_width);

这将性能提高了约2倍,但理想情况下,我希望提高圆绘制的效率,以进一步提高它。这将检查环是否完全在屏幕之外。

编辑2:类似地,添加反向检查进一步提高了性能。

if(rad[ix] >= 0.0)
    drawRing(rad[ix], rad[ix] - rad_width);

性能现在相当不错,但我再次没有修改圆的实际绘图代码,这就是我打算在这个问题上重点关注的。

编辑3:矩阵到极坐标:

inline void matrix_to_polar(int i, int j, float* r, float* s)
{
    float x, y;
    matrix_to_cartesian(i, j, &x, &y);
    calcPolar(x, y, r, s);
}
inline void matrix_to_cartesian(int i, int j, float* x, float* y)
{
    *x = getX(i);
    *y = getY(j);  
}
inline void calcPolar(float x, float y, float* r, float* s)
{
    *r = sqrt(x * x + y * y);
    *s = atan2(y, x);
}
inline float getX(int xc)
{
    return (float(xc) - 6.0);
}
inline float getY(int yc)
{
    return (float(yc) - 6.0); 
}

在对Clifford的响应中,如果没有内联的话,实际上会有很多函数调用。

编辑4:drawRing只画了两个圆,首先是一个模式为ON的外圆,然后是模式为OFF的内圆。我很有信心还有一种更有效的方法来画这样的形状,但这会分散问题的注意力。

您正在进行大量不需要的计算。例如,你正在计算极坐标的角度,但永远不要使用它。通过比较值的平方也可以很容易地避免平方根。

不做任何花哨的事情,像这样的事情应该是一个好的开始:

int intRad = (int)rad;
int intRadSqr = (int)(rad * rad);
for (int ix = 0; ix <= intRad; ++ix)
{
    for (int jx = 0; jx <= intRad; ++jx)
    {
        if (ix * ix + jx * jx <= radSqr)
        {
            matrix[6 - ix][6 - jx] = mode;
            matrix[6 - ix][6 + jx] = mode;
            matrix[6 + ix][6 - jx] = mode;
            matrix[6 + ix][6 + jx] = mode;
        }
    }
}

这以整数格式完成所有数学运算,并利用了圆对称性。

以上变化,基于评论中的反馈:

int intRad = (int)rad;
int intRadSqr = (int)(rad * rad);
for (int ix = 0; ix <= intRad; ++ix)
{
    for (int jx = 0; ix * ix + jx * jx <= radSqr; ++jx)
    {
        matrix[6 - ix][6 - jx] = mode;
        matrix[6 - ix][6 + jx] = mode;
        matrix[6 + ix][6 - jx] = mode;
        matrix[6 + ix][6 + jx] = mode;
    }
}

不要低估在没有FPU的处理器上使用浮点进行基本运算的成本。浮点似乎不太可能是必要的,但它的使用细节隐藏在matrix_to_polar()实现中。

您当前的实现将每个像素都视为候选像素,这也是不必要的。

使用方程y=cy±√[rad2-(x-cx)2],其中cx,cy是中心(在这种情况下是7,7),以及合适的整数平方根实现,可以这样画圆:

void drawCircle( int rad, unsigned char mode )
{
    int r2 = rad * rad ;
    for( int x = 7 - rad; x <= 7 + rad; x++ )
    {
        int dx = x - 7 ;
        int dy = isqrt( r2 - dx * dx ) ;
        matrix[x][7 - dy] = mode ;
        matrix[x][7 + dy] = mode ;
    }
}

在我的测试中,我根据这里的代码使用了下面的isqrt(),但假设r2所需的最大值为169(13<2>),如果需要,你可以实现16甚至8位的优化版本。如果你的处理器是32位,这可能没问题。

uint32_t isqrt(uint32_t n)
{
   uint32_t root = 0, bit, trial;
   bit = (n >= 0x10000) ? 1<<30 : 1<<14;
   do
   {
      trial = root+bit;
      if (n >= trial)
      {
         n -= trial;
         root = trial+bit;
      }
      root >>= 1;
      bit >>= 2;
   } while (bit);
   return root;
}

也就是说,在这样一个低分辨率的设备上,通过手动为所需的每个半径生成位图查找表,您可能会获得更好质量的圆圈和更快的性能。如果内存有问题,那么单个圆圈只需要7个字节来描述一个7 x 7象限,您可以将其反映到所有三个象限,或者为了获得更高的性能,您可以使用7 x 16位字来描述半圆(因为反转位顺序比反转阵列访问更贵,除非您使用带位的ARM Cortex-M)。使用半圆查找,13个圆将需要13 x 7 x 2字节(182字节),象限查找将是7 x 8 x 13(91字节)-您可能会发现这比计算圆所需的代码空间更少。

对于一个只有13x13元素显示器的慢速嵌入式设备,您实际上应该只制作一个查找表。例如:

struct ComputedCircle
{
    float rMax;
    char col[13][2];
};

其中绘制例程使用rMax来确定要使用哪个LUT元素。例如,如果有两个元素,其中一个rMax=1.4f,另一个=1.7f,那么1.4f和1.7f之间的任何半径都将使用该条目。

列元素将为每行指定零、一或两个线段,这些线段可以编码在每个字符的低4位和高4位中-1可以用作此行中的任何内容的哨兵值。使用多少个查找表条目取决于您,但使用13x13网格,您应该能够对条目远低于100个的像素的每一个可能结果进行编码,并且只使用10个左右的合理近似值。您还可以用压缩来换取绘制速度,例如,将col[13][2]矩阵放在平面列表中,并对定义的行数进行编码。

如果MooseBoy能更好地解释他提出的方法,我会接受他的回答。以下是我对查找表方法的看法。

使用查找表求解

13x13的显示器非常小,如果你只需要在这个像素数内完全可见的圆圈,你会得到一个非常小的桌子。即使你需要更大的圆圈,如果你需要它的速度(并且有ROM来存储它),它也应该比任何算法方法都要好。

如何操作

您基本上需要定义每个可能的圆在13x13显示器上的样子。仅仅为13x13显示器生成快照是不够的,因为您可能希望在任意位置绘制圆。我对表格条目的看法如下:

struct circle_entry_s{
    unsigned int diameter;
    unsigned int offset;
};

该条目将以像素为单位的给定直径映射到包含圆形状的大字节表中的偏移。例如,对于直径9,字节序列如下所示:

0x1CU, 0x00U, /* 000111000 */
0x63U, 0x00U, /* 011000110 */
0x41U, 0x00U, /* 010000010 */
0x80U, 0x80U, /* 100000001 */
0x80U, 0x80U, /* 100000001 */
0x80U, 0x80U, /* 100000001 */
0x41U, 0x00U, /* 010000010 */
0x63U, 0x00U, /* 011000110 */
0x1CU, 0x00U, /* 000111000 */

直径指定表中有多少字节属于圆:一行像素由(diameter + 7) >> 3字节生成,行数与直径相对应。这些的输出代码可以制作得很快,而查找表足够紧凑,如果需要,甚至可以比其中定义的13x13显示圆更大。

请注意,当由中心位置输出时,以这种方式为奇数和偶数直径定义圆可能会也可能不会吸引您。奇数直径的圆看起来中心在像素的"中间",而偶数直径的圆则看起来中心在一个像素的"角落"。

稍后,您可能还会发现改进整体方法,使其具有不同表观大小的多个圆,但具有相同的像素半径,这很好。这取决于你的目标:如果你想要某种流畅的动画,你最终可能会达到目的。

我认为算法解决方案在这里的表现大多很差,因为在这种有限的显示表面上,每个像素的状态都对外观很重要。