Search code examples
delphi3dgeometry

Draw Sphere on TImage control of Delphi


I want to draw sphere like this: enter image description here

Below code is generates Circle's Vertices and Drawing a Circle on TIMAGE BUT i want it for SPHERE:

for i := 0 to 360 do begin 
   //Find value of X and Y 
   pntCordXY.X := Radius * Cos(DegToRad(i)); 
   pntCordXY.Y := Radius * Sin(DegToRad(i)); 
   if i = 0 then 
      image1.Canvas.MoveTo(Round(pntCordXY.X), Round(pntCordXY.Y)) 
   else 
      image1.Canvas.LineTo(Round(pntCordXY.X), Round(pntCordXY.Y)); 
end;

Solution

  • This turned out to be a fun exercise; nice question!

    At first, you ask specifically for drawing such a sphere on a TImage, but that component is supposed to be used for showing graphics. Sure, it has a canvas on which can be drawn, but hereunder I use a TPaintBox which is the preferred component for own painting. Because, you will have to paint this yourself. Entirely.

    Ingredients needed:

    • Some math for calculating the 3D points on a sphere, for rotating the globe around multiple axes, and maybe for converting the 3D points to the 2D screen coordinate system. The basics are:

      type
        TPoint3D = record
          X: Double;
          Y: Double;
          Z: Double;
        end;
      
      function Sphere(Phi, Lambda: Double): TPoint3D;
      begin
        Result.X := Cos(Phi) * Sin(Lambda);
        Result.Y := Sin(Phi);
        Result.Z := Cos(Phi) * Cos(Lambda);
      end;
      
      function RotateAroundX(const P: TPoint3D; Alfa: Double): TPoint3D;
      begin
        Result.X := P.X;
        Result.Y := P.Y * Cos(Alfa) + P.Z * Sin(Alfa);
        Result.Z := P.Y * -Sin(Alfa) + P.Z * Cos(Alfa);
      end;
      
      function RotateAroundY(const P: TPoint3D; Beta: Double): TPoint3D;
      begin
        Result.X := P.X * Cos(Beta) + P.Z * Sin(Beta);
        Result.Y := P.Y;
        Result.Z := P.X * -Sin(Beta) + P.Z * Cos(Beta);
      end;
      
    • Some globe-variables to work with:

      var
        Alfa: Integer;   //Rotation around X axis
        Beta: Integer;   //Rotation around Y axis
        C: TPoint;       //Center
        R: Integer;      //Radius
        Phi: Integer;    //Angle relative to XY plane
        Lambda: Integer; //Angle around Z axis (from pole to pole)
        P: TPoint3D;     //2D projection of a 3D point on the sphere's surface
      
    • Code to calculate all points of the latitude circles:

      for Phi := -8 to 8 do
        for Lambda := 0 to 360 do
        begin
          P := Sphere(DegToRad(Phi * 10), DegToRad(Lambda));
          P := RotateAroundX(P, Alfa);
          P := RotateAroundY(P, Beta);
        end;
      
    • Code to calculate all points of the longitude meridians:

      for Lambda := 0 to 17 do
        for Phi := 0 to 360 do
        begin
          P := Sphere(DegToRad(Phi), DegToRad(Lambda * 10));
          P := RotateAroundX(P, Alfa);
          P := RotateAroundY(P, Beta);
        end;
      

      These points can be used to draw lines or curves on the paint box. The Z value of these points are not used for drawing, but they are helpful to decide whether the point lies on the back or front side of the globe.

    • Logic and aids. Before all points, lines or curves in front of the globe can be drawn, the ones in the back of globe have to be drawn first, in order to preserve depth.

    • A drawing framework or drawing library. Delphi is by default equipped with standard Windows GDI, available via the Canvas property of the paint box. Another possibility is GDI+ which is more advanced and can be more efficient. Especially considering anti-aliassing. These are the two frameworks I worked with, but there are also others. For example: OpenGL, which converts 3D objects to 2D automatically and is capable of adding 3D surfaces, lights, materials, shaders, and many more features.

    • A testing application, which is added at the bottom of this question.

    • A double buffering technique to get the paint work flicker-free. I chose a separate bitmap object on which everything is drawn, prior to painting that bitmap on the paint box. The demo program also demonstrates the performance without it (routine: GDIMultipleColorsDirect).

    Setup:

    Drop a paint box on your form, and set its Align property to alClient, add a timer component for simulation, add form event handlers for OnCreate, OnDestroy, OnKeyPress, and OnResize, and add an event handler for PaintBox1.OnPaint.

    object Form1: TForm1
      Left = 497
      Top = 394
      Width = 450
      Height = 450
      Caption = 'Sphere'
      Color = clWhite
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWindowText
      Font.Height = -11
      Font.Name = 'MS Sans Serif'
      Font.Style = []
      OldCreateOrder = False
      OnCreate = FormCreate
      OnDestroy = FormDestroy
      OnKeyPress = FormKeyPress
      OnResize = FormResize
      PixelsPerInch = 96
      TextHeight = 13
      object PaintBox1: TPaintBox
        Left = 0
        Top = 0
        Width = 434
        Height = 414
        Align = alClient
        OnPaint = PaintBox1Paint
      end
      object Timer1: TTimer
        Interval = 25
        OnTimer = Timer1Timer
        Left = 7
        Top = 7
      end
    end
    

    First attempt:

    With default GDI, I draw lines from every point to every next point. To add a feeling of depth (perspective), I gave the lines in front a greater width. Also, I gradually let the colors of the lines overflow from dark to light (routine: GDIMultipleColors).

    Sphere 1

    Second attempt:

    Nice, but all pixels are so hard! Let's try doing some anti-aliassing ourselfs... ;) Furthermore, I reduced the color count to two: dark in front, light in the back. This in order to get rid of all separate line segments: now every circle and meridian is devided into two polylines. I used a third color in between for the anti-aliassing effect (routine: GDIThreeColors).

    Sphere 2

    GDI+ to the rescue:

    This anti-aliassing isn't most charming. To get really smooth paint work, let's convert the code to GDI+ style. For Delphi 2009 and up, the library is available from here. For older Delphi versions, the library is available from here.

    In GDI+, drawing works a bit differently. Create a TGPGraphics object and attach it to a device context with its constructor. Subsequently, drawing operations on the object are translated by the API and will be output to the destination context, the bitmap in this case (routine: GDIPlusDualLinewidths).

    Sphere 3

    Can it even better?

    Well, that's quite someting already. But this globe is made up out of polylines with just two different line widths. Let's add some in between. The count of segments in each circle or meridian is controlled by the Precision constant (routine: GDIPlusMultipleLinewidths).

    enter image description here

    Sample application:

    Press a key to cycle through the above mentioned routines.

    unit Globe;
    
    interface
    
    uses
      Windows, SysUtils, Classes, Graphics, Controls, Forms, ExtCtrls, Math,
      GDIPAPI, GDIPOBJ;
    
    type
      TForm1 = class(TForm)
        PaintBox1: TPaintBox;
        Timer1: TTimer;
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
        procedure FormResize(Sender: TObject);
        procedure Timer1Timer(Sender: TObject);
        procedure FormKeyPress(Sender: TObject; var Key: Char);
        procedure PaintBox1Paint(Sender: TObject);
      private
        FBmp: TBitmap;
        FPen: TGPPen;
        procedure GDIMultipleColorsDirect;
        procedure GDIMultipleColors;
        procedure GDIThreeColors;
        procedure GDIPlusDualLinewidths;
        procedure GDIPlusMultipleLinewidths;
      public
        A: Integer; //Alfa, rotation round X axis
        B: Integer; //Beta, rotation round Y axis
        C: TPoint;  //Center
        R: Integer; //Radius
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.DFM}
    
    const
      LineColorFore = $00552B00;
      LineColorMiddle = $00AA957F;
      LineColorBack = $00FFDFBF;
      BackColor = clWhite;
      LineWidthFore = 4.5;
      LineWidthBack = 1.5;
      Precision = 10; //Should be even!
    
    type
      TCycle = 0..Precision - 1;
    
      TPoint3D = record
        X: Double;
        Y: Double;
        Z: Double;
      end;
    
    function Sphere(Phi, Lambda: Double): TPoint3D;
    begin
      Result.X := Cos(Phi) * Sin(Lambda);
      Result.Y := Sin(Phi);
      Result.Z := Cos(Phi) * Cos(Lambda);
    end;
    
    function RotateAroundX(const P: TPoint3D; Alfa: Double): TPoint3D;
    begin
      Result.X := P.X;
      Result.Y := P.Y * Cos(Alfa) + P.Z * Sin(Alfa);
      Result.Z := P.Y * -Sin(Alfa) + P.Z * Cos(Alfa);
    end;
    
    function RotateAroundY(const P: TPoint3D; Beta: Double): TPoint3D;
    begin
      Result.X := P.X * Cos(Beta) + P.Z * Sin(Beta);
      Result.Y := P.Y;
      Result.Z := P.X * -Sin(Beta) + P.Z * Cos(Beta);
    end;
    
    { TForm1 }
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      Brush.Style := bsClear; //This is múch cheaper then DoubleBuffered := True
      FBmp := TBitmap.Create;
      FPen := TGPPen.Create(ColorRefToARGB(ColorToRGB(clBlack)));
      A := 35;
      B := 25;
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      FPen.Free;
      FBmp.Free;
    end;
    
    procedure TForm1.FormResize(Sender: TObject);
    begin
      C.X := PaintBox1.ClientWidth div 2;
      C.Y := PaintBox1.ClientHeight div 2;
      R := Min(C.X, C.Y) - 10;
      FBmp.Width := PaintBox1.ClientWidth;
      FBmp.Height := PaintBox1.ClientHeight;
    end;
    
    procedure TForm1.Timer1Timer(Sender: TObject);
    begin
      A := A + 2;
      B := B + 1;
      PaintBox1.Invalidate;
    end;
    
    procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
    begin
      Tag := Tag + 1;
      PaintBox1.Invalidate;
    end;
    
    procedure TForm1.PaintBox1Paint(Sender: TObject);
    begin
      case Tag mod 5 of
        0: GDIMultipleColorsDirect;
        1: GDIMultipleColors;
        2: GDIThreeColors;
        3: GDIPlusDualLinewidths;
        4: GDIPlusMultipleLinewidths;
      end;
    end;
    
    procedure TForm1.GDIPlusMultipleLinewidths;
    var
      Lines: array of TPointFDynArray;
      PointCount: Integer;
      LineCount: Integer;
      Drawing: TGPGraphics;
      Alfa: Double;
      Beta: Double;
      Cycle: TCycle;
      Phi: Integer;
      Lambda: Integer;
      P: TPoint3D;
      Filter: TCycle;
      PrevFilter: TCycle;
      I: Integer;
    
      procedure ResetLines;
      begin
        SetLength(Lines, 0);
        LineCount := 0;
        PointCount := 0;
      end;
    
      procedure FinishLastLine;
      begin
        if PointCount < 2 then
          Dec(LineCount)
        else
          SetLength(Lines[LineCount - 1], PointCount);
      end;
    
      procedure NewLine;
      begin
        if LineCount > 0 then
          FinishLastLine;
        SetLength(Lines, LineCount + 1);
        SetLength(Lines[LineCount], 361);
        Inc(LineCount);
        PointCount := 0;
      end;
    
      procedure AddPoint(X, Y: Single);
      begin
        Lines[LineCount - 1][PointCount] := MakePoint(X, Y);
        Inc(PointCount);
      end;
    
      function CycleFromZ(Z: Single): TCycle;
      begin
        Result := Round((Z + 1) / 2 * High(TCycle));
      end;
    
      function CycleToLineWidth(ACycle: TCycle): Single;
      begin
        Result := LineWidthBack +
          (LineWidthFore - LineWidthBack) * (ACycle / High(TCycle));
      end;
    
      function CycleToLineColor(ACycle: TCycle): TGPColor;
      begin
        if ACycle <= (High(TCycle) div 2) then
          Result := ColorRefToARGB(ColorToRGB(LineColorBack))
        else
          Result := ColorRefToARGB(ColorToRGB(LineColorFore));
      end;
    
    begin
      Drawing := TGPGraphics.Create(FBmp.Canvas.Handle);
      try
        Drawing.Clear(ColorRefToARGB(ColorToRGB(clWhite)));
        Drawing.SetSmoothingMode(SmoothingModeAntiAlias);
        Alfa := DegToRad(A);
        Beta := DegToRad(B);
        for Cycle := Low(TCycle) to High(TCycle) do
        begin
          ResetLines;
          //Latitude
          for Phi := -8 to 8 do
          begin
            NewLine;
            PrevFilter := 0;
            for Lambda := 0 to 360 do
            begin
              P := Sphere(DegToRad(Phi * 10), DegToRad(Lambda));
              P := RotateAroundX(P, Alfa);
              P := RotateAroundY(P, Beta);
              Filter := CycleFromZ(P.Z);
              if Filter <> PrevFilter then
              begin
                AddPoint(C.X + P.X * R, C.Y + P.Y * R);
                NewLine;
              end;
              if Filter = Cycle then
                AddPoint(C.X + P.X * R, C.Y + P.Y * R);
              PrevFilter := Filter;
            end;
          end;
          //Longitude
          for Lambda := 0 to 17 do
          begin
            NewLine;
            PrevFilter := 0;
            for Phi := 0 to 360 do
            begin
              P := Sphere(DegToRad(Phi), DegToRad(Lambda * 10));
              P := RotateAroundX(P, Alfa);
              P := RotateAroundY(P, Beta);
              Filter := CycleFromZ(P.Z);
              if Filter <> PrevFilter then
              begin
                AddPoint(C.X + P.X * R, C.Y + P.Y * R);
                NewLine;
              end;
              if Filter = Cycle then
                AddPoint(C.X + P.X * R, C.Y + P.Y * R);
              PrevFilter := Filter;
            end;
          end;
          FinishLastLine;
          FPen.SetColor(CycleToLineColor(Cycle));
          FPen.SetWidth(CycleToLineWidth(Cycle));
          for I := 0 to LineCount - 1 do
            Drawing.DrawLines(FPen, PGPPointF(@(Lines[I][0])), Length(Lines[I]));
          if Cycle = (High(TCycle) div 2 + 1) then
            Drawing.DrawEllipse(FPen, C.X - R, C.Y - R, 2 * R, 2 * R);
        end;
      finally
        Drawing.Free;
      end;
      PaintBox1.Canvas.Draw(0, 0, FBmp);
    end;
    
    procedure TForm1.GDIPlusDualLinewidths;
    const
      LineColors: array[Boolean] of TColor = (LineColorFore, LineColorBack);
      LineWidths: array[Boolean] of Single = (LineWidthFore, LineWidthBack);
      BackColor = clWhite;
    var
      Lines: array of TPointFDynArray;
      PointCount: Integer;
      LineCount: Integer;
      Drawing: TGPGraphics;
      Alfa: Double;
      Beta: Double;
      Phi: Integer;
      Lambda: Integer;
      BackSide: Boolean;
      P: TPoint3D;
      PrevZ: Double;
      I: Integer;
    
      procedure ResetLines;
      begin
        SetLength(Lines, 0);
        LineCount := 0;
        PointCount := 0;
      end;
    
      procedure FinishLastLine;
      begin
        if PointCount < 2 then
          Dec(LineCount)
        else
          SetLength(Lines[LineCount - 1], PointCount);
      end;
    
      procedure NewLine;
      begin
        if LineCount > 0 then
          FinishLastLine;
        SetLength(Lines, LineCount + 1);
        SetLength(Lines[LineCount], 361);
        Inc(LineCount);
        PointCount := 0;
      end;
    
      procedure AddPoint(X, Y: Single);
      begin
        Lines[LineCount - 1][PointCount] := MakePoint(X, Y);
        Inc(PointCount);
      end;
    
    begin
      Drawing := TGPGraphics.Create(FBmp.Canvas.Handle);
      try
        Drawing.Clear(ColorRefToARGB(ColorToRGB(clWhite)));
        Drawing.SetSmoothingMode(SmoothingModeAntiAlias);
        Alfa := DegToRad(A);
        Beta := DegToRad(B);
        for BackSide := True downto False do
        begin
          ResetLines;
          //Latitude
          for Phi := -8 to 8 do
          begin
            NewLine;
            PrevZ := 0;
            for Lambda := 0 to 360 do
            begin
              P := Sphere(DegToRad(Phi * 10), DegToRad(Lambda));
              P := RotateAroundX(P, Alfa);
              P := RotateAroundY(P, Beta);
              if Sign(P.Z) <> Sign(PrevZ) then
                NewLine;
              if (BackSide and (P.Z < 0)) or (not BackSide and (P.Z >= 0)) then
                AddPoint(C.X + P.X * R, C.Y + P.Y * R);
              PrevZ := P.Z;
            end;
          end;
          //Longitude
          for Lambda := 0 to 17 do
          begin
            NewLine;
            PrevZ := 0;
            for Phi := 0 to 360 do
            begin
              P := Sphere(DegToRad(Phi), DegToRad(Lambda * 10));
              P := RotateAroundX(P, Alfa);
              P := RotateAroundY(P, Beta);
              if Sign(P.Z) <> Sign(PrevZ) then
                NewLine;
              if (BackSide and (P.Z < 0)) or (not BackSide and (P.Z >= 0)) then
                AddPoint(C.X + P.X * R, C.Y + P.Y * R);
              PrevZ := P.Z;
            end;
          end;
          FinishLastLine;
          FPen.SetColor(ColorRefToARGB(ColorToRGB(LineColors[BackSide])));
          FPen.SetWidth(LineWidths[BackSide]);
          for I := 0 to LineCount - 1 do
            Drawing.DrawLines(FPen, PGPPointF(@(Lines[I][0])), Length(Lines[I]));
        end;
        Drawing.DrawEllipse(FPen, C.X - R, C.Y - R, 2 * R, 2 * R);
      finally
        Drawing.Free;
      end;
      PaintBox1.Canvas.Draw(0, 0, FBmp);
    end;
    
    procedure TForm1.GDIThreeColors;
    const
      LineColors: array[TValueSign] of TColor = (LineColorBack, LineColorMiddle,
        LineColorFore);
      LineWidths: array[TValueSign] of Integer = (2, 4, 2);
    var
      Lines: array of array of TPoint;
      PointCount: Integer;
      LineCount: Integer;
      Alfa: Double;
      Beta: Double;
      Phi: Integer;
      Lambda: Integer;
      BackSide: Boolean;
      P: TPoint3D;
      PrevZ: Double;
      I: TValueSign;
      J: Integer;
    
      procedure ResetLines;
      begin
        SetLength(Lines, 0);
        LineCount := 0;
        PointCount := 0;
      end;
    
      procedure FinishLastLine;
      begin
        if PointCount < 2 then
          Dec(LineCount)
        else
          SetLength(Lines[LineCount - 1], PointCount);
      end;
    
      procedure NewLine;
      begin
        if LineCount > 0 then
          FinishLastLine;
        SetLength(Lines, LineCount + 1);
        SetLength(Lines[LineCount], 361);
        Inc(LineCount);
        PointCount := 0;
      end;
    
      procedure AddPoint(APoint: TPoint); overload;
      var
        Last: TPoint;
      begin
        if PointCount > 0 then
        begin
          Last := Lines[LineCount - 1][PointCount - 1];
          if (APoint.X = Last.X) and (APoint.Y = Last.Y) then
            Exit;
        end;
        Lines[LineCount - 1][PointCount] := APoint;
        Inc(PointCount);
      end;
    
      procedure AddPoint(X, Y: Integer); overload;
      begin
        AddPoint(Point(X, Y));
      end;
    
    begin
      FBmp.Canvas.Brush.Color := BackColor;
      FBmp.Canvas.FillRect(Rect(0, 0, FBmp.Width, FBmp.Height));
      Alfa := DegToRad(A);
      Beta := DegToRad(B);
      for BackSide := True downto False do
      begin
        ResetLines;
        //Latitude
        for Phi := -8 to 8 do
        begin
          NewLine;
          PrevZ := 0;
          for Lambda := 0 to 360 do
          begin
            P := Sphere(DegToRad(Phi * 10), DegToRad(Lambda));
            P := RotateAroundX(P, Alfa);
            P := RotateAroundY(P, Beta);
            if Sign(P.Z) <> Sign(PrevZ) then
              NewLine;
            if (BackSide and (P.Z < 0)) or (not BackSide and (P.Z >= 0)) then
              AddPoint(Round(C.X + P.X * R), Round(C.Y + P.Y * R));
            PrevZ := P.Z;
          end;
        end;
        //Longitude
        for Lambda := 0 to 17 do
        begin
          NewLine;
          PrevZ := 0;
          for Phi := 0 to 360 do
          begin
            P := Sphere(DegToRad(Phi), DegToRad(Lambda * 10));
            P := RotateAroundX(P, Alfa);
            P := RotateAroundY(P, Beta);
            if Sign(P.Z) <> Sign(PrevZ) then
              NewLine;
            if (BackSide and (P.Z < 0)) or (not BackSide and (P.Z >= 0)) then
              AddPoint(Round(C.X + P.X * R), Round(C.Y + P.Y * R));
            PrevZ := P.Z;
          end;
        end;
        FinishLastLine;
        if BackSide then
        begin
          FBmp.Canvas.Pen.Color := LineColors[-1];
          FBmp.Canvas.Pen.Width := LineWidths[-1];
          for J := 0 to LineCount - 1 do
            FBmp.Canvas.Polyline(Lines[J]);
        end
        else
          for I := 0 to 1 do
          begin
            FBmp.Canvas.Pen.Color := LineColors[I];
            FBmp.Canvas.Pen.Width := LineWidths[I];
            for J := 0 to LineCount - 1 do
              FBmp.Canvas.Polyline(Lines[J])
          end
      end;
      FBmp.Canvas.Brush.Style := bsClear;
      FBmp.Canvas.Ellipse(C.X - R, C.Y - R, C.X + R, C.Y + R);
      PaintBox1.Canvas.Draw(0, 0, FBmp);
    end;
    
    procedure TForm1.GDIMultipleColors;
    var
      Alfa: Double;
      Beta: Double;
      Phi: Integer;
      Lambda: Integer;
      P: TPoint3D;
      Backside: Boolean;
    
      function ColorFromZ(Z: Single): TColorRef;
      var
        R: Integer;
        G: Integer;
        B: Integer;
      begin
        Z := (Z + 1) / 2;
        R := GetRValue(LineColorFore) - GetRValue(LineColorBack);
        R := GetRValue(LineColorBack) + Round(Z * R);
        G := GetGValue(LineColorFore) - GetGValue(LineColorBack);
        G := GetGValue(LineColorBack) + Round(Z * G);
        B := GetBValue(LineColorFore) - GetBValue(LineColorBack);
        B := GetBValue(LineColorBack) + Round(Z * B);
        Result := RGB(R, G, B);
      end;
    
    begin
      FBmp.Canvas.Pen.Width := 2;
      FBmp.Canvas.Brush.Color := BackColor;
      FBmp.Canvas.FillRect(PaintBox1.ClientRect);
      Alfa := DegToRad(A);
      Beta := DegToRad(B);
      for Backside := True downto False do
      begin
        if not BackSide then
          FBmp.Canvas.Pen.Width := 3;
        //Latitude
        for Phi := -8 to 8 do
          for Lambda := 0 to 360 do
          begin
            P := Sphere(DegToRad(Phi * 10), DegToRad(Lambda));
            P := RotateAroundX(P, Alfa);
            P := RotateAroundY(P, Beta);
            if (Lambda = 0) or (Backside and (P.Z >= 0)) or
              (not Backside and (P.Z < 0)) then
                FBmp.Canvas.MoveTo(C.X + Round(P.X * R), C.Y + Round(P.Y * R))
            else
            begin
              FBmp.Canvas.Pen.Color := ColorFromZ(P.Z);
              FBmp.Canvas.LineTo(C.X + Round(P.X * R), C.Y + Round(P.Y * R));
            end;
          end;
        //Longitude
        for Lambda := 0 to 17 do
          for Phi := 0 to 360 do
          begin
            P := Sphere(DegToRad(Phi), DegToRad(Lambda * 10));
            P := RotateAroundX(P, Alfa);
            P := RotateAroundY(P, Beta);
            if (Phi = 0) or (Backside and (P.Z >= 0)) or
              (not Backside and (P.Z < 0)) then
                FBmp.Canvas.MoveTo(C.X + Round(P.X * R), C.Y + Round(P.Y * R))
            else
            begin
              FBmp.Canvas.Pen.Color := ColorFromZ(P.Z);
              FBmp.Canvas.LineTo(C.X + Round(P.X * R), C.Y + Round(P.Y * R));
            end;
          end;
      end;
      PaintBox1.Canvas.Draw(0, 0, FBmp);
    end;
    
    procedure TForm1.GDIMultipleColorsDirect;
    var
      Alfa: Double;
      Beta: Double;
      Phi: Integer;
      Lambda: Integer;
      P: TPoint3D;
      Backside: Boolean;
    
      function ColorFromZ(Z: Single): TColorRef;
      var
        R: Integer;
        G: Integer;
        B: Integer;
      begin
        Z := (Z + 1) / 2;
        R := GetRValue(LineColorFore) - GetRValue(LineColorBack);
        R := GetRValue(LineColorBack) + Round(Z * R);
        G := GetGValue(LineColorFore) - GetGValue(LineColorBack);
        G := GetGValue(LineColorBack) + Round(Z * G);
        B := GetBValue(LineColorFore) - GetBValue(LineColorBack);
        B := GetBValue(LineColorBack) + Round(Z * B);
        Result := RGB(R, G, B);
      end;
    
    begin
      PaintBox1.Canvas.Pen.Width := 2;
      PaintBox1.Canvas.Brush.Color := BackColor;
      PaintBox1.Canvas.FillRect(PaintBox1.ClientRect);
      Alfa := DegToRad(A);
      Beta := DegToRad(B);
      for Backside := True downto False do
      begin
        if not BackSide then
          PaintBox1.Canvas.Pen.Width := 3;
        //Latitude
        for Phi := -8 to 8 do
          for Lambda := 0 to 360 do
          begin
            P := Sphere(DegToRad(Phi * 10), DegToRad(Lambda));
            P := RotateAroundX(P, Alfa);
            P := RotateAroundY(P, Beta);
            if (Lambda = 0) or (Backside and (P.Z >= 0)) or
              (not Backside and (P.Z < 0)) then
                PaintBox1.Canvas.MoveTo(C.X + Round(P.X * R), C.Y + Round(P.Y * R))
            else
            begin
              PaintBox1.Canvas.Pen.Color := ColorFromZ(P.Z);
              PaintBox1.Canvas.LineTo(C.X + Round(P.X * R), C.Y + Round(P.Y * R));
            end;
          end;
        //Longitude
        for Lambda := 0 to 17 do
          for Phi := 0 to 360 do
          begin
            P := Sphere(DegToRad(Phi), DegToRad(Lambda * 10));
            P := RotateAroundX(P, Alfa);
            P := RotateAroundY(P, Beta);
            if (Phi = 0) or (Backside and (P.Z >= 0)) or
              (not Backside and (P.Z < 0)) then
                PaintBox1.Canvas.MoveTo(C.X + Round(P.X * R), C.Y + Round(P.Y * R))
            else
            begin
              PaintBox1.Canvas.Pen.Color := ColorFromZ(P.Z);
              PaintBox1.Canvas.LineTo(C.X + Round(P.X * R), C.Y + Round(P.Y * R));
            end;
          end;
      end;
    end;
    
    end.
    

    (With thanks to bummi's comment.)