Search code examples
c++graphicsc++buildervcl

Antialiased line with C++ Builder VCL


Question: I need to upgrade an old Embarcadero VCL graphic math application by introducing antialiased lines. So, I wrote in C++ the algorithm indicated in the page: https://en.wikipedia.org/wiki/Xiaolin_Wu%27s_line_algorithm.

How to write correctly the function 'plot' to draw the pixel at (x,y) with a brightness 'c', especially on the Embarcadero VCL.

Solution: This solution has been possible by the contribution of @Spektre (use of a union to mix colors according to some brightness). pC is a canvas pointer, funcColor is the line intended color, and are properties of the Observer class:

//Antialiased line:
void Observer::aaLine(int x0, int y0, int x1, int y1)
{
union {
        uint32_t dd;//The color value
        uint8_t  db[4];//To work on channels: {00.RR.GG.BB}
    } c, c0;//Line color, and background color

    //Color mixer, with calculations on each channel, because there is no
    //Alpha channel with VCL:
    auto plot = [&](int X, int Y, float brightness){
        c.dd  = funcColor;//Line color
        c0.dd = pC->Pixels[X][Y];//Background color
        //Find coefficients to simulate transparency, where there is not:
        //Front color is augmented when background is decreased:
        for(int i = 0; i < 3; ++i)
            c.db[i] = int(c.db[i] * brightness + c0.db[i] * (1 - brightness));
        //Output obtained by conversion:
        pC->Pixels[X][Y] = static_cast<TColor>(c.dd);
    };

    //Wu's algorithm:
    //Fractional part of x:
    auto fpart  = [](double x) { return x - floor(x); };
    auto rfpart = [&](double x) { return 1 - fpart(x); };

    bool steep = abs(y1 - y0) > abs(x1 - x0);//Means slope > 45 deg.

    if(steep) {
        std::swap(x0, y0);
        std::swap(x1, y1);
    }

    if( x0 > x1 ) {
        std::swap(x0, x1);
        std::swap(y0, y1);
    }

    double  dx = x1 - x0, dy = y1 - y0, gradient = (dx == 0. ? 1. : dy/dx) ;

    //Handle first endpoint
    double xend = x0,
         yend  = y0 + gradient * (xend - x0),
         xgap  = rfpart(x0 + 0.5),
         xpxl1 = xend, // this will be used in the main loop
         ypxl1 = floor(yend);

    if( steep ) {
        plot(ypxl1,   xpxl1, rfpart(yend) * xgap);
        plot(ypxl1+1, xpxl1,  fpart(yend) * xgap);
    }
    else {
        plot(xpxl1, ypxl1  , rfpart(yend) * xgap);
        plot(xpxl1, ypxl1+1,  fpart(yend) * xgap);
    }
    auto intery = yend + gradient; // first y-intersection for the main loop

    //Handle second endpoint
    xend = round(x1);
    yend = y1 + gradient * (xend - x1);
    xgap = fpart(x1 + 0.5);
    auto xpxl2 = xend, //this will be used in the main loop
         ypxl2 = floor(yend);

    if( steep ){
        plot(ypxl2  , xpxl2, rfpart(yend) * xgap);
        plot(ypxl2+1, xpxl2,  fpart(yend) * xgap);
        //Main loop:
        for(double x = xpxl1 + 1 ; x <= xpxl2 - 1 ; x += 1) {
            plot(int(intery)  , x, rfpart(intery));
            plot(int(intery+1), x,  fpart(intery));
            intery += gradient;
        }
    }
    else {
        plot(xpxl2, ypxl2,  rfpart(yend) * xgap);
        plot(xpxl2, ypxl2+1, fpart(yend) * xgap);
        //Main loop:
        for(double x = xpxl1 + 1 ; x <= xpxl2 - 1 ; x += 1) {
            plot(x, int(intery),  rfpart(intery));
            plot(x, int(intery+1), fpart(intery));
            intery += gradient;
        }
    }    
}//Observer::aaLine.

The source code above is updated, and works for me as a solution. The image below comes from tests: Blue's are NOT antialiased, and Red's ones are the results from the solution above. I am satisfied with what I want to do. Red=anti-alias, Blue=no anti-alias


Solution

  • I think your problem lies in this:

    auto plot = [&](double X, double Y, double brighness){
        pC->Pixels[X][Y] = brightness; };
    

    If I understand it correctly pC is some target TCanvas ... this has 2 major problems:

    1. pC->Pixels[X][Y] = brightness; will handle brightness as color according to selected mode (so copy,xor,... or whatever) and not as brightness.

      I would use form of alpha blending where you take originaly render color (or background) and wanted color of rendered line and mix it with brightness as parameter:

       TColor c0=pC->Pixels[X][Y],c0=color of your line;
      
       // here mix colors c = (c0*(1.0-brightness)) + (c1*brightness)
       // however you need to do this according to selected pixelformat of you graphic object and color channel wise...
      
       pC->Pixels[X][Y]=c;
      

      Beware VCL transparency does not use alpha parameter its just opaque or not ... For more info about the mixing see similar:

      especially pay attention to the:

       union
               {
               DWORD dd;
               BYTE db[4];
               } c,c0;
      

      as TColor is 32bit int anyway ...

    2. speed of pC->Pixels[X][Y] in VCL (or any GDI based api) is pitiful at best

      in case you handle many pixels you should consider to use ScanLine[Y] from Graphics::TBitmap ... and render to bitmap as backbufer. This usually improve speed from ~1000 to ~10000 times. for more info see: