2

序幕

我终于解决了 AT32UC3xxxxx 和 SSD1306 OLED I2C 显示器之间的硬件不兼容问题(两者都有导致它们不兼容的硬件错误)允许我以 400KBaud(每帧约 26.6 毫秒)使用硬件 I2C。所以我决定为这个 LCD 重写我的旧驱动程序,以通过添加填充表面(三角形、四边形)而不只是线条和图案线条(我已经实现)来利用新的速度。

问题是显示器是 128x64 像素,但没有颜色或灰色阴影,只是 BW 开/关

因此,为了例如渲染旋转立方体,我需要以某种方式区分表面。我正在考虑随机填充模式,其中表面填充到某个百分比而不是颜色。

这是我当前的代码(整个库,但没有错误,它应该正常工作):

LCD_SSD1306_I2C.h

//------------------------------------------------------------------------------------------
//--- SSD1306 I2C OLED LCD driver ver 2.000 ------------------------------------------------
//------------------------------------------------------------------------------------------
#ifndef _LCD_SSD1306_I2C_h
#define _LCD_SSD1306_I2C_h
//------------------------------------------------------------------------------------------
#define SSD1306_SETCONTRAST 0x81
#define SSD1306_DISPLAYALLON_RESUME 0xA4
#define SSD1306_DISPLAYALLON 0xA5
#define SSD1306_NORMALDISPLAY 0xA6
#define SSD1306_INVERTDISPLAY 0xA7
#define SSD1306_DISPLAYOFF 0xAE
#define SSD1306_DISPLAYON 0xAF
#define SSD1306_SETDISPLAYOFFSET 0xD3
#define SSD1306_SETCOMPINS 0xDA
#define SSD1306_SETVCOMDETECT 0xDB
#define SSD1306_SETDISPLAYCLOCKDIV 0xD5
#define SSD1306_SETPRECHARGE 0xD9
#define SSD1306_SETMULTIPLEX 0xA8
#define SSD1306_SETLOWCOLUMN 0x00
#define SSD1306_SETHIGHCOLUMN 0x10
#define SSD1306_SETSTARTLINE 0x40
#define SSD1306_MEMORYMODE 0x20
#define SSD1306_COLUMNADDR 0x21
#define SSD1306_PAGEADDR   0x22
#define SSD1306_COMSCANINC 0xC0
#define SSD1306_COMSCANDEC 0xC8
#define SSD1306_SEGREMAP 0xA0
#define SSD1306_CHARGEPUMP 0x8D
#define SSD1306_SWITCHCAPVCC 0x2
// Scrolling #defines
#define SSD1306_ACTIVATE_SCROLL 0x2F
#define SSD1306_DEACTIVATE_SCROLL 0x2E
#define SSD1306_SET_VERTICAL_SCROLL_AREA 0xA3
#define SSD1306_RIGHT_HORIZONTAL_SCROLL 0x26
#define SSD1306_LEFT_HORIZONTAL_SCROLL 0x27
#define SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL 0x29
#define SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL 0x2A
//------------------------------------------------------------------------------------------
//#define I2C_send(adr,buf,siz){}
//------------------------------------------------------------------------------------------
#ifndef _brv8_tab
#define _brv8_tab
static const U8 brv8[256] =     // 8 bit bit reversal
    {
    0,128,64,192,32,160,96,224,16,144,80,208,48,176,112,240,8,136,72,200,40,168,104,232,24,152,
    88,216,56,184,120,248,4,132,68,196,36,164,100,228,20,148,84,212,52,180,116,244,12,140,76,204,
    44,172,108,236,28,156,92,220,60,188,124,252,2,130,66,194,34,162,98,226,18,146,82,210,50,178,
    114,242,10,138,74,202,42,170,106,234,26,154,90,218,58,186,122,250,6,134,70,198,38,166,102,230,
    22,150,86,214,54,182,118,246,14,142,78,206,46,174,110,238,30,158,94,222,62,190,126,254,1,129,
    65,193,33,161,97,225,17,145,81,209,49,177,113,241,9,137,73,201,41,169,105,233,25,153,89,217,57,
    185,121,249,5,133,69,197,37,165,101,229,21,149,85,213,53,181,117,245,13,141,77,205,45,173,109,
    237,29,157,93,221,61,189,125,253,3,131,67,195,35,163,99,227,19,147,83,211,51,179,115,243,11,139,
    75,203,43,171,107,235,27,155,91,219,59,187,123,251,7,135,71,199,39,167,103,231,23,151,87,215,55,
    183,119,247,15,143,79,207,47,175,111,239,31,159,95,223,63,191,127,255
    };
#endif
//------------------------------------------------------------------------------------------
class LCD_SSD1306_I2C           // max 96 lines
    {
public:
    // screen
    int adr;                    // I2C adr
    int xs,ys,sz;               // resoluiton
    U8 _scr[((128*96)>>3)+1];   // screen buffer
    U8 *scr;
    U8 *pyx[96];                // scan lines
    // pattern
    U32 pat,pat_m,pat_b;        // binary pattern,max used bit mask,actual bit mask
    // filling
    U32 seed;
    int bufl[96];
    int bufr[96];

    // system api
    void init(int _adr,int _xs,int _ys);                    // initialize LCD: I2C_adr,xs,ys
    void _command(U8 cmd);                                  // *internal* do not cal it (sends command to LCD over I2C)
    void rfsscr();                                          // copy actual screen buffer to LCD (by I2C)
    // gfx rendering col = <0,1>
    void clrscr();                                          // clear screen buffer
    void rotate(int ang);                                   // rotate 180 deg
    void pixel(int x,int y,bool col);                       // set/res pixel
    bool pixel(int x,int y);                                // get pixel
    void line(int x0,int y0,int x1,int y1,bool col);        // line
    void triangle(int x0,int y0,int x1,int y1,int x2,int y2,bool col);// triangle
    void quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,bool col);
    void rect(int x0,int y0,int x1,int y1,bool col);        // rectangle using diagonal points
    // patern rendering
    void pat_set(char *s);                                  // set binary pattern from bianry number string MSB renders first
    void pat_beg();                                         // set pattern state to start of pattern
    void pat_line(int x0,int y0,int x1,int y1,bool col);
    void pat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,bool col);
    void pat_quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,bool col);
    void pat_rect(int x0,int y0,int x1,int y1,bool col);
    // filled polygons col = <0,255>
    void _fill_line(int x0,int y0,int x1,int y1);           // *internal* do not call it (render line into bufl/bufr)
    void _fill_seed();                                      // *internal* do not call it (reset seed)
    U8   _fill_rand();                                      // *internal* do not call it (get pseudo random number)
    void fill_triangle(int x0,int y0,int x1,int y1,int x2,int y2,U8 col);
    void fill_quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,U8 col);
    // text rendering
    void prnchr(int x,int y,char c);                        // render char at x,y (y is rounded to multiple of 8)
    void prntxt(int x,int y,const char *txt);               // render text at x,y (y is rounded to multiple of 8)
    };
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::_command(U8 cmd)
    {
    U8 buf[2]=
        {
        0x00,       // 0x40 data/command
        cmd,
        };
    I2C_send(adr,buf,2);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::init(int _adr,int _xs,int _ys)
    {
    int y;
    adr=_adr;
    xs=_xs;
    ys=_ys;
    sz=xs*(ys>>3);
    const bool _external_Vcc=false;
    // VRAM buffer
    scr=_scr+1;                                         // skip first Byte (VRAM/command selection)
    for (y=0;y<ys;y++) pyx[y]=scr+((y>>3)*xs);          // scanlines for fast direct pixel access
    clrscr();
    // Init sequence
    U8 comPins = 0x02;
    U8 contrast = 0x8F;
         if((xs == 128) && (ys == 32)) { comPins = 0x02; contrast = 0x8F; }
    else if((xs == 128) && (ys == 64)) { comPins = 0x12; contrast = (_external_Vcc) ? 0x9F : 0xCF; }
    else if((xs ==  96) && (ys == 16)) { comPins = 0x02; contrast = (_external_Vcc) ? 0x10 : 0xAF; }
    else {} // Other screens
    static U8 init0[27]=
        {
        0x00,                                           // commands
        SSD1306_DISPLAYOFF,                             // 0xAE
        SSD1306_SETDISPLAYCLOCKDIV,0x80,                // 0xD5
        SSD1306_SETMULTIPLEX,ys-1,                      // 0xA8
        SSD1306_SETDISPLAYOFFSET,0x00,                  // 0xD3 no offset
        SSD1306_SETSTARTLINE | 0x0,                     // line 0
        SSD1306_CHARGEPUMP,(_external_Vcc)?0x10:0x14,   // 0x8D
        SSD1306_MEMORYMODE,0x00,                        // 0x20 horizontal (scanlines)
        SSD1306_SEGREMAP | 0x1,
        SSD1306_COMSCANDEC,
        SSD1306_SETCOMPINS,comPins,
        SSD1306_SETCONTRAST,contrast,
        SSD1306_SETPRECHARGE,(_external_Vcc)?0x22:0xF1, // 0xd9
        SSD1306_SETVCOMDETECT,0x40,                     // 0xDB
        SSD1306_DISPLAYALLON_RESUME,                    // 0xA4
        SSD1306_NORMALDISPLAY,                          // 0xA6
        SSD1306_DEACTIVATE_SCROLL,
        SSD1306_DISPLAYON,                              // Main screen turn on
        };
    I2C_send(adr,init0,sizeof(init0));
    // init default pattern
    pat_set("111100100");
    // clear filling buffers
    for (y=0;y<96;y++)
        {
        bufl[y]=-1;
        bufr[y]=-1;
        }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::clrscr()
    {
    for (int a=0;a<sz;a++) scr[a]=0x00;
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::rotate(int ang)
    {
    U8 a0,a1;
    int x0,y0,x1,y1;
    if (ang==180)
     for (y0=0,y1=ys-8;y0<y1;y0+=8,y1-=8)
      for (x0=0,x1=xs-1;x0<xs;x0++,x1--)
          {
          a0=brv8[pyx[y0][x0]];
          a1=brv8[pyx[y1][x1]];
          pyx[y0][x0]=a1;
          pyx[y1][x1]=a0;
          }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::rfsscr()
    {
    static const U8 init1[] =
        {
        0x00,                           // commands
        SSD1306_MEMORYMODE,0,           // horizontal addresing mode
        SSD1306_COLUMNADDR,0,xs-1,      // Column start/end address (0/127 reset)
        SSD1306_PAGEADDR,0,(ys>>3)-1,   // Page start/end address (0 reset)
        };
    I2C_send(adr,(U8*)init1,sizeof(init1));

    _scr[0]=0x40;                       // 0x40 VRAM
    // SW I2C can pass whole VRAM in single packet
//  I2C_send(adr,_scr,sz+1);

    // HW I2C must use packets up to 255 bytes so 128+1 (as TWIM0 on UC3 has 8 bit counter)
    int i,n=128; U8 *p=_scr,a;
    for (i=0;i<sz;i+=n){ a=p[0]; p[0]=0x40; I2C_send(adr,p,n+1); p[0]=a; p+=n; }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pixel(int x, int y,bool col)
    {
    // clip to screen
    if ((x<0)||(x>=xs)||(y<0)||(y>=ys)) return;
    // add or remove bit
    if (col) pyx[y][x] |= (1<<(y&7));
    else     pyx[y][x] &= (255)^(1<<(y&7));
    }
//------------------------------------------------------------------------------------------
bool LCD_SSD1306_I2C::pixel(int x, int y)
    {
    // clip to screen
    if ((x<0)||(x>=xs)||(y<0)||(y>=ys)) return false;
    // get bit
    return ((pyx[y][x]>>(y&7))&1);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::line(int x0,int y0,int x1,int y1,bool col)
    {
    int i,n,cx,cy,sx,sy;
    // line DDA parameters
    x1-=x0; sx=0; if (x1>0) sx=+1; if (x1<0) { sx=-1; x1=-x1; } if (x1) x1++;           n=x1;
    y1-=y0; sy=0; if (y1>0) sy=+1; if (y1<0) { sy=-1; y1=-y1; } if (y1) y1++; if (n<y1) n=y1;
    // single pixel (not a line)
    if (!n){ pixel(x0,y0,col); return; }
    // ND DDA algo i is parameter
    for (cx=cy=n,i=0;i<n;i++)
        {
        pixel(x0,y0,col);
        cx-=x1; if (cx<=0){ cx+=n; x0+=sx; }
        cy-=y1; if (cy<=0){ cy+=n; y0+=sy; }
        }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_line(int x0,int y0,int x1,int y1,bool col)
    {
    bool ccc;
    int i,n,cx,cy,sx,sy;
    // line DDA parameters
    x1-=x0; sx=0; if (x1>0) sx=+1; if (x1<0) { sx=-1; x1=-x1; } if (x1) x1++;           n=x1;
    y1-=y0; sy=0; if (y1>0) sy=+1; if (y1<0) { sy=-1; y1=-y1; } if (y1) y1++; if (n<y1) n=y1;
    // single pixel (not a line)
    if (!n){ pixel(x0,y0,col); return; }
    // ND DDA algo i is parameter
    for (cx=cy=n,i=0;i<n;i++)
        {
        ccc=(pat&pat_b); ccc^=(!col);
        pat_b>>=1; if (!pat_b) pat_b=pat_m;
        pixel(x0,y0,ccc);
        cx-=x1; if (cx<=0){ cx+=n; x0+=sx; }
        cy-=y1; if (cy<=0){ cy+=n; y0+=sy; }
        }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::_fill_line(int x0,int y0,int x1,int y1)
    {
    int i,n,cx,cy,sx,sy,*buf;
    // line DDA parameters
    x1-=x0; sx=0; if (x1>0) sx=+1; if (x1<0) { sx=-1; x1=-x1; } if (x1) x1++;           n=x1;
    y1-=y0; sy=0; if (y1>0) sy=+1; if (y1<0) { sy=-1; y1=-y1; } if (y1) y1++; if (n<y1) n=y1;
    // single pixel (not a line)
    if (!n)
        {
        if ((y0>=0)&&(y0<ys))
            {
            bufl[y0]=x0;
            bufr[y0]=x0;
            }
        return;
        }
    // target buffer depend on y direction
    if (sy>0) buf=bufl; else buf=bufr;
    // ND DDA algo i is parameter
    for (cx=cy=n,i=0;i<n;i++)
        {
        if ((y0>=0)&&(y0<ys)) buf[y0]=x0;
        cx-=x1; if (cx<=0){ cx+=n; x0+=sx; }
        cy-=y1; if (cy<=0){ cy+=n; y0+=sy; }
        }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::_fill_seed()
    {
    seed=0x017A357E1;
//  RandSeed=0x017A357E1;
    }
//------------------------------------------------------------------------------------------
U8 LCD_SSD1306_I2C::_fill_rand()
    {
    U32 a,b,c;
    a= seed     &0x0FFFF;
    b=(seed>>16)&0x0FFFF;
    seed<<=11;
    seed^=(a<<16);
    seed&=0x0FFFF0000;
    seed|=b+17;
    return seed&255;
//  return Random(256);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::triangle(int x0,int y0,int x1,int y1,int x2,int y2,bool col)
    {
    line(x0,y0,x1,y1,col);
    line(x1,y1,x2,y2,col);
    line(x2,y2,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,bool col)
    {
    pat_line(x0,y0,x1,y1,col);
    pat_line(x1,y1,x2,y2,col);
    pat_line(x2,y2,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::fill_triangle(int x0,int y0,int x1,int y1,int x2,int y2,U8 col)
    {
    int x,y,X0,X1,Y0,Y1;
    // y range to render
    Y0=Y1=y0;
    if (Y0>y1) Y0=y1;
    if (Y1<y1) Y1=y1;
    if (Y0>y2) Y0=y2;
    if (Y1<y2) Y1=y2;
    // clip to screen in y axis
    if ((Y1<0)||(Y0>=ys)) return;
    if (Y0<  0) Y0=   0;
    if (Y1>=ys) Y1=ys-1;
    // clear buffers
    for (y=Y0;y<=Y1;y++)
        {
        bufl[y]=xs;
        bufr[y]=-1;
        }
    // render circumference
    _fill_line(x0,y0,x1,y1);
    _fill_line(x1,y1,x2,y2);
    _fill_line(x2,y2,x0,y0);
    // fill horizontal lines
    _fill_seed();
    for (y=Y0;y<=Y1;y++)
        {
        // x range to render
        X0=bufl[y];
        X1=bufr[y];
        if (X0>X1){ x=X0; X0=X1; X1=x; }
        // clip to screen in y axis
        if ((X1<0)||(X0>=xs)) continue;
        if (X0<  0) X0=   0;
        if (X1>=xs) X1=xs-1;
             if (col==  0) for (x=X0;x<=X1;x++) pixel(x,y,0);
        else if (col==255) for (x=X0;x<=X1;x++) pixel(x,y,1);
        else for (x=X0;x<=X1;x++) pixel(x,y,(_fill_rand()<=col));
        }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,bool col)
    {
    line(x0,y0,x1,y1,col);
    line(x1,y1,x2,y2,col);
    line(x2,y2,x3,y3,col);
    line(x3,y3,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,bool col)
    {
    pat_line(x0,y0,x1,y1,col);
    pat_line(x1,y1,x2,y2,col);
    pat_line(x2,y2,x3,y3,col);
    pat_line(x3,y3,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::fill_quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,U8 col)
    {
    int x,y,X0,X1,Y0,Y1;
    // y range to render
    Y0=Y1=y0;
    if (Y0>y1) Y0=y1;
    if (Y1<y1) Y1=y1;
    if (Y0>y2) Y0=y2;
    if (Y1<y2) Y1=y2;
    if (Y0>y3) Y0=y3;
    if (Y1<y3) Y1=y3;
    // clip to screen in y axis
    if ((Y1<0)||(Y0>=ys)) return;
    if (Y0<  0) Y0=   0;
    if (Y1>=ys) Y1=ys-1;
    // clear buffers
    for (y=Y0;y<=Y1;y++)
        {
        bufl[y]=xs;
        bufr[y]=-1;
        }
    // render circumference
    _fill_line(x0,y0,x1,y1);
    _fill_line(x1,y1,x2,y2);
    _fill_line(x2,y2,x3,y3);
    _fill_line(x3,y3,x0,y0);
    // fill horizontal lines
    _fill_seed();
    for (y=Y0;y<=Y1;y++)
        {
        // x range to render
        X0=bufl[y];
        X1=bufr[y];
        if (X0>X1){ x=X0; X0=X1; X1=x; }
        // clip to screen in y axis
        if ((X1<0)||(X0>=xs)) continue;
        if (X0<  0) X0=   0;
        if (X1>=xs) X1=xs-1;
             if (col==  0) for (x=X0;x<=X1;x++) pixel(x,y,0);
        else if (col==255) for (x=X0;x<=X1;x++) pixel(x,y,1);
        else for (x=X0;x<=X1;x++) pixel(x,y,(_fill_rand()<=col));
        }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::rect(int x0,int y0,int x1,int y1,bool col)
    {
    line(x0,y0,x1,y0,col);
    line(x1,y0,x1,y1,col);
    line(x1,y1,x0,y1,col);
    line(x0,y1,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_rect(int x0,int y0,int x1,int y1,bool col)
    {
    pat_line(x0,y0,x1,y0,col);
    pat_line(x1,y0,x1,y1,col);
    pat_line(x1,y1,x0,y1,col);
    pat_line(x0,y1,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::prnchr(int x,int y,char c)
    {
    y&=0xFFFFFFF8;  // multiple of 8
    if ((y<0)||(y>ys-8)) return;
    int i,a;
    a=c; a<<=3;
    for (i=0;i<8;i++,x++,a++)
     if ((x>=0)&&(x<xs))
      pyx[y][x]=font[a];
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::prntxt(int x,int y,const char *txt)
    {
    for (;*txt;txt++,x+=8) prnchr(x,y,*txt);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_set(char *s)
    {
    int i=1;
    pat=0;
    if (s!=NULL)
     for (i=0;(*s)&&(i<32);s++,i++)
        {
        pat<<=1;
        if (*s=='1') pat|=1;
        }
    if (!i) i=1;
    pat_m=1<<(i-1);
    pat_beg();
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_beg()
    {
    pat_b=pat_m;
    }
//------------------------------------------------------------------------------------------
#undef I2C_send
//------------------------------------------------------------------------------------------
#endif
//------------------------------------------------------------------------------------------

它是为 AVR32 studio 2.7 编写的,因此在没有 U8/U16/U32 的平台上使用 unsigned int 而不是相同(或更大)的位宽。

代码没有经过优化,并且是故意以它的方式编写的(不是为了速度,我也在我的讲座中使用它,这样学生就可以掌握我在做什么)

现在,当我使用这种技术使用 2D 填充四边形渲染旋转立方体(在 PC 上的 win32 VCL 测试应用程序上)时:

//---------------------------------------------------------------------------
#include <vcl.h>
#include <math.h>
#pragma hdrstop
#include "win_main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMain *Main;
const int sz=4;         // LCD pixel size
int xs,ys;              // LCD resolution * sz
int *psz;               // screen pixel to LCD pixel
Graphics::TBitmap *bmp; // screen buffer
//---------------------------------------------------------------------------
typedef BYTE U8; // here use data types you got like unsigned __int8_t ...
typedef WORD U16;
typedef DWORD U32;
void I2C_send(int adr,U8 *buf,int siz){}
//#include "font_8x8.h"  // font file
static U8 font[256<<3]; // empty font instead (no printing used)
#include "LCD_SSD1306_I2C.h"
LCD_SSD1306_I2C lcd;
//---------------------------------------------------------------------------
const float cube_pos[]=
    {
//  x    y    z     //ix
    -1.0,+1.0,-1.0, //0
    +1.0,+1.0,-1.0, //1
    +1.0,-1.0,-1.0, //2
    -1.0,-1.0,-1.0, //3

    -1.0,-1.0,+1.0, //4
    +1.0,-1.0,+1.0, //5
    +1.0,+1.0,+1.0, //6
    -1.0,+1.0,+1.0, //7

    -1.0,-1.0,-1.0, //3
    +1.0,-1.0,-1.0, //2
    +1.0,-1.0,+1.0, //5
    -1.0,-1.0,+1.0, //4

    +1.0,-1.0,-1.0, //2
    +1.0,+1.0,-1.0, //1
    +1.0,+1.0,+1.0, //6
    +1.0,-1.0,+1.0, //5

    +1.0,+1.0,-1.0, //1
    -1.0,+1.0,-1.0, //0
    -1.0,+1.0,+1.0, //7
    +1.0,+1.0,+1.0, //6

    -1.0,+1.0,-1.0, //0
    -1.0,-1.0,-1.0, //3
    -1.0,-1.0,+1.0, //4
    -1.0,+1.0,+1.0, //7
    };

const float cube_nor[]=
    {
//   nx   ny   nz   //ix
     0.0, 0.0,-1.0, //0
     0.0, 0.0,-1.0, //1
     0.0, 0.0,-1.0, //2
     0.0, 0.0,-1.0, //3

     0.0, 0.0,+1.0, //4
     0.0, 0.0,+1.0, //5
     0.0, 0.0,+1.0, //6
     0.0, 0.0,+1.0, //7

     0.0,-1.0, 0.0, //0
     0.0,-1.0, 0.0, //1
     0.0,-1.0, 0.0, //5
     0.0,-1.0, 0.0, //4

    +1.0, 0.0, 0.0, //1
    +1.0, 0.0, 0.0, //2
    +1.0, 0.0, 0.0, //6
    +1.0, 0.0, 0.0, //5

     0.0,+1.0, 0.0, //2
     0.0,+1.0, 0.0, //3
     0.0,+1.0, 0.0, //7
     0.0,+1.0, 0.0, //6

    -1.0, 0.0, 0.0, //3
    -1.0, 0.0, 0.0, //0
    -1.0, 0.0, 0.0, //4
    -1.0, 0.0, 0.0, //7
    };
//---------------------------------------------------------------------------
void  matrix_mul_pos(float *c,const float *a,const float *b)
    {
    float q[3];
    q[0]=(a[ 0]*b[0])+(a[ 4]*b[1])+(a[ 8]*b[2])+(a[12]);
    q[1]=(a[ 1]*b[0])+(a[ 5]*b[1])+(a[ 9]*b[2])+(a[13]);
    q[2]=(a[ 2]*b[0])+(a[ 6]*b[1])+(a[10]*b[2])+(a[14]);
    for(int i=0;i<3;i++) c[i]=q[i];
    }
void  matrix_mul_dir(float *c,const float *a,const float *b)
    {
    float q[3];
    q[0]=(a[ 0]*b[0])+(a[ 4]*b[1])+(a[ 8]*b[2]);
    q[1]=(a[ 1]*b[0])+(a[ 5]*b[1])+(a[ 9]*b[2]);
    q[2]=(a[ 2]*b[0])+(a[ 6]*b[1])+(a[10]*b[2]);
    for(int i=0;i<3;i++) c[i]=q[i];
    }
void  matrix_mul_mat(float *c,float *a,float *b)
    {
    float q[16];
    q[ 0]=(a[ 0]*b[ 0])+(a[ 1]*b[ 4])+(a[ 2]*b[ 8])+(a[ 3]*b[12]);
    q[ 1]=(a[ 0]*b[ 1])+(a[ 1]*b[ 5])+(a[ 2]*b[ 9])+(a[ 3]*b[13]);
    q[ 2]=(a[ 0]*b[ 2])+(a[ 1]*b[ 6])+(a[ 2]*b[10])+(a[ 3]*b[14]);
    q[ 3]=(a[ 0]*b[ 3])+(a[ 1]*b[ 7])+(a[ 2]*b[11])+(a[ 3]*b[15]);
    q[ 4]=(a[ 4]*b[ 0])+(a[ 5]*b[ 4])+(a[ 6]*b[ 8])+(a[ 7]*b[12]);
    q[ 5]=(a[ 4]*b[ 1])+(a[ 5]*b[ 5])+(a[ 6]*b[ 9])+(a[ 7]*b[13]);
    q[ 6]=(a[ 4]*b[ 2])+(a[ 5]*b[ 6])+(a[ 6]*b[10])+(a[ 7]*b[14]);
    q[ 7]=(a[ 4]*b[ 3])+(a[ 5]*b[ 7])+(a[ 6]*b[11])+(a[ 7]*b[15]);
    q[ 8]=(a[ 8]*b[ 0])+(a[ 9]*b[ 4])+(a[10]*b[ 8])+(a[11]*b[12]);
    q[ 9]=(a[ 8]*b[ 1])+(a[ 9]*b[ 5])+(a[10]*b[ 9])+(a[11]*b[13]);
    q[10]=(a[ 8]*b[ 2])+(a[ 9]*b[ 6])+(a[10]*b[10])+(a[11]*b[14]);
    q[11]=(a[ 8]*b[ 3])+(a[ 9]*b[ 7])+(a[10]*b[11])+(a[11]*b[15]);
    q[12]=(a[12]*b[ 0])+(a[13]*b[ 4])+(a[14]*b[ 8])+(a[15]*b[12]);
    q[13]=(a[12]*b[ 1])+(a[13]*b[ 5])+(a[14]*b[ 9])+(a[15]*b[13]);
    q[14]=(a[12]*b[ 2])+(a[13]*b[ 6])+(a[14]*b[10])+(a[15]*b[14]);
    q[15]=(a[12]*b[ 3])+(a[13]*b[ 7])+(a[14]*b[11])+(a[15]*b[15]);
    for(int i=0;i<16;i++) c[i]=q[i];
    }
//---------------------------------------------------------------------------
float deg=M_PI/180.0,angx=0.0,angy=0.0,angz=5.0*deg;
float view_FOVx=128.0*tan(30.0*deg)*0.5;
void obj2scr(int &x,int &y,const float *m,const float *pos)
    {
    float p[3],d;
    x=0; y=0;
    matrix_mul_pos(p,m,pos);
    if (fabs(p[2])>1e-3) d=view_FOVx/p[2]; else d=0.0;
    p[0]*=d; x=2.5*p[0]; x+=64;
    p[1]*=d; y=2.5*p[1]; y+=32;
    }
//---------------------------------------------------------------------------
void TMain::draw()
    {
    int i,j,n,nz;
    int x,y,x0,y0,x1,y1,x2,y2,x3,y3;
    lcd.clrscr();

    // modelview
    float p[3],c,s,m[16],m0[16]=
        {
         1.0, 0.0, 0.0,0.0,
         0.0, 1.0, 0.0,0.0,
         0.0, 0.0, 1.0,0.0,
         0.0, 0.0, 7.0,1.0,
        };
    c=cos(angx); s=sin(angx);
    float rx[16]= { 1, 0, 0, 0,
                     0, c, s, 0,
                     0,-s, c, 0,
                     0, 0, 0, 1 };
    c=cos(angy); s=sin(angy);
    float ry[16]= { c, 0, s, 0,
                    0, 1, 0, 0,
                   -s, 0, c, 0,
                    0, 0, 0, 1 };
    c=cos(angz); s=sin(angz);
    float rz[16]= { c, s, 0, 0,
                   -s, c, 0, 0,
                    0, 0, 1, 0,
                    0, 0, 0, 1 };
    matrix_mul_mat(m,rx,ry);
    matrix_mul_mat(m,m,rz);
    matrix_mul_mat(m,m,m0);
    angx=fmod(angx+1.0*deg,2.0*M_PI);
    angy=fmod(angy+5.0*deg,2.0*M_PI);
    angz=fmod(angz+2.0*deg,2.0*M_PI);

    n=6*4*3;
    for (i=0;i<n;)
        {
        matrix_mul_dir(p,m,cube_nor+i);
        nz=float(-255.0*p[2]*fabs(p[2]));
        obj2scr(x0,y0,m,cube_pos+i); i+=3;
        obj2scr(x1,y1,m,cube_pos+i); i+=3;
        obj2scr(x2,y2,m,cube_pos+i); i+=3;
        obj2scr(x3,y3,m,cube_pos+i); i+=3;
        if (nz>0)
            {
            nz=100+((150*nz)>>8);
            lcd.fill_quad(x0,y0,x1,y1,x2,y2,x3,y3,nz);
            }
        }
    lcd.rfsscr();

    // copy LCD to Canvas to see result
    if (1)
        {
        DWORD col[2]={0x00100018,0x00FFFFFF},*p;
        int x,y,xx,yy;
        for (                           y=0,yy=psz[y];y<ys;y++,yy=psz[y])
         for (p=(DWORD*)bmp->ScanLine[y],x=0,xx=psz[x];x<xs;x++,xx=psz[x])
          p[x]=col[lcd.pixel(xx,yy)];
        }
    Canvas->Draw(0,0,bmp);
    }
//---------------------------------------------------------------------------
__fastcall TMain::TMain(TComponent* Owner) : TForm(Owner)
    {
    lcd.init(0x3C,128,64);
    bmp=new Graphics::TBitmap;
    bmp->HandleType=bmDIB;
    bmp->PixelFormat=pf32bit;
    xs=lcd.xs*sz;
    ys=lcd.ys*sz;
    bmp->SetSize(xs,ys);
    ClientWidth=xs;
    ClientHeight=ys;

    int i,n;
    n=xs; if (n<ys) n=ys;
    psz=new int[n+1];
    for (i=0;i<n;i++) psz[i]=i/sz; psz[n]=0;
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormDestroy(TObject *Sender)
    {
    delete[] psz;
    delete bmp;
    }
//---------------------------------------------------------------------------
void __fastcall TMain::tim_redrawTimer(TObject *Sender)
    {
    draw();
    }
//---------------------------------------------------------------------------

只需忽略 VCL 的东西。该应用程序具有定期重绘屏幕的单个计时器。lcd需要先初始化。lcd 使用I2C_send功能通过 I2C 进行通信,因此您必须实现它才能与真正的 LCD 一起使用,如果您只是将屏幕复制到图像或视图(仿真)中,那么像我一样的空功能就足够了。打印文本也是如此,它需要这种字体(它不适合这里),所以我使用了空字体(因为该示例无论如何都不会打印任何东西)。

我得到了这个输出(使用随机着色填充和基本法线着色):

预习

正如你所看到的,由于分辨率低,立方体几乎无法区分,我希望有更好的东西。

所以最后我的问题:

如何在视觉上改善这种阴影

我正在考虑某种类似于 Freescape 引擎输出的孵化或预定义模式,如下所示:

预定义模式

您对如何进行这种 2D 凸多边形填充有任何想法或指示吗?

限制是低分辨率 128x64 1bpp 图像,内存使用率低,因为目标 AVR32 UC3 平台只有 (16+32+32) KB 的 RAM,如果有人想使用 AVR8 芯片,那么只有 2 KB(你知道 Arduino 使用那些) .

速度不是主要问题,因为目标平台有 ~91 MIPS。

我对 BW 着色不是很熟练(在过去我主要使用线框进行 BW 输出),所以即使是来自有经验的用户的提示,例如:

  • 有多少种色调16/32/256
  • 多大的图案4x4/8x8/16x16
  • 如何生成模式(硬编码或一些算法)
  • 如何处理多边形运动以避免噪音/闪烁(现在我在每个多边形上重置种子)

可能对我有很大帮助。

这是用于测试的示例输入图像:

样本输入

4

2 回答 2

3

我设法让这个工作。我最终得到了 17 个 8x8 像素大小的硬编码阴影图案(在 Paint 中手动创建)。这些是阴影:

17 种色调

从那里我只是使用x,y渲染像素mod 8作为阴影 LUT 中的坐标。这是结果:

阴影立方体

如您所见,它比基于 PRNG 的着色要好得多。这里更新的代码:

//------------------------------------------------------------------------------------------
//--- SSD1306 I2C OLED LCD driver ver 2.001 ------------------------------------------------
//------------------------------------------------------------------------------------------
/*
 [Notes]
 + I2C transfere size is not limitted on LCD side
 - No memory address reset command present so any corruption while VRAM transfere permanently damages output untill power on/off but somehow after some timeout or overflow the adress is reseted
 - UC3 HW I2C limits up to 255 bytes per packet
 - UC3 glitch on SDA before ACK which confuse LCD to read instead of write operation and do not ACK
 */
#ifndef _LCD_SSD1306_I2C_h
#define _LCD_SSD1306_I2C_h
//------------------------------------------------------------------------------------------
#define SSD1306_SETCONTRAST 0x81
#define SSD1306_DISPLAYALLON_RESUME 0xA4
#define SSD1306_DISPLAYALLON 0xA5
#define SSD1306_NORMALDISPLAY 0xA6
#define SSD1306_INVERTDISPLAY 0xA7
#define SSD1306_DISPLAYOFF 0xAE
#define SSD1306_DISPLAYON 0xAF
#define SSD1306_SETDISPLAYOFFSET 0xD3
#define SSD1306_SETCOMPINS 0xDA
#define SSD1306_SETVCOMDETECT 0xDB
#define SSD1306_SETDISPLAYCLOCKDIV 0xD5
#define SSD1306_SETPRECHARGE 0xD9
#define SSD1306_SETMULTIPLEX 0xA8
#define SSD1306_SETLOWCOLUMN 0x00
#define SSD1306_SETHIGHCOLUMN 0x10
#define SSD1306_SETSTARTLINE 0x40
#define SSD1306_MEMORYMODE 0x20
#define SSD1306_COLUMNADDR 0x21
#define SSD1306_PAGEADDR   0x22
#define SSD1306_COMSCANINC 0xC0
#define SSD1306_COMSCANDEC 0xC8
#define SSD1306_SEGREMAP 0xA0
#define SSD1306_CHARGEPUMP 0x8D
#define SSD1306_SWITCHCAPVCC 0x2
// Scrolling #defines
#define SSD1306_ACTIVATE_SCROLL 0x2F
#define SSD1306_DEACTIVATE_SCROLL 0x2E
#define SSD1306_SET_VERTICAL_SCROLL_AREA 0xA3
#define SSD1306_RIGHT_HORIZONTAL_SCROLL 0x26
#define SSD1306_LEFT_HORIZONTAL_SCROLL 0x27
#define SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL 0x29
#define SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL 0x2A
//------------------------------------------------------------------------------------------
//#define I2C_send(adr,buf,siz){}
//------------------------------------------------------------------------------------------
#ifndef _brv8_tab
#define _brv8_tab
static const U8 brv8[256] =     // 8 bit bit reversal
    {
    0,128,64,192,32,160,96,224,16,144,80,208,48,176,112,240,8,136,72,200,40,168,104,232,24,152,
    88,216,56,184,120,248,4,132,68,196,36,164,100,228,20,148,84,212,52,180,116,244,12,140,76,204,
    44,172,108,236,28,156,92,220,60,188,124,252,2,130,66,194,34,162,98,226,18,146,82,210,50,178,
    114,242,10,138,74,202,42,170,106,234,26,154,90,218,58,186,122,250,6,134,70,198,38,166,102,230,
    22,150,86,214,54,182,118,246,14,142,78,206,46,174,110,238,30,158,94,222,62,190,126,254,1,129,
    65,193,33,161,97,225,17,145,81,209,49,177,113,241,9,137,73,201,41,169,105,233,25,153,89,217,57,
    185,121,249,5,133,69,197,37,165,101,229,21,149,85,213,53,181,117,245,13,141,77,205,45,173,109,
    237,29,157,93,221,61,189,125,253,3,131,67,195,35,163,99,227,19,147,83,211,51,179,115,243,11,139,
    75,203,43,171,107,235,27,155,91,219,59,187,123,251,7,135,71,199,39,167,103,231,23,151,87,215,55,
    183,119,247,15,143,79,207,47,175,111,239,31,159,95,223,63,191,127,255
    };
#endif
static const U8 shade8x8[17*8]= // 17 shade patterns 8x8 pixels
    {
      0,  0,  0,  0,  0,  0,  0,  0,
     17,  0,  0,  0, 17,  0,  0,  0,
     17,  0, 68,  0, 17,  0, 68,  0,
     68, 17, 68,  0, 68, 17, 68,  0,
     85,  0,170,  0, 85,  0,170,  0,
     68,170,  0,170, 68,170,  0,170,
     68,170, 17,170, 68,170, 17,170,
     85,136, 85,170, 85,136, 85,170,
     85,170, 85,170, 85,170, 85,170,
     85,238, 85,170,119,170, 85,170,
    221,170,119,170,221,170,119,170,
    221,170,255,170,221,170,255,170,
     85,255,170,255, 85,255,170,255,
    221,119,221,255,221,119,221,255,
    119,255,221,255,119,255,221,255,
    119,255,255,255,119,255,255,255,
    255,255,255,255,255,255,255,255
    };
//------------------------------------------------------------------------------------------
class LCD_SSD1306_I2C           // max 96 lines
    {
public:
    // screen
    int adr;                    // I2C adr
    int xs,ys,sz;               // resoluiton
    U8 _scr[((128*96)>>3)+1];   // screen buffer
    U8 *scr;
    U8 *pyx[96];                // scan lines
    // pattern
    U32 pat,pat_m,pat_b;        // binary pattern,max used bit mask,actual bit mask
    // filling
    int bufl[96];
    int bufr[96];

    // system api
    void init(int _adr,int _xs,int _ys);                    // initialize LCD: I2C_adr,xs,ys
    void _command(U8 cmd);                                  // *internal* do not cal it (sends command to LCD over I2C)
    void rfsscr();                                          // copy actual screen buffer to LCD (by I2C)
    // gfx rendering col = <0,1>
    void clrscr();                                          // clear screen buffer
    void rotate(int ang);                                   // rotate 180 deg
    void pixel(int x,int y,bool col);                       // set/res pixel
    bool pixel(int x,int y);                                // get pixel
    void line(int x0,int y0,int x1,int y1,bool col);        // line
    void triangle(int x0,int y0,int x1,int y1,int x2,int y2,bool col);// triangle
    void quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,bool col);
    void rect(int x0,int y0,int x1,int y1,bool col);        // rectangle using diagonal points
    // patern rendering
    void pat_set(char *s);                                  // set binary pattern from bianry number string MSB renders first
    void pat_beg();                                         // set pattern state to start of pattern
    void pat_line(int x0,int y0,int x1,int y1,bool col);
    void pat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,bool col);
    void pat_quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,bool col);
    void pat_rect(int x0,int y0,int x1,int y1,bool col);
    // filled polygons col = <0,255>
    void _fill_line(int x0,int y0,int x1,int y1);           // *internal* do not call it (render line into bufl/bufr)
    void _fill(int Y0,int Y1,U8 col);                       // *internal* do not call it (render bufl/bufr onto screen)
    void fill_triangle(int x0,int y0,int x1,int y1,int x2,int y2,U8 col);
    void fill_quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,U8 col);
    // text rendering
    void prnchr(int x,int y,char c);                        // render char at x,y (y is rounded to multiple of 8)
    void prntxt(int x,int y,const char *txt);               // render text at x,y (y is rounded to multiple of 8)
    };
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::_command(U8 cmd)
    {
    U8 buf[2]=
        {
        0x00,       // 0x40 data/command
        cmd,
        };
    I2C_send(adr,buf,2);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::init(int _adr,int _xs,int _ys)
    {
    int y;
    adr=_adr;
    xs=_xs;
    ys=_ys;
    sz=xs*(ys>>3);
    const bool _external_Vcc=false;
    // VRAM buffer
    scr=_scr+1;                                         // skip first Byte (VRAM/command selection)
    for (y=0;y<ys;y++) pyx[y]=scr+((y>>3)*xs);          // scanlines for fast direct pixel access
    clrscr();
    // Init sequence
    U8 comPins = 0x02;
    U8 contrast = 0x8F;
         if((xs == 128) && (ys == 32)) { comPins = 0x02; contrast = 0x8F; }
    else if((xs == 128) && (ys == 64)) { comPins = 0x12; contrast = (_external_Vcc) ? 0x9F : 0xCF; }
    else if((xs ==  96) && (ys == 16)) { comPins = 0x02; contrast = (_external_Vcc) ? 0x10 : 0xAF; }
    else {} // Other screens
    static U8 init0[27]=
        {
        0x00,                                           // commands
        SSD1306_DISPLAYOFF,                             // 0xAE
        SSD1306_SETDISPLAYCLOCKDIV,0x80,                // 0xD5
        SSD1306_SETMULTIPLEX,ys-1,                      // 0xA8
        SSD1306_SETDISPLAYOFFSET,0x00,                  // 0xD3 no offset
        SSD1306_SETSTARTLINE | 0x0,                     // line 0
        SSD1306_CHARGEPUMP,(_external_Vcc)?0x10:0x14,   // 0x8D
        SSD1306_MEMORYMODE,0x00,                        // 0x20 horizontal (scanlines)
        SSD1306_SEGREMAP | 0x1,
        SSD1306_COMSCANDEC,
        SSD1306_SETCOMPINS,comPins,
        SSD1306_SETCONTRAST,contrast,
        SSD1306_SETPRECHARGE,(_external_Vcc)?0x22:0xF1, // 0xd9
        SSD1306_SETVCOMDETECT,0x40,                     // 0xDB
        SSD1306_DISPLAYALLON_RESUME,                    // 0xA4
        SSD1306_NORMALDISPLAY,                          // 0xA6
        SSD1306_DEACTIVATE_SCROLL,
        SSD1306_DISPLAYON,                              // Main screen turn on
        };
    I2C_send(adr,init0,sizeof(init0));
    // init default pattern
    pat_set("111100100");
    // clear filling buffers
    for (y=0;y<96;y++)
        {
        bufl[y]=-1;
        bufr[y]=-1;
        }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::clrscr()
    {
    for (int a=0;a<sz;a++) scr[a]=0x00;
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::rotate(int ang)
    {
    U8 a0,a1;
    int x0,y0,x1,y1;
    if (ang==180)
     for (y0=0,y1=ys-8;y0<y1;y0+=8,y1-=8)
      for (x0=0,x1=xs-1;x0<xs;x0++,x1--)
          {
          a0=brv8[pyx[y0][x0]];
          a1=brv8[pyx[y1][x1]];
          pyx[y0][x0]=a1;
          pyx[y1][x1]=a0;
          }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::rfsscr()
    {
    static const U8 init1[] =
        {
        0x00,                           // commands
        SSD1306_MEMORYMODE,0,           // horizontal addresing mode
        SSD1306_COLUMNADDR,0,xs-1,      // Column start/end address (0/127 reset)
        SSD1306_PAGEADDR,0,(ys>>3)-1,   // Page start/end address (0 reset)
        };
    I2C_send(adr,(U8*)init1,sizeof(init1));

    _scr[0]=0x40;                       // 0x40 VRAM
    // SW I2C can pass whole VRAM in single packet
//  I2C_send(adr,_scr,sz+1);

    // HW I2C must use packets up to 255 bytes so 128+1 (as TWIM0 on UC3 has 8 bit counter)
    int i,n=128; U8 *p=_scr,a;
    for (i=0;i<sz;i+=n){ a=p[0]; p[0]=0x40; I2C_send(adr,p,n+1); p[0]=a; p+=n; }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pixel(int x, int y,bool col)
    {
    // clip to screen
    if ((x<0)||(x>=xs)||(y<0)||(y>=ys)) return;
    // add or remove bit
    if (col) pyx[y][x] |= (1<<(y&7));
    else     pyx[y][x] &= (255)^(1<<(y&7));
    }
//------------------------------------------------------------------------------------------
bool LCD_SSD1306_I2C::pixel(int x, int y)
    {
    // clip to screen
    if ((x<0)||(x>=xs)||(y<0)||(y>=ys)) return false;
    // get bit
    return ((pyx[y][x]>>(y&7))&1);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::line(int x0,int y0,int x1,int y1,bool col)
    {
    int i,n,cx,cy,sx,sy;
    // line DDA parameters
    x1-=x0; sx=0; if (x1>0) sx=+1; if (x1<0) { sx=-1; x1=-x1; } if (x1) x1++;           n=x1;
    y1-=y0; sy=0; if (y1>0) sy=+1; if (y1<0) { sy=-1; y1=-y1; } if (y1) y1++; if (n<y1) n=y1;
    // single pixel (not a line)
    if (!n){ pixel(x0,y0,col); return; }
    // ND DDA algo i is parameter
    for (cx=cy=n,i=0;i<n;i++)
        {
        pixel(x0,y0,col);
        cx-=x1; if (cx<=0){ cx+=n; x0+=sx; }
        cy-=y1; if (cy<=0){ cy+=n; y0+=sy; }
        }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_line(int x0,int y0,int x1,int y1,bool col)
    {
    bool ccc;
    int i,n,cx,cy,sx,sy;
    // line DDA parameters
    x1-=x0; sx=0; if (x1>0) sx=+1; if (x1<0) { sx=-1; x1=-x1; } if (x1) x1++;           n=x1;
    y1-=y0; sy=0; if (y1>0) sy=+1; if (y1<0) { sy=-1; y1=-y1; } if (y1) y1++; if (n<y1) n=y1;
    // single pixel (not a line)
    if (!n){ pixel(x0,y0,col); return; }
    // ND DDA algo i is parameter
    for (cx=cy=n,i=0;i<n;i++)
        {
        ccc=(pat&pat_b); ccc^=(!col);
        pat_b>>=1; if (!pat_b) pat_b=pat_m;
        pixel(x0,y0,ccc);
        cx-=x1; if (cx<=0){ cx+=n; x0+=sx; }
        cy-=y1; if (cy<=0){ cy+=n; y0+=sy; }
        }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::_fill_line(int x0,int y0,int x1,int y1)
    {
    int i,n,cx,cy,sx,sy,*buf;
    // line DDA parameters
    x1-=x0; sx=0; if (x1>0) sx=+1; if (x1<0) { sx=-1; x1=-x1; } if (x1) x1++;           n=x1;
    y1-=y0; sy=0; if (y1>0) sy=+1; if (y1<0) { sy=-1; y1=-y1; } if (y1) y1++; if (n<y1) n=y1;
    // single pixel (not a line)
    if (!n)
        {
        if ((y0>=0)&&(y0<ys))
            {
            bufl[y0]=x0;
            bufr[y0]=x0;
            }
        return;
        }
    // target buffer depend on y direction
    if (sy>0) buf=bufl; else buf=bufr;
    // ND DDA algo i is parameter
    for (cx=cy=n,i=0;i<n;i++)
        {
        if ((y0>=0)&&(y0<ys)) buf[y0]=x0;
        cx-=x1; if (cx<=0){ cx+=n; x0+=sx; }
        cy-=y1; if (cy<=0){ cy+=n; y0+=sy; }
        }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::_fill(int Y0,int Y1,U8 col)
    {
    U8 shd;
    int x,y,X0,X1,i;
    // select shade pattern
    i=col;
    if (i< 0) i=0;
    if (i>17) i=17;
    i<<=3;
    // fill horizontal lines
    for (y=Y0;y<=Y1;y++)
        {
        shd=shade8x8[i+(y&7)];
        // x range to render
        X0=bufl[y];
        X1=bufr[y];
        if (X0>X1){ x=X0; X0=X1; X1=x; }
        // clip to screen in y axis
        if ((X1<0)||(X0>=xs)) continue;
        if (X0<  0) X0=   0;
        if (X1>=xs) X1=xs-1;
             if (col==  0) for (x=X0;x<=X1;x++) pixel(x,y,0);
        else if (col==255) for (x=X0;x<=X1;x++) pixel(x,y,1);
        else for (x=X0;x<=X1;x++) pixel(x,y,shd&(1<<(x&7)));
        }
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::triangle(int x0,int y0,int x1,int y1,int x2,int y2,bool col)
    {
    line(x0,y0,x1,y1,col);
    line(x1,y1,x2,y2,col);
    line(x2,y2,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,bool col)
    {
    pat_line(x0,y0,x1,y1,col);
    pat_line(x1,y1,x2,y2,col);
    pat_line(x2,y2,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::fill_triangle(int x0,int y0,int x1,int y1,int x2,int y2,U8 col)
    {
    int y,Y0,Y1;
    // y range to render
    Y0=Y1=y0;
    if (Y0>y1) Y0=y1;
    if (Y1<y1) Y1=y1;
    if (Y0>y2) Y0=y2;
    if (Y1<y2) Y1=y2;
    // clip to screen in y axis
    if ((Y1<0)||(Y0>=ys)) return;
    if (Y0<  0) Y0=   0;
    if (Y1>=ys) Y1=ys-1;
    // clear buffers
    for (y=Y0;y<=Y1;y++)
        {
        bufl[y]=xs;
        bufr[y]=-1;
        }
    // render circumference
    _fill_line(x0,y0,x1,y1);
    _fill_line(x1,y1,x2,y2);
    _fill_line(x2,y2,x0,y0);
    // fill horizontal lines
    _fill(Y0,Y1,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,bool col)
    {
    line(x0,y0,x1,y1,col);
    line(x1,y1,x2,y2,col);
    line(x2,y2,x3,y3,col);
    line(x3,y3,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,bool col)
    {
    pat_line(x0,y0,x1,y1,col);
    pat_line(x1,y1,x2,y2,col);
    pat_line(x2,y2,x3,y3,col);
    pat_line(x3,y3,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::fill_quad(int x0,int y0,int x1,int y1,int x2,int y2,int x3,int y3,U8 col)
    {
    int y,Y0,Y1;
    // y range to render
    Y0=Y1=y0;
    if (Y0>y1) Y0=y1;
    if (Y1<y1) Y1=y1;
    if (Y0>y2) Y0=y2;
    if (Y1<y2) Y1=y2;
    if (Y0>y3) Y0=y3;
    if (Y1<y3) Y1=y3;
    // clip to screen in y axis
    if ((Y1<0)||(Y0>=ys)) return;
    if (Y0<  0) Y0=   0;
    if (Y1>=ys) Y1=ys-1;
    // clear buffers
    for (y=Y0;y<=Y1;y++)
        {
        bufl[y]=xs;
        bufr[y]=-1;
        }
    // render circumference
    _fill_line(x0,y0,x1,y1);
    _fill_line(x1,y1,x2,y2);
    _fill_line(x2,y2,x3,y3);
    _fill_line(x3,y3,x0,y0);
    // fill horizontal lines
    _fill(Y0,Y1,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::rect(int x0,int y0,int x1,int y1,bool col)
    {
    line(x0,y0,x1,y0,col);
    line(x1,y0,x1,y1,col);
    line(x1,y1,x0,y1,col);
    line(x0,y1,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_rect(int x0,int y0,int x1,int y1,bool col)
    {
    pat_line(x0,y0,x1,y0,col);
    pat_line(x1,y0,x1,y1,col);
    pat_line(x1,y1,x0,y1,col);
    pat_line(x0,y1,x0,y0,col);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::prnchr(int x,int y,char c)
    {
    y&=0xFFFFFFF8;  // multiple of 8
    if ((y<0)||(y>ys-8)) return;
    int i,a;
    a=c; a<<=3;
    for (i=0;i<8;i++,x++,a++)
     if ((x>=0)&&(x<xs))
      pyx[y][x]=font[a];
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::prntxt(int x,int y,const char *txt)
    {
    for (;*txt;txt++,x+=8) prnchr(x,y,*txt);
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_set(char *s)
    {
    int i=1;
    pat=0;
    if (s!=NULL)
     for (i=0;(*s)&&(i<32);s++,i++)
        {
        pat<<=1;
        if (*s=='1') pat|=1;
        }
    if (!i) i=1;
    pat_m=1<<(i-1);
    pat_beg();
    }
//------------------------------------------------------------------------------------------
void LCD_SSD1306_I2C::pat_beg()
    {
    pat_b=pat_m;
    }
//------------------------------------------------------------------------------------------
#undef I2C_send
//------------------------------------------------------------------------------------------
#endif
//------------------------------------------------------------------------------------------

我把所有的填充都移到了成员函数_fill中,看看 LUTshade8x8是如何使用的......

[Edit1] 弗洛伊德-斯坦伯格抖动

感谢 Scheff,我想尝试这种抖动。遗憾的是,他的代码不能用于多边形光栅化(由于它的特殊性和缺少输入图像),所以我自己实现了。我挣扎了一段时间以使其正常工作,而无法按预期工作的唯一方法是:

  • 颜色阈值是最大强度的 50%
  • 错误传播是在有符号整数上完成的

第二个要求带来了巨大的性能损失,因为不能再使用简单的位移,但是我认为硬编码的 LUT 将克服这一点。

这是我当前使用展位抖动的实现:

void LCD_SSD1306_I2C::_fill(int Y0,int Y1,U8 col)
    {
    int x,y,X0,X1,i;
    const U8 colmax=17;
    // bayer like dithering using precomputed shader patterns
    if (dither_mode==0)
        {
        U8 shd;
        // select shade pattern
        i=col;
        if (i< 0) i=0;
        if (i>17) i=17;
        i<<=3;
        // fill horizontal lines
        for (y=Y0;y<=Y1;y++)
            {
            shd=shade8x8[i+(y&7)];
            // x range to render
            X0=bufl[y];
            X1=bufr[y];
            if (X0>X1){ x=X0; X0=X1; X1=x; }
            // clip to screen in y axis
            if ((X1<0)||(X0>=xs)) continue;
            if (X0<  0) X0=   0;
            if (X1>=xs) X1=xs-1;
                 if (col==     0) for (x=X0;x<=X1;x++) pixel(x,y,0);
            else if (col==colmax) for (x=X0;x<=X1;x++) pixel(x,y,1);
            else for (x=X0;x<=X1;x++) pixel(x,y,shd&(1<<(x&7)));
            }
        }
    // Floyd–Steinberg dithering
    if (dither_mode==1)
        {
        int c0,c1,c2,rows[256+4],*r0=rows+1,*r1=rows+128+3,*rr,thr=colmax/2;
        // clear error;
        c0=0; for (x=0;x<256+4;x++) rows[x]=0;
        // fill horizontal lines
        for (y=Y0;y<=Y1;y++)
            {
            // x range to render
            X0=bufl[y];
            X1=bufr[y];
            if (X0>X1){ x=X0; X0=X1; X1=x; }
            // clip to screen in y axis
            if ((X1<0)||(X0>=xs)) continue;
            if (X0<  0) X0=   0;
            if (X1>=xs) X1=xs-1;
                 if (col==     0) for (x=X0;x<=X1;x++) pixel(x,y,0);
            else if (col==colmax) for (x=X0;x<=X1;x++) pixel(x,y,1);
            else for (x=X0;x<=X1;x++)
                {
                c0=col; c0+=r0[x]; ; r0[x]=0;
                if (c0>thr){ pixel(x,y,1); c0-=colmax; }
                 else        pixel(x,y,0);
                c2=c0;
                c1=(3*c0)/16; r1[x-1] =c1; c2-=c1;
                c1=(5*c0)/16; r1[x  ] =c1; c2-=c1;
                c1=(  c0)/16; r1[x+1] =c1; c2-=c1;
                              r0[x+1]+=c2;
                }
            rr=r0; r0=r1; r1=rr;
            }
        }
    }

dither_mode只是决定使用哪种抖动。在这里并排预览。左边是 Bayer ditheringlike(我画的图案),右边是 Floyd-Steinberg 抖动:

并排

Floyd-Steinberg 抖动有时会在某些旋转中创建丑陋的(平行线)图案,而恒定图案在所有情况下看起来都很平滑所以我会坚持使用我的原始渲染。

于 2021-03-06T15:55:00.067 回答
2

抖动

…一直是我一直想更详细地探讨的主题之一。虽然 OP 总是提供一个自我回答,但在它出现之前我已经开始了自己的摆弄。因此,我想介绍一下我得到的。

我实施了

  • 拜耳抖动和
  • 弗洛伊德-斯坦伯格抖动

并做了一个

  • 演示(在 Qt 中)
  • 视觉输出和一些
  • 时间比较

这还不够严肃,不能称之为基准,但可能会提供一个提示。

拜耳抖动

链接的 Wikipedia 文章提到了可以直接在源代码中定义的 2×2、4×4 和 8×8 拜耳矩阵的阈值图。

2×2 拜耳矩阵

4×4 拜耳矩阵

8×8 拜耳矩阵

相反,我使用了拜耳矩阵的递归定义

拜耳矩阵的递归定义

因此,应用相同的函数来初始化静态拜耳矩阵以进行查找以及在需要时即时计算相关元素。

我的实现ditherBayer.h

// Bayer Dithering (Ordered Dithering)
// https://en.wikipedia.org/wiki/Ordered_dithering

#ifndef DITHER_BAYER_H
#define DITHER_BAYER_H

// standard C++ header:
#include <cassert>
#include <algorithm>

// own header:
#include "types.h"

// a function for recursive definition of Bayer matrix
template <uint Q>
uint8 mBayer(uint i, uint j);

// specialization for Q == 1
template <>
uint8 mBayer<1>(uint i, uint j)
{
  assert(i < 2); assert(j < 2);
  static uint8 m[2][2] {
    { 0, 2 },
    { 3, 1 }
  };
  return m[i][j];
}

// general case: recursive definition
template <uint Q>
uint8 mBayer(uint i, uint j)
{
  const uint q_1 = Q - 1; // for recursive descent
  const uint mask = ~(1 << q_1);
  return 4 * mBayer<q_1>(i & mask, j & mask)
    + mBayer<1>(i >> q_1, j >> q_1);
}

/* Bayer Dithering
 *
 * Q   : quality 1, 2, 3, 4
 *       The Bayer matrix has dimension 2^Q x 2^Q.
 * MAP : true ... stored matrix for look-up\n
 *       false ... compute matrix values on-the-fly in each call
 *
 * w, h: width and height of images
 * imgMono: buffer for mono image (output)
 *          1 bit per pixel
 * bytesPerRowMono: bytes per row in the mono image
 *          to consider row alignment in case
 * imgGray: buffer for graylevel image (input)
 *          8 bits per pixel
 * bytesPerRowGray: bytes per row in the graylevel image
 *          to consider row alignment in case
 */
template <uint Q, bool MAP>
void ditherBayer(
  uint w, uint h,
  uint8 *imgMono, uint bytesPerRowMono,
  const uint8 *imgGray, uint bytesPerRowGray)
{
  static_assert(Q >= 1); static_assert(Q <= 4);
  const uint n = 1 << Q; // width/height of Bayer matrix (a power of 2)
  const uint mask = n - 1; // a bit mask, used for % n in bit-arith.
  // get pixel (x, y) from gray image
  auto getPixel = [&](uint x, uint y) {
    assert(y < h); assert(x < bytesPerRowGray);
    return imgGray[y * bytesPerRowGray + x];
  };
  // set pixel (x, y) in mono image
  auto setPixel = [&](uint x, uint y, bool value) {
    assert(y < h);
    const uint xByte = x >> 3, xBit = 7 - (x & 7);
    assert(xByte < bytesPerRowMono);
    imgMono[y * bytesPerRowMono + xByte] |= value << xBit;
  };
  // init mono image (clear all bits initially)
  std::fill(imgMono, imgMono + w * h / 8, 0);
  // apply dithering
  if (MAP) { // use look-up matrix
    static uint8 m[n][n];
    if (static bool init = false; !init) {
      for (uint i = 0; i < n; ++i) {
        for (uint j = 0; j < n; ++j) {
          m[i][j] = mBayer<Q>(i, j) << (8 - 2 * Q);
        }
      }
      init = true;
    }
    for (uint y = 0; y < h; ++y) {
      for (uint x = 0; x < w; ++x) {
        setPixel(x, y, getPixel(x, y) > m[y & mask][x & mask]);
      }
    }
  } else { // compute matrix values on-the-fly
    for (uint y = 0; y < h; ++y) {
      for (uint x = 0; x < w; ++x) {
        setPixel(x, y,
          getPixel(x, y) > mBayer<Q>(y & mask, x & mask) << (8 - 2 * Q));
      }
    }
  }
}

#endif // DITHER_BAYER_H

因此,我将拜耳矩阵的元素缩放到输入像素范围 [0, 255]。因此,输入像素可以直接与当前拜耳矩阵元素进行比较,以产生0(黑色)或1(白色)输出像素值。

弗洛伊德-斯坦伯格抖动

链接的 Wikipedia 文章强调了 Floyd-Steinberg 抖动可用于就地抖动(即输入图像就地修改)这一事实。在我的情况下,这不是目的,因为我打算保持输入图像不变。(在 OPs 的情况下,像素值是 3d 渲染的结果,也没有可用的输入图像。)当我从输入图像的副本开始时,我意识到一次总是只需要两个连续的行而算法正在抖动。因此,我改变了我的实现,只管理两个行缓冲区,每行都重复使用它们。

此外,这些行缓冲区可以使用带符号且大于输入像素类型的像素类型,以实现更精确的错误处理。

最后,行缓冲区比行大一点,这消除了在没有任何特殊考虑的情况下处理边界情况(行的第一个和最后一个像素)的需要。

我的实现ditherFloydSteinberg.h

// Floyd-Steinberg Dithering
// https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering

#ifndef DITHER_FLOYD_STEINBERG_H
#define DITHER_FLOYD_STEINBERG_H

// standard C++ header:
#include <cassert>
#include <algorithm>
#include <vector>

/* Floyd-Steinberg Dithering
 *
 * w, h: width and height of images
 * imgMono: buffer for mono image (output)
 *          1 bit per pixel
 * bytesPerRowMono: bytes per row in the mono image
 *          to consider row alignment in case
 * imgGray: buffer for graylevel image (input)
 *          8 bits per pixel
 * bytesPerRowGray: bytes per row in the graylevel image
 *          to consider row alignment in case
 */
void ditherFloydSteinberg(
  uint w, uint h,
  uint8 *imgMono, uint bytesPerRowMono,
  const uint8 *imgGray, uint bytesPerRowGray)
{
  // get pixel (x, y) from gray image
  auto getPixel = [&](uint x, uint y) {
    assert(y < h); assert(x < bytesPerRowGray);
    return imgGray[y * bytesPerRowGray + x];
  };
  // set pixel (x, y) in mono image
  auto setPixel = [&](uint x, uint y, bool value) {
    assert(y < h);
    const uint xByte = x >> 3, xBit = 7 - (x & 7);
    assert(xByte < bytesPerRowMono);
    imgMono[y * bytesPerRowMono + xByte] |= value << xBit;
  };
  // two row buffers
  // for cumulative error compensation
  // and to simplify handling of border cases (hence: w + 2)
  std::vector<int> buffer0(w + 2), buffer1(w + 2);
  int *buffers[2] = { &buffer0[1], &buffer1[1] };
  // copy pixel row y into buffer
  auto initBuffer = [&](int *buffer, uint y)
  {
    buffer[-1] = getPixel(0, y);
    for (uint i = 0; i < w; ++i) buffer[i] = getPixel(i, y);
  };
  // init mono image (clear all bits initially)
  std::fill(imgMono, imgMono + w * h / 8, 0);
  // process pixels
  initBuffer(buffers[0], 0);
  for (uint y = 0; y < h; ++y) {
    if (y + 1 < h) initBuffer(buffers[1], y); // init buffer for next row
    for (int x = 0; x < (int)w; ++x) {
      const int valueOld = buffers[0][x];
      const bool valueNew = valueOld >= 128;
      setPixel(x, y, valueNew);
      const int error = valueOld - valueNew * 0xff;
      buffers[0][x + 1] += error * 7 / 16;
      buffers[1][x - 1] += error * 3 / 16;
      buffers[1][x    ] += error * 5 / 16;
      buffers[1][x + 1] += error * 1 / 16;
    }
    std::swap(buffers[0], buffers[1]); // next row -> current row
  }
}

/* Floyd-Steinberg Dithering in serpentines
 *
 * Every second row is processed from right to left.
 *
 * w, h: width and height of images
 * imgMono: buffer for mono image (output)
 *          1 bit per pixel
 * bytesPerRowMono: bytes per row in the mono image
 *          to consider row alignment in case
 * imgGray: buffer for graylevel image (input)
 *          8 bits per pixel
 * bytesPerRowGray: bytes per row in the graylevel image
 *          to consider row alignment in case
 */
void ditherFloydSteinbergSerp(
  uint w, uint h,
  uint8 *imgMono, uint bytesPerRowMono,
  const uint8 *imgGray, uint bytesPerRowGray)
{
  // get pixel (x, y) from gray image
  auto getPixel = [&](uint x, uint y) {
    assert(y < h); assert(x < bytesPerRowGray);
    return imgGray[y * bytesPerRowGray + x];
  };
  // set pixel (x, y) in mono image
  auto setPixel = [&](uint x, uint y, bool value) {
    assert(y < h);
    const uint xByte = x >> 3, xBit = 7 - (x & 7);
    assert(xByte < bytesPerRowMono);
    imgMono[y * bytesPerRowMono + xByte] |= value << xBit;
  };
  // two row buffers
  // for cumulative error compensation
  // and to simplify handling of border cases (hence: w + 2)
  std::vector<int> buffer0(w + 2), buffer1(w + 2);
  int *buffers[2] = { &buffer0[1], &buffer1[1] };
  // copy pixel row y into buffer
  auto initBuffer = [&](int *buffer, uint y)
  {
    buffer[-1] = getPixel(0, y);
    for (uint i = 0; i < w; ++i) buffer[i] = getPixel(i, y);
  };
  // init mono image (clear all bits initially)
  std::fill(imgMono, imgMono + w * h / 8, 0);
  // process pixels
  initBuffer(buffers[0], 0);
  for (uint y = 0; y < h; ++y) {
    if (y + 1 < h) initBuffer(buffers[1], y); // init buffer for next row
    for (int x = 0; x < (int)w; ++x) {
      const int valueOld = buffers[0][x];
      const bool valueNew = valueOld >= 128;
      setPixel(x, y, valueNew);
      const int error = valueOld - valueNew * 0xff;
      buffers[0][x + 1] += error * 7 / 16;
      buffers[1][x - 1] += error * 3 / 16;
      buffers[1][x    ] += error * 5 / 16;
      buffers[1][x + 1] += error * 1 / 16;
    }
    std::swap(buffers[0], buffers[1]); // next row -> current row
    // next iteration (unrolled)
    if (++y == h) break;
    if (y + 1 < h) initBuffer(buffers[1], y);
    for (int x = w; x--;) {
      const int valueOld = buffers[0][x];
      const bool valueNew = valueOld >= 192;
      setPixel(x, y, valueNew);
      const int error = valueOld - valueNew * 0xff;
      buffers[0][x - 1] += error * 7 / 16;
      buffers[1][x + 1] += error * 3 / 16;
      buffers[1][x    ] += error * 5 / 16;
      buffers[1][x - 1] += error * 1 / 16;
    }
    std::swap(buffers[0], buffers[1]); // next row -> current row
  }
}

#endif // DITHER_FLOYD_STEINBERG_H

演示(在 Qt 中)

对于一个说明性的演示,我编写了一个小型 Qt 程序。Qt 提供开箱即用的基本图像处理(包括 PNG 和 JPEG 文件加载器)。因此,为此提供MCVE所需的代码相当少。

types.h

#ifndef TYPES_H
#define TYPES_H

#include <cstdint>

// convenience types

using uint = unsigned;
using ulong = unsigned long;
using uint8 = std::uint8_t;
using uint16 = std::uint16_t;

#endif // TYPES_H

testQImageDithering.cc

// Qt header:
#include <QtWidgets>

// own header:
#include "types.h"
#include "ditherBayer.h"
#include "ditherFloydSteinberg.h"

/* ensures that the output image has proper size and format
 *
 * qImgMono output image (mono image)
 * qImgGray input image (graylevel image)
 */
void adjustMonoImage(
  QImage &qImgMono, const QImage &qImgGray)
{
  if (qImgMono.format() != QImage::Format_Mono
    || qImgMono.width() != qImgGray.width()
    || qImgMono.height() != qImgGray.height()) {
    qImgMono
      = QImage(qImgGray.width(), qImgGray.height(), QImage::Format_Mono);
  }
}

// convenience types for std::chrono

using Clock = std::chrono::steady_clock;
using Time = Clock::time_point;
using USecs = std::chrono::microseconds;

/* provides a wrapper function to apply the dither algorithms
 * to QImage objects.
 *
 * return: duration in microseconds
 */
template <void (*FUNC)(uint, uint, uint8*, uint, const uint8*, uint)>
ulong dither(
  QImage &qImgMono, const QImage &qImgGray)
{
  adjustMonoImage(qImgMono, qImgGray);
  const Time t0 = Clock::now();
  FUNC((uint)qImgGray.width(), (uint)qImgGray.height(),
    qImgMono.bits(), qImgMono.bytesPerLine(),
    qImgGray.bits(), qImgGray.bytesPerLine());
  const Time t1 = Clock::now();
  return std::chrono::duration_cast<USecs>(t1 - t0).count();
}

// main application
int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // setup data
  QImage qImg("test.jpg");
  QImage qImgG = qImg.convertToFormat(QImage::Format_Grayscale8);
  QImage qImgDith(qImgG.width(), qImgG.height(), QImage::Format_Mono);
  // dithering algorithms
  using DitherFunc = ulong(QImage&, const QImage&);
  using DitherSlot = std::function<DitherFunc>;
  using FuncEntry = std::pair<QString, DitherSlot>;
  FuncEntry tblDither[] = {
    { "Bayer (2 x 2, table)", &dither<ditherBayer<1, true>> },
    { "Bayer (4 x 4, table)", &dither<ditherBayer<2, true>> },
    { "Bayer (8 x 8, table)", &dither<ditherBayer<3, true>> },
    { "Bayer (2 x 2, func.)", &dither<ditherBayer<1, false>> },
    { "Bayer (4 x 4, func.)", &dither<ditherBayer<2, false>> },
    { "Bayer (8 x 8, func.)", &dither<ditherBayer<3, false>> },
    { "Floyd Steinberg", &dither<ditherFloydSteinberg> },
    { "Floyd Steinberg (serp.)", &dither<ditherFloydSteinbergSerp> }
  };
  // setup GUI
  QWidget qWinMain;
  qWinMain.setWindowTitle("Test Dithering");
  //qWinMain.resize(800, 600);
  QGridLayout qGrid;
  QHBoxLayout qHBoxImgOrig;
  QLabel qLblImgOrig("Original Image:");
  qHBoxImgOrig.addWidget(&qLblImgOrig);
  QCheckBox qTglImgOrigGray("Gray");
  qTglImgOrigGray.setChecked(true);
  qHBoxImgOrig.addWidget(&qTglImgOrigGray, 1);
  qGrid.addLayout(&qHBoxImgOrig, 0, 0);
  QHBoxLayout qHBoxImgDith;
  QLabel qLblImgDith("Dithered Image:");
  qHBoxImgDith.addWidget(&qLblImgDith);
  QComboBox qCBoxFuncDith;
  for (const FuncEntry &entry : tblDither) {
    qCBoxFuncDith.addItem(entry.first);
  }
  qHBoxImgDith.addWidget(&qCBoxFuncDith, 1);
  qGrid.addLayout(&qHBoxImgDith, 0, 1);
  QLabel qViewImgOrig;
  qGrid.addWidget(&qViewImgOrig, 1, 0);
  QLabel qViewImgDith;
  qGrid.addWidget(&qViewImgDith, 1, 1);
  QLabel qLblDTDith("Duration:");
  qGrid.addWidget(&qLblDTDith, 2, 1);
  qGrid.setRowStretch(1, 1);
  qWinMain.setLayout(&qGrid);
  qWinMain.show();
  // install signal handlers
  auto updateImgOrig = [&]() {
    qViewImgOrig.setPixmap(
      QPixmap::fromImage(qTglImgOrigGray.isChecked() ? qImgG : qImg));
  };
  auto updateImgDith = [&]() {
    const int i = qCBoxFuncDith.currentIndex();
    const ulong dt = i < 0
      ? qImgDith.fill(0), 0ul
      : tblDither[i].second(qImgDith, qImgG);
    qViewImgDith.setPixmap(QPixmap::fromImage(qImgDith));
    qLblDTDith.setText(QString::fromUtf8("Duration: %1 \xce\xbcs").arg(dt));
  };
  updateImgOrig();
  updateImgDith();
  QObject::connect(&qTglImgOrigGray, &QCheckBox::toggled,
    [&](bool) { updateImgOrig(); });
  QObject::connect(&qCBoxFuncDith, QOverload<int>::of(&QComboBox::currentIndexChanged),
    [&](int) { updateImgDith(); });
  // runtime loop
  return app.exec();
}

为了比较不同算法的视觉结果,我使用了我的一只猫的照片。我选择了这个,因为猫是黑色的。因此,这张照片由很多灰色阴影组成,我认为这是抖动算法的一项雄心勃勃的任务。

莫里茨,猫
猫莫里茨(256×256 像素)——转换为灰度进行测试

视觉输出

testQImageDithering.exe 的快照 - 拜耳 (2×2)
拜耳抖动 (2×2)

testQImageDithering.exe 的快照 - 拜耳 (4×4)
拜耳抖动 (4×4)

testQImageDithering.exe 的快照 - 拜耳 (8×8)
拜耳抖动 (8×8)

testQImageDithering.exe 的快照 - Floyd-Steinberg
弗洛伊德-斯坦伯格抖动

testQImageDithering.exe 的快照 - Floyd-Steinberg (serp.)
Floyd-Steinberg 抖动(蛇形)

虽然这可能会给人一种不同抖动方法的视觉结果的印象,但时间应该持保留态度。

时间比较

在玩演示时,我注意到计算持续时间的输出中有大量噪音。因此,我使用另一个 1024×576 像素的图像来获得一些时间。下表提供了一般印象,但并未声称被视为严肃的基准。

Method                  │ Duration (μs)
────────────────────────┼──────────────
Bayer (2 x 2, table)    │     1371
Bayer (4 x 4, table)    │     1368
Bayer (8 x 8, table)    │     1371
Bayer (2 x 2, func.)    │     1481
Bayer (4 x 4, func.)    │     1862
Bayer (8 x 8, func.)    │     2473
Floyd Steinberg         │     4037
Floyd Steinberg (serp.) │     4396

发现:

  • 对于带有预计算表的拜耳抖动,拜耳矩阵的大小影响较小。
  • 在动态计算拜耳矩阵元素时(使用递归函数),情况并非如此。递归应该在编译时展开并不会改变这一事实。
  • Floyd-Steinberg 抖动通常较慢(尽管我认为视觉效果更好)。
  • Floyd-Steinberg 和 Floyd-Steinberg (serp.) 之间的差异可以忽略不计。(在玩弄的时候,我也经常看到相反的关系。)

testQImageDithering.exe 的快照 - 使用两只猫获得更好的可测量性能影响


OP的示例图像:

OP 的示例图像

输出:

testQImageDithering.exe 的快照 - 拜耳 (2×2)

testQImageDithering.exe 的快照 - 拜耳 (4×4)

testQImageDithering.exe 的快照 - 拜耳 (8×8)

testQImageDithering.exe 的快照 - Floyd-Steinberg

testQImageDithering.exe 的快照 - Floyd-Steinberg (serp.)

笔记:

在考虑了 OP 3d 渲染之后……应用拜耳抖动可能很容易,而对于 Floyd-Steinberg 抖动(每个像素影响右侧和底部邻居)并不容易。

于 2021-03-07T16:11:45.637 回答