unit Frost.RenderEngine;

interface

uses
  Frost.GameObjects,
  OpenGl,
  _Geometry,
  Frost.GL;  

const
  INTERFACE_LAYER =  15;
     
type
TCameraClearMode = (
  None       = 0,
  ClearAll   = 1,
  ClearDepth = 2,
  ClearColor = 3);

TViewPort = array[0..3] of longint;

TCamera = class (TComponent)
  private                   
    FPosition: TMatrix4;
    FView: TMatrix4;
    FOrder: Byte;
    FMask: Cardinal;
    FClearMode: TCameraClearMode;
    FClearBitField: TGLbitfield;
    procedure Use;
    procedure SetClearMode(ClearMode: TCameraClearMode);
  public
    function PointToScreen(Point: TVector3): TVector3;  
    function ScreenToPoint(X, Y: Integer; Depth: Single = 0): TVector3;
    //
    procedure OnStart; override;
    procedure OnUpdate(const delta: Double); override;
    procedure OnDestroy; override;
    procedure SetPerspectiveView(Fov, Aspect: Double; ZNear: Double = 0.05; ZFar: Double = 500);
    procedure SetOrthoView(Left, Right, Bottom, Top: Double; ZNear: Double = 0.05; ZFar: Double = 500);
    property Order: Byte read FOrder write FOrder;
    property View: TMatrix4 read FView;
    property Mask: Cardinal read FMask write FMask;  
    property ClearMode: TCameraClearMode read FClearMode write SetClearMode;
end;

TRenderLayer = class
  protected
    FOrder: Byte;      
    procedure Render(Mask: Cardinal); virtual;
  public
    property Order: Byte read FOrder write FOrder;
  end;

TRenderEngine = class sealed
    class var FInstance: TRenderEngine;
  private
    FCameraCount,
    FMaxCameras: Integer;
    FCamera: array of TCamera;
    FLayerCount,
    FMaxLayers: Integer;
    FLayer: array of TRenderLayer;
  public
    constructor Create(Camera: TCamera);
    destructor Destroy; override;
    procedure AddCamera(Camera: TCamera);
    procedure AddLayer(Layer: TRenderLayer);
    procedure Render;
  end;

implementation

{$REGION 'Matrix operations'}
function PerspectiveMatrix(Fov, Aspect, ZNear, ZFar : Double): TMatrix4;
var
  XMin, YMin, XMax, YMax: Double;
begin
  YMax := ZNear * Tan(Fov * PI / 360.0);
  YMin := -YMax;
  XMin := YMin * aspect;
  XMax := YMax * aspect;

  Result.A[0, 0] := (2.0 * ZNear) / (xmax - xmin);
  Result.A[0, 1] := 0;
  Result.A[0, 2] := (XMax + XMin) / (XMax - XMin);
  Result.A[0, 3] := 0;
  Result.A[1, 0] := 0;
  Result.A[1, 1] := (2.0 * ZNear) / (ymax - ymin);
  Result.A[1, 2] := (YMax + YMin) / (YMax - YMin);
  Result.A[1, 3] := 0;
  Result.A[2, 0] := 0;
  Result.A[2, 1] := 0;
  Result.A[2, 2] := -(ZFar + ZNear) / (ZFar - ZNear);
  Result.A[2, 3] := -(2.0 * ZFar * ZNear) / (ZFar - ZNear);
  Result.A[3, 0] := 0;
  Result.A[3, 1] := 0;
  Result.A[3, 2] := -1.0;
  Result.A[3, 3] := 0;
end;

function OrthoMatrix(Left, Right, Bottom, Top, ZNear, ZFar: Double): TMatrix4;
var
  Tx, Ty, Tz: Double;
begin
  Tx := - (Right + Left) / (Right - Left);
  Ty := - (Top + Bottom) / (Top - Bottom);
  Tz := - (ZFar + ZNear) / (ZFar - ZNear);

  Result.A[0, 0] := 2.0 / (Right - Left);
  Result.A[0, 1] := 0;
  Result.A[0, 2] := 0;
  Result.A[0, 3] := Tx;

  Result.A[1, 0] := 0;
  Result.A[1, 1] := 2.0 / (Top - Bottom);
  Result.A[1, 2] := 0;
  Result.A[1, 3] := Ty;

  Result.A[2, 0] := 0;
  Result.A[2, 1] := 0;
  Result.A[2, 2] := -2.0 / (ZFar - ZNear);
  Result.A[2, 3] := Tz;

  Result.A[3, 0] := 0;
  Result.A[3, 1] := 0;
  Result.A[3, 2] := 0;
  Result.A[3, 3] := 1;
end;
{$ENDREGION}

{$REGION 'project / unproject'}
// TODO refactoring needed
function ProjectPointToScreen(
    objx: Single; objy: Single; objz: Single;
    const modelview: TMatrix4;
    const projection: TMatrix4;
    viewport: TVector4I): TVector3;
var
  fTempo: Array [0..7] of Single;
begin
  fTempo[0] := modelview.L[0] * objx + modelview.L[4] * objy + modelview.L[8]  * objz + modelview.L[12];  //w is always 1
  fTempo[1] := modelview.L[1] * objx + modelview.L[5] * objy + modelview.L[9]  * objz + modelview.L[13];
  fTempo[2] := modelview.L[2] * objx + modelview.L[6] * objy + modelview.L[10] * objz + modelview.L[14];
  fTempo[3] := modelview.L[3] * objx + modelview.L[7] * objy + modelview.L[11] * objz + modelview.L[15];
  //Modelview transform
  //Projection transform, the final row of projection matrix is always [0 0 -1 0]
  //so we optimize for that.
  fTempo[4] := projection.L[0] * fTempo[0] + projection.L[4] * fTempo[1] + projection.L[8]  * fTempo[2] + projection.L[12] * fTempo[3];
  fTempo[5] := projection.L[1] * fTempo[0] + projection.L[5] * fTempo[1] + projection.L[9]  * fTempo[2] + projection.L[13] * fTempo[3];
  fTempo[6] := projection.L[2] * fTempo[0] + projection.L[6] * fTempo[1] + projection.L[10] * fTempo[2] + projection.L[14] * fTempo[3];
  fTempo[7] := -fTempo[2]; 
  //The result normalizes between -1 and 1
  if (fTempo[7] = 0.0) then        //The w value
  begin
    Result := Vector3(-1, -1, -1);
  end;
  fTempo[7] := 1.0 / fTempo[7];
  //Perspective division
  fTempo[4] := fTempo[4] * fTempo[7];
  fTempo[5] := fTempo[5] * fTempo[7];
  fTempo[6] := fTempo[6] * fTempo[7];
  //Window coordinates
  //Map x, y to range 0-1
  Result.X := (fTempo[4] * 0.5 + 0.5) * viewport[2] + viewport[0];
  Result.Y := (fTempo[5] * 0.5 + 0.5) * viewport[3] + viewport[1];
  //This is only correct when glDepthRange(0.0, 1.0)
  Result.Z := (1.0 + fTempo[6]) * 0.5;  //Between 0 and 1
end;

//function ProjectScreenTOPoint(
//    winx: Single; winy: Single; winz: Single;
//    const modelview: TMatrix4;
//    const projection: TMatrix4;
//    viewport: TViewPort): TVector3;
//var
//  A, M: TMatrix4;
//  InV, OutV: TVector4;
//begin
//  A := projection * modelview;
//  InV[0] := (winx - viewport[0]) / viewport[2] * 2.0 - 1.0;
//  InV[1] := (winy - viewport[1]) / viewport[3] * 2.0 - 1.0;
//  InV[2] := 2.0 * winz - 1.0;
//  InV[3] := 1.0;
//end;
//      //Calculation for inverting a matrix, compute projection x modelview
//      //and store in A[16]
//      MultiplyMatrices4by4OpenGL_FLOAT(A, projection, modelview);
//      //Now compute the inverse of matrix A
//      if(glhInvertMatrixf2(A, m)==0)
//         return 0;
//      //Transformation of normalized coordinates between -1 and 1
//      in[0]=(winx-(float)viewport[0])/(float)viewport[2]*2.0-1.0;
//      in[1]=(winy-(float)viewport[1])/(float)viewport[3]*2.0-1.0;
//      in[2]=2.0*winz-1.0;
//      in[3]=1.0;
//      //Objects coordinates
//      MultiplyMatrixByVector4by4OpenGL_FLOAT(out, m, in);
//      if(out[3]==0.0)
//         return 0;
//      out[3]=1.0/out[3];
//      objectCoordinate[0]=out[0]*out[3];
//      objectCoordinate[1]=out[1]*out[3];
//      objectCoordinate[2]=out[2]*out[3];
//      return 1;
//  }
{$ENDREGION}

{$REGION 'TCamera'}
procedure TCamera.Use;
begin
  glClear(FClearBitField);  //clear
  glMatrixMode(GL_Projection);
  glLoadIdentity;
  glMultMatrixf(@FView);
  glMatrixMode(Gl_ModelView);
  glLoadIdentity;
  glMultMatrixf(@FPosition);
end;

procedure TCamera.SetPerspectiveView(Fov, Aspect: Double; ZNear: Double = 0.05; ZFar: Double = 500);
begin
  FView := PerspectiveMatrix(Fov, Aspect, ZNear, ZFar);
end;

procedure TCamera.SetOrthoView(Left, Right, Bottom, Top: Double; ZNear: Double = 0.05; ZFar: Double = 500);
begin
  FView := OrthoMatrix(Left, Right, Bottom, Top, ZNear, ZFar);
end;

{$REGION 'Native'}
// TODO
function Point_To_Coord(Point: TVector3; Modelview, Projection: TMatrix4d; Viewport: TVector4I): TVector3;
var
  X, Y, Z: GLdouble;
begin
  gluProject(
      Point.X, Point.Y, Point.Z,
      ModelView, Projection,
      Viewport, @X, @Y, @Z);
  Result.Vector3(X, Y, Z);
end;
     
// TODO
function Coord_To_Point(MouseCoords: TPoint; Depth: Single; Modelview, Projection: TMatrix4d; Viewport: TVector4I): TVector3;
Var
  X,Y,Z: GLdouble;
//  Viewport: TVector4i;
//  Projection, Modelview: TMatrix4d;
begin
//  glGetIntegerv(GL_VIEWPORT,@Viewport);
//  glGetDoublev(GL_PROJECTION_MATRIX,@Projection);
//  glGetDoublev(GL_MODELVIEW_MATRIX,@Modelview);
  gluUnProject(
      MouseCoords.X, MouseCoords.Y, Depth,
      Modelview, Projection, Viewport,
      @X, @Y, @Z);
  Result.Vector3(X, Y, Z);
end;
{$ENDREGION}

function TCamera.PointToScreen(Point: TVector3): TVector3;
var
  ViewPort: TVector4I;
//  Modelview, Projection: TMatrix4d;
  i, j: byte;
begin
  ViewPort[0] := 0;
  ViewPort[1] := 0;
  ViewPort[2] := GL.Width;
  ViewPort[3] := GL.Height;
  Result := ProjectPointToScreen(Point.X, Point.Y, Point.Z, FPosition, FView, ViewPort);
//  for i := 0 to 3 do
//  for j := 0 to 3 do
//  begin
//    Modelview[i][j] := FPosition.A[i][j];
//    Projection[i][j] := FView.A[i][j];
//  end;
//  Result := Point_To_Coord(Point, Modelview, Projection, ViewPort);
end;

function TCamera.ScreenToPoint(X, Y: Integer; Depth: Single = 0): TVector3;
var
  ViewPort: TVector4I;
  Modelview, Projection: TMatrix4d;
  i, j: byte;
begin
  ViewPort[0] := 0;
  ViewPort[1] := 0;
  ViewPort[2] := GL.Width;
  ViewPort[3] := GL.Height;
  for i := 0 to 3 do
    for j := 0 to 3 do
    begin
      Modelview[i][j] := FPosition.A[i][j];
      Projection[i][j] := FView.A[i][j];
    end;
  Result := FPosition * Coord_To_Point(Point(X, Y), Depth, Modelview, Projection, ViewPort);
end;

procedure TCamera.SetClearMode(ClearMode: TCameraClearMode);
begin                    
  FClearMode := ClearMode;
  case ClearMode of
    None: FClearBitField := 0;
    ClearAll: FClearBitField := GL_Color_Buffer_Bit xor GL_Depth_Buffer_Bit;
    ClearDepth: FClearBitField := GL_Depth_Buffer_Bit;
    ClearColor: FClearBitField := GL_Color_Buffer_Bit;
  end;
end;

procedure TCamera.OnStart;
begin
  FView := PerspectiveMatrix(45, GL.Width / GL.Height, 0.5, 500);
  FMask := 1 xor FMask;           // 0x11111111..10

  FMask := 1 shl INTERFACE_LAYER; // 0x10000000..00
  FMask := not FMask;             // 0x01111111..11
  SetClearMode(None);
  if (TRenderEngine.FInstance <> nil) then
    TRenderEngine.FInstance.AddCamera(Self);
end;

procedure TCamera.OnUpdate(const delta: Double);
begin
  FPosition := Matrix4(GameObject.Rotation.Transpose, -GameObject.Rotation * GameObject.Position);
end;

procedure TCamera.OnDestroy;
begin

end;
{$ENDREGION}

{$REGION 'TLayer'}
procedure TRenderLayer.Render(Mask: Cardinal);
begin

end;
{$ENDREGION}

{$REGION 'TRenderEngine'}
constructor TRenderEngine.Create(Camera: TCamera);
begin
  FInstance := Self;
  FCameraCount := 0;
  FMaxCameras := 0;
  FLayerCount := 0;
  FMaxLayers := 0;
  if Camera <> nil then
  begin
    AddCamera(Camera);
  end;
end;

destructor TRenderEngine.Destroy;
begin
  inherited;
end;

procedure TRenderEngine.AddCamera(Camera: TCamera);
begin
  if Camera = nil then
    Exit;
  if (FCameraCount >= FMaxCameras) then
    begin
      FMaxCameras := FMaxCameras + 1;
      SetLength(FCamera, FMaxCameras + 1);
    end;
  FCamera[FCameraCount] := Camera;
  Camera.Order := FCameraCount;
  Inc(FCameraCount);
end;

procedure TRenderEngine.AddLayer(Layer: TRenderLayer);
begin
  if Layer = nil then
    Exit;
  if (FLayerCount >= FMaxLayers) then
  begin
    FMaxLayers := FMaxLayers + 1;
    SetLength(FLayer, FMaxLayers + 1);
  end;
  FLayer[FLayerCount] := Layer;
  Layer.Order := FLayerCount;
  Inc(FLayerCount);
end;

procedure TRenderEngine.Render;
var
  i, j: Word;
begin             
  gl.BeginRender;
  for i := 0 to FCameraCount - 1 do
    if FCamera[i] <> nil then
    begin
      FCamera[i].Use;
      for j := 0 to FLayerCount - 1 do
        if FLayer[j] <> nil then
        begin    
          FLayer[j].Render(FCamera[i].FMask);
        end;
    end;
  gl.EndRender;
end;    
{$ENDREGION}

end.
