//
// This unit is part of the GLScene Engine, http://glscene.org
//

unit GLTrail;

(* Creates a trail-like mesh. *)

interface

{$I GLScene.inc}

uses
  System.Classes,
  System.SysUtils,
  
  GLScene,
  GLVectorTypes,
  GLMeshUtils,
  GLVectorGeometry,
  GLVectorFileObjects,
  GLMesh,
  GLObjects,
  GLMaterial,
  GLS.Strings,
  GLBaseClasses;


const cMaxVerts = 2000;

type

  TMarkStyle = (msUp, msDirection, msFaceCamera, msRight);

  TGLTrail = class(TGlMesh)
  private

    fVertLimit: integer;
    fTimeLimit: single;
    fMinDistance: single;
    fAlpha: single;
    fAlphaFade: Boolean;
    fUVScale: Single;
    fVerts: array[1..cMaxVerts] of TVector3f;
    fUVs: array[1..cMaxVerts] of TTexpoint;
    fTimeStamps: array[1..cMaxVerts] of Double;
    fVertStart,fVertEnd,fVertCount: integer;


    fLastV0Pos,fLastPos, fLastDir, fLastUp: TVector3f;
    FLastUVs: single;

    // used for UV scaling
    fLastP1,fLastP2: TVector3f;
    FTrailObject: TGLBaseSceneObject;
    FMarkStyle: TMarkStyle;
    FMarkWidth: single;
    FEnabled: boolean;
    FAntiZFightOffset: Single;
    procedure SetTrailObject(const Value: TGLBaseSceneObject);
    procedure SetMarkStyle(const Value: TMarkStyle);
    procedure SetAlpha(const Value: single);
    procedure SetAlphaFade(const Value: Boolean);
    procedure SetMinDistance(const Value: single);
    procedure SetTimeLimit(const Value: single);
    procedure SetUVScale(const Value: single);
    procedure SetVertLimit(const Value: integer);
    procedure SetMarkWidth(const Value: single);
    procedure SetEnabled(const Value: boolean);
    function StoreAntiZFightOffset: Boolean;
    
  protected
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
  public

    //EnableUVmapping: boolean; // generate UV's or not

    procedure DoProgress(const progressTime : TGLProgressTimes); override;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    procedure CreateMark(obj: TGlBaseSceneObject; width: single;CurrentTime : Double); overload;
    procedure CreateMark(const APos,ADir,AUp: TVector3f; AWidth: single;ACurrentTime : Double); overload;
    function CreateMark(const p1,p2: TVector3f;CurrentTime : Double):boolean; overload;

    procedure ClearMarks;

  published
    {Add a tiny bit of offset to help prevent z-fighting..
       Need a better solution here as this will get out of whack on really
       long trails and is dependant on scene scale. }
     property AntiZFightOffset: Single read FAntiZFightOffset write FAntiZFightOffset stored StoreAntiZFightOffset;

     property VertLimit: integer  read FVertLimit write SetVertLimit default 150;
     property TimeLimit: single  read FTimeLimit write SetTimeLimit;
     {Don't create mark unless moved at least this distance. }
     property MinDistance: single  read FMinDistance write SetMinDistance;
     property Alpha: single  read FAlpha write SetAlpha;
     property AlphaFade: Boolean  read FAlphaFade write SetAlphaFade default True;
     property UVScale: single  read FUVScale write SetUVScale;
     property MarkStyle : TMarkStyle read FMarkStyle write SetMarkStyle default msFaceCamera;
     property TrailObject : TGLBaseSceneObject read FTrailObject write SetTrailObject default nil;
     property MarkWidth : single read FMarkWidth write SetMarkWidth;
     property Enabled : boolean read FEnabled write SetEnabled default True;
  end;


implementation


constructor TGLTrail.Create(AOwner: TComponent);
begin
 inherited Create(AOwner);
 vertices.Clear;    // inherited tglmesh makes a triangle... remove it.
 Mode := mmTriangleStrip;
 FAntiZFightOffset := 0.0000266;
 VertexMode := vmVNCT;
 fVertStart := 1;
 fVertEnd := 0;
 fVertCount := 0;
 fVertLimit := 150;
 fTimeLimit := 0.5;
 fMinDistance := 0.05;
 fAlphaFade := True;
 fAlpha := 1.0;
 fLastUVs := 0;
 fUVScale := 1.0;
 FMarkWidth := 0.5;
 FEnabled := True;
 FMarkStyle := msFaceCamera;
 Material.BlendingMode := bmAdditive;
 Material.FaceCulling := fcNoCull;
end;

destructor TGLTrail.Destroy;
begin
  // notta?
   inherited Destroy;
end;

procedure TGLTrail.DoProgress(const progressTime: TGLProgressTimes);
begin
  inherited;
  if Enabled and Assigned(TrailObject) then
  begin
       CreateMark(TrailObject,MarkWidth,progressTime.NewTime);
  end;
end;

procedure TGLTrail.ClearMarks;
begin
 Vertices.Clear;
 fVertCount := 0;
 fVertEnd := 0;
 fVertStart := 1;
end;

procedure TGLTrail.CreateMark(obj: TGlBaseSceneObject; width: single;CurrentTime : Double);
var
   v0,dv,p1,p2 : TVector3f;
   v: TVector3f;
   c: TGLCamera;
begin
     case MarkStyle of
     msUp:         begin
                         v := AffinevectorMake(obj.AbsoluteUp);
                    end;
     msDirection:  begin
                         v := AffinevectorMake(obj.AbsoluteDirection);

                    end;
     msRight:       begin
                         v := AffinevectorMake(obj.AbsoluteRight);
                    end;
     msFaceCamera: begin
                         c := Scene.CurrentGLCamera;
                         if c<>nil then
                         begin
                              dv := VectorSubtract(fLastV0Pos,AffinevectorMake(obj.AbsolutePosition));
                              v := VectorCrossProduct(AffinevectorMake(VectorSubtract(c.AbsolutePosition,obj.AbsolutePosition)),dv);
                              NormalizeVector(v);

                         end;
                    end;
     else Assert(False, strErrorEx + strUnknownType);
     end;
     v0 := AffinevectorMake(Obj.AbsolutePosition);
     VectorScale(v,width,v);
     p1:=VectorSubtract(v0,v);
     p2:=VectorAdd(v0,v);

     // PREVENT REFLAT
     if not PointIsInHalfSpace(p1,fLastV0Pos,VectorSubtract(v0,fLastV0Pos)) then
        p1 := fLastp1;
     if not PointIsInHalfSpace(p2,fLastV0Pos,VectorSubtract(v0,fLastV0Pos)) then
        p2 := fLastp2;
     

     if CreateMark(p1,p2,CurrentTime) then
     begin
          fLastV0Pos := v0;
     end;
end;

function TGLTrail.CreateMark(const p1,p2: TVector3f; CurrentTime : Double): boolean;
var
   diff: integer;
   uv1,uv2: TTexPoint;
   apoint1,apoint2: TVector3f;
   currentvert: integer;
   i: integer;
   color: tVector4f;
   ramp: single;
   distance: single;
   uvsize: single;
   tinyoffset: TVector3f;
   MustRebuild : Boolean;
begin
    Result := false;
    apoint1 := p1;
    apoint2 := p2;

    // get distance moved, based on average of 2 point movement;
    distance := ( VectorDistance(fLastp1,p1) + VectorDistance(fLastp2,p2) ) / 2;

    if distance = 0 then
    begin
         apoint1 := AffineVectorMake(fLastp1.X,fLastp1.Y,fLastp1.Z);
         apoint2 := AffineVectorMake(fLastp2.X,fLastp2.Y,fLastp2.Z);
    end;

    uvsize :=  distance / fUVScale; // scale UV's
    uv2.S := 0+fLastUVs+uvsize; uv2.T := 0;
    uv1.S := 0+fLastUVs+uvsize; uv1.T := 1;


    // process verts, then send them to .vertices for rendering
    if fVertEnd >= cMaxVerts then
       fVertEnd := 0;


    fVerts[fVertEnd+1] := apoint2;
    fVerts[fVertEnd+2] := apoint1;
    fUVs[fVertEnd+1] := uv2;
    fUVs[fVertEnd+2] := uv1;
    //tstamp := GetTickCount; // win api
    fTimeStamps[fVertEnd+1] := CurrentTime;
    fTimeStamps[fVertEnd+2] := CurrentTime;


    MustRebuild := false;
    if distance >= fMinDistance then
    begin
         inc(fVertCount,2);
         inc(fVertEnd,2);

         // remember stuff
         fLastUVs := fLastUVs + uvsize;
         fLastp1 := p1; fLastp2 := p2;
         MustRebuild := true;
         Result := true;
    end;


    // remove expired verts over VertLimit
    if fVertCount > fVertLimit then
    begin
         diff := fVertCount - fVertLimit;
         inc(fVertStart,diff); // inc start, reducing count to fit in limit - rollover handled later
         dec(fVertCount,diff);
    end;

    // remove time expired verts over TimeLimit
    //currentvert := fVertStart;
    for i := 0 to fVertCount-1 do
    begin
         if (i + fVertStart)  > cMaxVerts then
            currentvert := (i + fVertStart) - cMaxVerts  // rollover
         else
             currentvert := (i + fVertStart);

         if fTimeLimit > 0 then
           if  CurrentTime - fTimeStamps[ currentvert ] > fTimeLimit then
           begin
               inc(fVertStart,1); // inc start, reducing count to fit in limit - rollover handled later
               dec(fVertCount,1);
               MustRebuild := true;
           end;
    end;

    // handle rollover
    if fVertStart > cMaxVerts then fVertStart := 0 + ( fVertStart - cMaxVerts); // adjust if rollover

    if MustRebuild then
    begin
         // give to .vertices, from start to count
         //currentvert := fVertStart;
         ramp := fAlpha / (fVertCount) ;
         color := material.FrontProperties.Diffuse.Color;
         Vertices.Clear;
         for i := 0 to fVertCount-1 do
         begin
              if (i + fVertStart)  > cMaxVerts then
                 currentvert := (i + fVertStart) - cMaxVerts  // rollover
              else
                  currentvert := (i + fVertStart);

              if fAlphaFade then
                 color.W :=  (ramp * i)
              else
                  color.W := fAlpha;
              // add a tiny bit of offset to help prevent z-fighting..
              // need a better solution here
              // as this will get out of whack on really long trails
              // and is dependant on scene scale
              TinyOffset.X := FAntiZFightOffset * i;
              TinyOffset.Y := FAntiZFightOffset * i;
              TinyOffset.Z := FAntiZFightOffset * i;
              TinyOffset :=  VectorAdd( fVerts[ currentvert ],Tinyoffset);
              //TinyOffset := fVerts[ currentvert]; // bypass
              Vertices.AddVertex( TinyOffset, NullVector, Color, fUVs[currentvert]  );
         end;
    end;

end;

procedure TGLTrail.CreateMark(const APos,ADir,AUp: TVector3f; AWidth: single;ACurrentTime : Double);
var
apoint1,apoint2,crossp: TVector3f;
begin

    if fMinDistance > 0 then
        if vectorDistance(APos,fLastPos) < fMinDistance then
           exit;

    fLastPos := APos;
    fLastDir := ADir;
    fLastUp := AUp;

    apoint1 := APos;
    apoint2 := APos;
    crossp :=  vectorcrossproduct(ADir,AUp);

    CombineVector( apoint1,vectornormalize(crossp),AWidth);
    CombineVector( apoint2,vectornormalize(VectorNegate(crossp)),AWidth);

    CreateMark( apoint1, apoint2,ACurrentTime);

end;

// NOTES and stuff:

 { // UV mapped 4x4 square for refrence /debug
  uv.S := 0; uv.T := 0;
  Vertices.AddVertex( AffineVectorMake(1, 1, 1), NullVector, NullHmgVector, UV  );
  uv.S := 0; uv.T := 1;
  Vertices.AddVertex( AffineVectorMake(1, 1, 4), NullVector, NullHmgVector, UV  );
  uv.S := 1; uv.T := 0;
  Vertices.AddVertex( AffineVectorMake(4, 1, 1), NullVector, NullHmgVector, UV  );
  uv.S := 1; uv.T := 1;
  Vertices.AddVertex( AffineVectorMake(4, 1, 4), NullVector, NullHmgVector, UV  );


// Directmode: append .vertices only, no way to process/delete except .clear;
// else we manage vertices/UV in our own arrays then dump them all to .vertices
// I don't know if directmode is that much faster, but could be considerably?

if directmode then
 begin
 if fUVTop then // start a new UV tile
   begin
    uv2.S := 0; uv2.T := 0;
    Vertices.AddVertex( AffineVectorMake(apoint2[0], apoint2[1],apoint2[2]), NullVector, NullHmgVector, UV2  );
    uv1.S := 0; uv1.T := 1;
    Vertices.AddVertex( AffineVectorMake(apoint1[0],apoint1[1],apoint1[2]), NullVector, NullHmgVector, UV1  );
   end
  else // finish a UV tile
   begin
    uv2.S := 1; uv2.T := 0;
    Vertices.AddVertex( AffineVectorMake(apoint2[0], apoint2[1],apoint2[2]), NullVector, NullHmgVector, UV2  );
    uv1.S := 1; uv1.T := 1;
    Vertices.AddVertex( AffineVectorMake(apoint1[0],apoint1[1],apoint1[2]), NullVector, NullHmgVector, UV1  );
   end;
 end
 }


procedure TGLTrail.SetTrailObject(const Value: TGLBaseSceneObject);
begin
  if FTrailObject <> nil then FTrailObject.RemoveFreeNotification(Self);
  FTrailObject := Value;
  if FTrailObject <> nil then FTrailObject.FreeNotification(Self);
end;

procedure TGLTrail.SetMarkStyle(const Value: TMarkStyle);
begin
  FMarkStyle := Value;
end;

procedure TGLTrail.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
   if (Operation = opRemove) and (AComponent = FTrailObject) then
      TrailObject:=nil;
  inherited;
end;

procedure TGLTrail.SetAlpha(const Value: single);
begin
  FAlpha := Value;
end;

procedure TGLTrail.SetAlphaFade(const Value: Boolean);
begin
  FAlphaFade := Value;
end;

procedure TGLTrail.SetMinDistance(const Value: single);
begin
  FMinDistance := Value;
end;

procedure TGLTrail.SetTimeLimit(const Value: single);
begin
  FTimeLimit := Value;
end;

procedure TGLTrail.SetUVScale(const Value: single);
begin
  FUVScale := Value;
end;

procedure TGLTrail.SetVertLimit(const Value: integer);
begin
  FVertLimit := Value;
end;


procedure TGLTrail.SetMarkWidth(const Value: single);
begin
  FMarkWidth := Value;
end;

procedure TGLTrail.SetEnabled(const Value: boolean);
begin
  FEnabled := Value;
end;

function TGLTrail.StoreAntiZFightOffset: Boolean;
begin
  Result := FAntiZFightOffset <> 0.0000266;
end;

// ------------------------------------------------------------------
// ------------------------------------------------------------------
// ------------------------------------------------------------------
initialization
// ------------------------------------------------------------------
// ------------------------------------------------------------------
// ------------------------------------------------------------------

   // class registrations
   RegisterClasses([TGLTrail]);


end.
