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

unit File3DSUtils;

(*
   Utility functions for the universal 3DS file reader and writer (TFile3DS).
   Essentially, the functions here are the heart of the import library as
   they deal actually with the database and chunks.
*)

interface

{$I GLScene.inc}
{$R-}

uses 
  System.Classes,
  System.SysUtils,
  File3DS,
  File3DSTypes,
  GLS.Strings;

// functions to retrieve global settings of a specific 3DS database
function GetAtmosphere(const Source: TFile3DS; var DB: TDatabase3DS): TAtmosphere3DS;
function GetBackground(const Source: TFile3DS; var DB: TDatabase3DS): TBackground3DS;
function GetMeshSet(const Source: TFile3DS; var DB: TDatabase3DS): TMeshSet3DS;
function GetViewport(const Source: TFile3DS; var DB: TDatabase3DS): TViewport3DS;

// functions to retrieve/modify data related to materials, lights and objects (meshs)
procedure AddChild(Parent, Child: PChunk3DS);
procedure AddChildOrdered(Parent, Child: PChunk3DS);
function FindChunk(Top: PChunk3DS; Tag: word): PChunk3DS;
function FindNextChunk(Local: PChunk3DS; Tag: word): PChunk3DS;
procedure FreeChunkData(var Chunk: PChunk3DS);
function GetCameraByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TCamera3DS;
function GetCameraCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;
function GetChunkValue(Tag: word): integer;
function GetMaterialByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TMaterial3DS;
function GetMaterialCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;
function GetMeshByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TMesh3DS;
function GetMeshCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;
function GetOmnilightByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TLight3DS;
function GetSpotlightByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TLight3DS;
function GetOmnilightCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;
function GetSpotlightCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;
procedure InitChunk(var Chunk: PChunk3DS);
procedure ReleaseCamera(Camera: PCamera3DS);
procedure ReleaseChunk(var Chunk: PChunk3DS);
procedure ReleaseChunkList(var List: PChunkList3DS);
procedure ReleaseLight(Light: PLight3DS);
procedure ReleaseMaterial(Mat: PMaterial3DS);
procedure ReleaseMeshObj(Mesh: PMesh3DS);

// functions to retrieve/modify keyframer (animation) data
function GetKFSettings(const Source: TFile3DS; var DB: TDatabase3DS): TKFSets3DS;

procedure ReleaseCameraMotion(Camera: PKFCamera3DS);
procedure GetCameraNodeNameList(const Source: TFile3DS; var DB: TDatabase3DS;
  List: TStringList);
function GetCameraNodeCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;
function GetCameraMotion(const Source: TFile3DS;
  CamChunk, TargetChunk: PChunk3DS): TKFCamera3DS;
function GetCameraMotionByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TKFCamera3DS;

procedure ReleaseAmbientLightMotion(Light: PKFAmbient3DS);
function GetAmbientLightMotion(const Source: TFile3DS;
  var DB: TDatabase3DS): TKFAmbient3DS;

procedure InitObjectMotion(var Obj: TKFMesh3DS;
  NewNPKeys, NewNRKeys, NewNSKeys, NewNMKeys, NewNHKeys: cardinal);
procedure ReleaseObjectMotion(Obj: PKFMesh3DS);
procedure GetObjectNodeNameList(const Source: TFile3DS; var DB: TDatabase3DS;
  List: TStringList);
function GetObjectNodeCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;
function GetObjectMotionByName(const Source: TFile3DS; var DB: TDatabase3DS;
  const Name: string): TKFMesh3DS;
function GetObjectMotionByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: cardinal): TKFMesh3DS;

procedure ReleaseOmnilightMotion(Light: PKFOmni3DS);
procedure GetOmnilightNodeNameList(const Source: TFile3DS; var DB: TDatabase3DS;
  List: TStringList);
function GetOmnilightNodeCount(const Source: TFile3DS; var DB: TDatabase3DS): cardinal;
function GetOmnilightMotionByName(const Source: TFile3DS; var DB: TDatabase3DS;
  const Name: string): TKFOmni3DS;
function GetOmnilightMotionByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: cardinal): TKFOmni3DS;

procedure ReleaseSpotlightMotion(Spot: PKFSpot3DS);
procedure GetSpotlightNodeNameList(const Source: TFile3DS; var DB: TDatabase3DS;
  List: TStringList);
function GetSpotlightNodeCount(const Source: TFile3DS; var DB: TDatabase3DS): cardinal;
function GetSpotlightMotionByName(const Source: TFile3DS; var DB: TDatabase3DS;
  const Name: string): TKFSpot3DS;
function GetSpotlightMotionByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: cardinal): TKFSpot3DS;

// version information
function GetM3dMagicRelease(const Source: TFile3DS; var DB: TDatabase3DS): TReleaseLevel;
function GetMeshRelease(const Source: TFile3DS; var DB: TDatabase3DS): TReleaseLevel;
function GetKfRelease(const Source: TFile3DS; var DB: TDatabase3DS): TReleaseLevel;
function GetDatabaseRelease(const Source: TFile3DS; var DB: TDatabase3DS): TReleaseLevel;

// support functions for text output of chunk and database contents
procedure ChunkHeaderReport(var Strings: TStrings; Chunk: PChunk3DS;
  IndentLevel: integer);
function ChunkTagToString(Tag: word): string;
procedure DumpChunk(const Source: TFile3DS; var Strings: TStrings;
  Chunk: PChunk3DS; IndentLevel: integer; DumpLevel: TDumpLevel);
procedure DumpKeyHeader(Strings: TStrings; const Key: TKeyHeader3DS; IndentLevel: integer);

// support functions for chunk handling
procedure DeleteChunk(var Chunk: PChunk3DS);
function FindNamedObjectByIndex(Source: TFile3DS; DB: TDatabase3DS;
  AType: word; Index: integer): PChunk3DS;

// error message routines
procedure ShowError(const ErrorMessage: string);
procedure ShowErrorFormatted(const ErrorMessage: string; const Args: array of const);

//-------------------------------------------------
implementation
//-------------------------------------------------

uses 
  File3DSConst;

type
  E3DSError = class(Exception);

//----------------- error handling ------------------------------------------------------------------------------------

procedure ShowError(const ErrorMessage: string);
begin
  raise E3DSError.Create(ErrorMessage);
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ShowErrorFormatted(const ErrorMessage: string; const Args: array of const);
begin
  raise E3DSError.CreateFmt(ErrorMessage, Args);
end;

//----------------- global settings functions -------------------------------------------------------------------------

function InitMeshSet: TMeshSet3DS;

  // initializes a mesh settings structure

begin
  FillChar(Result, SizeOf(Result), 0);
  with Result do
  begin
    MasterScale := 1;
    Shadow.Bias := 1;
    Shadow.RayBias := 1;
    Shadow.MapSize := 512;
    Shadow.Filter := 3;
    AmbientLight.R := 0.39216;
    AmbientLight.G := 0.39216;
    AmbientLight.B := 0.39216;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetMeshSet(const Source: TFile3DS; var DB: TDatabase3DS): TMeshSet3DS;

  // retrieves the mesh settings from the database

var
  MDataChunk, ColorChunk, Chunk: PChunk3DS;

begin
  FillChar(Result, SizeOf(Result), 0);

  // find the mesh data chunk
  MDataChunk := FindChunk(DB.TopChunk, MDATA);

  // If the mesh data section is found
  if Assigned(MDataChunk) then
  begin
    Result := InitMeshSet;
    with Result do
    begin
      // Search for a master_scale chunk
      Chunk := FindNextChunk(MDataChunk^.Children, MASTER_SCALE);
      if Assigned(Chunk) then
      begin
        Source.ReadChunkData(Chunk);
        MasterScale := Chunk^.Data.MasterScale^;
        FreeChunkData(Chunk);
      end;

      // search for Lo_Shadow_Bias chunk
      Chunk := FindNextChunk(MDataChunk^.Children, LO_SHADOW_BIAS);
      if Assigned(Chunk) then
      begin
        Source.ReadChunkData(Chunk);
        Shadow.Bias := Chunk^.Data.LoShadowBias^;
        FreeChunkData(Chunk);
      end;

      // Search for ray_Bias Chunk
      Chunk := FindNextChunk(MDataChunk^.Children, RAY_BIAS);
      if Assigned(Chunk) then
      begin
        Source.ReadChunkData(Chunk);
        Shadow.RayBias := Chunk^.Data.RayBias^;
        FreeChunkData(Chunk);
      end;

      // search for MapSize Chunk
      Chunk := FindNextChunk(MDataChunk^.Children, SHADOW_MAP_SIZE);
      if Assigned(Chunk) then
      begin
        Source.ReadChunkData(Chunk);
        Shadow.MapSize := Chunk^.Data.ShadowMapSize^;
        FreeChunkData(Chunk);
      end;

      // search for Shadow_Filter Chunk
      Chunk := FindNextChunk(MDataChunk^.Children, SHADOW_FILTER);
      if Assigned(Chunk) then
      begin
        Source.ReadChunkData(Chunk);
        Shadow.Filter := Chunk^.Data.ShadowFilter^;
        FreeChunkData(Chunk);
      end;

      // search for ambient_light Chunk
      Chunk := FindNextChunk(MDataChunk^.Children, AMBIENT_LIGHT);
      if Assigned(Chunk) then
      begin
        // search for the old style Color chunk inside the ambient Light Chunk
        ColorChunk := FindChunk(Chunk, COLOR_F);
        if Assigned(ColorChunk) then
        begin
          Source.ReadChunkData(ColorChunk);
          AmbientLight.R := ColorChunk^.Data.ColorF^.Red;
          AmbientLight.G := ColorChunk^.Data.ColorF^.Green;
          AmbientLight.B := ColorChunk^.Data.ColorF^.Blue;
          FreeChunkData(ColorChunk);
        end
        else
        begin
          // just for robust completeness, search for the COLOR_24 chunk
          ColorChunk := FindChunk(Chunk, COLOR_24);
          if Assigned(ColorChunk) then
          begin
            Source.ReadChunkData(ColorChunk);
            AmbientLight.R := ColorChunk^.Data.Color24^.Red / 255;
            AmbientLight.G := ColorChunk^.Data.Color24^.Green / 255;
            AmbientLight.B := ColorChunk^.Data.Color24^.Blue / 255;
            FreeChunkData(ColorChunk);
          end;
        end;

        // search for the newer linear Color Chunk inside the ambient Light chunk
        ColorChunk := FindChunk(Chunk, LIN_COLOR_F);
        if Assigned(ColorChunk) then
        begin
          Source.ReadChunkData(ColorChunk);
          AmbientLight.R := ColorChunk^.Data.LinColorF^.Red;
          AmbientLight.G := ColorChunk^.Data.LinColorF^.Green;
          AmbientLight.B := ColorChunk^.Data.LinColorF^.Blue;
          FreeChunkData(ColorChunk);
        end
        else
        begin
          // just for completeness, search for the LIN_COLOR_24 chunk
          ColorChunk := FindChunk(Chunk, LIN_COLOR_24);
          if Assigned(ColorChunk) then
          begin
            Source.ReadChunkData(ColorChunk);
            AmbientLight.R := ColorChunk^.Data.LinColorF^.Red / 255;
            AmbientLight.G := ColorChunk^.Data.LinColorF^.Green / 255;
            AmbientLight.B := ColorChunk^.Data.LinColorF^.Blue / 255;
            FreeChunkData(ColorChunk);
          end;
        end;
      end;

      // Search for the oconst chunk
      Chunk := FindNextChunk(MDataChunk^.Children, O_CONSTS);
      if Assigned(Chunk) then
      begin
        Source.ReadChunkData(Chunk);
        oconsts.x := Chunk^.Data.OConsts^.X;
        oconsts.y := Chunk^.Data.OConsts^.Y;
        oconsts.z := Chunk^.Data.OConsts^.Z;
        FreeChunkData(Chunk);
      end;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function InitAtmosphere: TAtmosphere3DS;

  // initializes a atmosphere structure

begin
  FillChar(Result, SizeOf(Result), 0);
  with Result do
  begin
    Fog.FarPlane := 1000;
    Fog.FarDensity := 100;
    Fog.FogBgnd := True;

    LayerFog.ZMax := 100;
    LayerFog.Density := 50;
    LayerFog.Falloff := lfNoFall;
    LayerFog.Fogbgnd := True;

    DCue.FarPlane := 1000;
    DCue.FarDim := 100;

    ActiveAtmo := atNoAtmo;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetAtmosphere(const Source: TFile3DS; var DB: TDatabase3DS): TAtmosphere3DS;

  // retrieves the atmospheric settings from database

var
  MDataChunk, FogChunk, BgnChunk, ColorChunk, Chunk: PChunk3DS;

begin
  FillChar(Result, SizeOf(Result), 0);

  // find the MDATA chunk
  MDataChunk := FindChunk(DB.TopChunk, MDATA);

  // if the MDATA chunk was found, then search for the atmospheric chunks
  if Assigned(MDataChunk) then
  begin
    Result := InitAtmosphere;
    // Search for fog chunk
    FogChunk := FindChunk(MDataChunk, FOG);
    if Assigned(FogChunk) then
      with Result do
      begin
        // read the chunk information
        Source.ReadChunkData(FogChunk);

        // Copy the FogChunk data into the structure
        Fog.NearPlane := FogChunk^.Data.Fog^.NearPlaneDist;
        Fog.NearDensity := FogChunk^.Data.Fog^.NearPlaneDensity;
        Fog.FarPlane := FogChunk^.Data.Fog^.FarPlanedist;
        Fog.FarDensity := FogChunk^.Data.Fog^.FarPlaneDensity;

        // Search for fog Color chunk
        ColorChunk := FindChunk(FogChunk, COLOR_F);
        if Assigned(ColorChunk) then
        begin
          Source.ReadChunkData(ColorChunk);
          Fog.FogColor.R := ColorChunk^.Data.ColorF^.Red;
          Fog.Fogcolor.G := ColorChunk^.Data.ColorF^.Green;
          Fog.Fogcolor.B := ColorChunk^.Data.ColorF^.Blue;
          FreeChunkData(ColorChunk);
        end;

        // Search for FOG_BGND chunk
        BgnChunk := FindChunk(FogChunk, FOG_BGND);
        if Assigned(BgnChunk) then
          Fog.FogBgnd := True
        else
          Fog.FogBgnd := False;
        FreeChunkData(FogChunk);

        // search for LAYER_FOG chunk
        FogChunk := FindChunk(MDataChunk, LAYER_FOG);
        if Assigned(FogChunk) then
        begin
          Source.ReadChunkData(FogChunk);

          LayerFog.ZMin := FogChunk^.Data.LayerFog^.ZMin;
          LayerFog.ZMax := FogChunk^.Data.LayerFog^.ZMax;
          LayerFog.Density := FogChunk^.Data.LayerFog^.Density;

          if (FogChunk^.Data.LayerFog^.AType and LayerFogBgnd) <> 0 then
            LayerFog.FogBgnd := True
          else
            LayerFog.FogBgnd := False;

          if (FogChunk^.Data.LayerFog^.AType and TopFalloff) <> 0 then
            LayerFog.Falloff := lfTopFall
          else if (FogChunk^.Data.LayerFog^.AType and BottomFalloff) <> 0 then
            LayerFog.Falloff := lfBottomFall
          else
            LayerFog.Falloff := lfNoFall;

          ColorChunk := FindChunk(FogChunk, COLOR_F);
          if Assigned(ColorChunk) then
          begin
            Source.ReadChunkData(ColorChunk);
            LayerFog.FogColor.R := ColorChunk^.Data.ColorF^.Red;
            LayerFog.Fogcolor.G := ColorChunk^.Data.ColorF^.Green;
            LayerFog.Fogcolor.B := ColorChunk^.Data.ColorF^.Blue;
            FreeChunkData(ColorChunk);
          end;
          FreeChunkData(FogChunk);
        end;

        // search for DISTANCE_CUE chunk
        Chunk := FindChunk(MDataChunk, DISTANCE_CUE);
        if Assigned(Chunk) then
        begin
          Source.ReadChunkData(Chunk);

          DCue.NearPlane := Chunk^.Data.DistanceCue^.NearPlaneDist;
          DCue.neardim := Chunk^.Data.DistanceCue^.NearPlaneDimming;
          DCue.FarPlane := Chunk^.Data.DistanceCue^.FarPlaneDist;
          DCue.FarDim := Chunk^.Data.DistanceCue^.FarPlaneDimming;

          BgnChunk := FindChunk(Chunk, DCUE_BGND);
          if Assigned(BgnChunk) then
            DCue.DCueBgnd := True
          else
            DCue.DCueBgnd := False;
          FreeChunkData(Chunk);
        end;

        // search for USE_FOG, USE_LAYER_FOG or USE_DISTANCE_CUE chunk
        Chunk := FindChunk(MDataChunk, USE_FOG);
        if Assigned(Chunk) then
          ActiveAtmo := atUseFog
        else
        begin
          Chunk := FindChunk(MDataChunk, USE_LAYER_FOG);
          if Assigned(Chunk) then
            ActiveAtmo := atUseLayerFog
          else
          begin
            Chunk := FindChunk(MDataChunk, USE_DISTANCE_CUE);
            if Assigned(Chunk) then
              ActiveAtmo := atUseDistanceCue
            else
              ActiveAtmo := atNoAtmo;
          end;
        end;
      end; // with Result do
  end; // if Assigned(MDataChunk)
end;

//---------------------------------------------------------------------------------------------------------------------

function InitBackground: TBackground3DS;

  // initializes the TBackground3DS structure

begin
  FillChar(Result, SizeOf(Result), 0);
  Result.VGradient.GradPercent := 0.5;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetBackground(const Source: TFile3DS; var DB: TDatabase3DS): TBackground3DS;

  // retrieves the background settings from the database

var
  MDataChunk, ColorChunk, TopColor, MidColor, BotColor, Chunk: PChunk3DS;

begin
  FillChar(Result, SizeOf(Result), 0);

  // Find the MDATA chunk
  MDataChunk := FindChunk(DB.TopChunk, MDATA);

  // only continue with structure filling if an MDATA chunk is found
  if Assigned(MDataChunk) then
    with Result do
    begin
      Result := InitBackground;
      // search for bitmap chunk
      Chunk := FindChunk(MDataChunk, BIT_MAP);
      if Assigned(Chunk) then
      begin
        // read the chunk information
        Source.ReadChunkData(Chunk);
        // copy the bitmap filename to the structure
        if Assigned(Chunk^.Data.BitmapName) then
          Bitmap := Chunk^.Data.BitmapName^
        else
          Bitmap := '';
        FreeChunkData(Chunk);
      end;

      Chunk := FindChunk(MDataChunk, SOLID_BGND);
      if Assigned(Chunk) then
      begin
        ColorChunk := FindChunk(Chunk, COLOR_F);
        if Assigned(ColorChunk) then
        begin
          Source.ReadChunkData(ColorChunk);
          Solid.R := ColorChunk^.Data.ColorF^.Red;
          Solid.G := ColorChunk^.Data.ColorF^.Green;
          Solid.B := ColorChunk^.Data.ColorF^.Blue;
          FreeChunkData(ColorChunk);
        end;

        ColorChunk := FindChunk(Chunk, LIN_COLOR_F);
        if Assigned(ColorChunk) then
        begin
          Source.ReadChunkData(ColorChunk);
          Solid.R := ColorChunk^.Data.ColorF^.Red;
          Solid.G := ColorChunk^.Data.ColorF^.Green;
          Solid.B := ColorChunk^.Data.ColorF^.Blue;
          FreeChunkData(ColorChunk);
        end;
      end;

      Chunk := FindChunk(MDataChunk, V_GRADIENT);
      if Assigned(Chunk) then
      begin
        // the COLOR_F chunks are the old, non-gamma corrected colors
        Source.ReadChunkData(Chunk);
        VGradient.GradPercent := Chunk^.Data.VGradient^;
        TopColor := FindChunk(Chunk, COLOR_F);
        if Assigned(TopColor) then
        begin
          Source.ReadChunkData(TopColor);
          VGradient.Top.R := TopColor^.Data.ColorF^.Red;
          VGradient.Top.G := TopColor^.Data.ColorF^.Green;
          VGradient.Top.B := TopColor^.Data.ColorF^.Blue;
          MidColor := FindNextChunk(TopColor^.Sibling, COLOR_F);
          if Assigned(MidColor) then
          begin
            Source.ReadChunkData(MidColor);
            VGradient.Mid.R := MidColor^.Data.ColorF^.Red;
            VGradient.Mid.G := MidColor^.Data.ColorF^.Green;
            VGradient.Mid.B := MidColor^.Data.ColorF^.Blue;
            BotColor := FindNextChunk(MidColor^.Sibling, COLOR_F);
            if Assigned(BotColor) then
            begin
              Source.ReadChunkData(BotColor);
              VGradient.Bottom.R := MidColor^.Data.ColorF^.Red;
              VGradient.Bottom.G := MidColor^.Data.ColorF^.Green;
              VGradient.Bottom.B := MidColor^.Data.ColorF^.Blue;
              FreeChunkData(BotColor);
            end;
            FreeChunkData(MidColor);
          end;
          FreeChunkData(TopColor);
        end;

        // If the newer, gamma correct colors are available, then use them instead
        TopColor := FindChunk(Chunk, LIN_COLOR_F);
        if Assigned(TopColor) then
        begin
          Source.ReadChunkData(TopColor);
          VGradient.Top.R := TopColor^.Data.ColorF^.Red;
          VGradient.Top.G := TopColor^.Data.ColorF^.Green;
          VGradient.Top.B := TopColor^.Data.ColorF^.Blue;
          MidColor := FindNextChunk(TopColor^.Sibling, LIN_COLOR_F);
          if Assigned(MidColor) then
          begin
            Source.ReadChunkData(MidColor);
            VGradient.Mid.R := MidColor^.Data.ColorF^.Red;
            VGradient.Mid.G := MidColor^.Data.ColorF^.Green;
            VGradient.Mid.B := MidColor^.Data.ColorF^.Blue;
            BotColor := FindNextChunk(MidColor^.Sibling, LIN_COLOR_F);
            if Assigned(BotColor) then
            begin
              Source.ReadChunkData(BotColor);
              VGradient.Bottom.R := MidColor^.Data.ColorF^.Red;
              VGradient.Bottom.G := MidColor^.Data.ColorF^.Green;
              VGradient.Bottom.B := MidColor^.Data.ColorF^.Blue;
              FreeChunkData(BotColor);
            end;
            FreeChunkData(MidColor);
          end;
          FreeChunkData(TopColor);
        end;
        FreeChunkData(Chunk);
      end;

      // Search for use_bitmap, use_solid_bgnd and use_v_gradient chunks
      Chunk := FindChunk(MDataChunk, USE_BIT_MAP);
      if Assigned(Chunk) then
        BgndUsed := btUseBitmapBgnd
      else
      begin
        Chunk := FindChunk(MDataChunk, USE_SOLID_BGND);
        if Assigned(Chunk) then
          BgndUsed := btUseSolidBgnd
        else
        begin
          Chunk := FindChunk(MDataChunk, USE_V_GRADIENT);
          if Assigned(Chunk) then
            BgndUsed := btUseVGradientBgnd
          else
            BgndUsed := btNoBgnd;
        end;
      end;
    end;
end;

//---------------------------------------------------------------------------------------------------------------------

function InitViewport: TViewport3DS;

begin
  FillChar(Result, SizeOf(Result), 0);
  with Result do
  begin
    AType := vtTopView3DS;
    Ortho.Zoom := 0.7395;
    User.Zoom := 0.7395;
    User.HorAng := 20;
    User.VerAng := 30;
    CameraStr := '';
    Size.Width := 1000;
    Size.Height := 1000;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetViewportEntry(Source: TFile3DS; Section: PChunk3DS): TViewport3DS;

var
  Chunk, VLayout: PChunk3DS;
  PortIndex: integer;
  foundV3: boolean;

begin
  Result := InitViewport;
  VLayout := FindNextChunk(Section^.Children, VIEWPORT_LAYOUT);

  if Assigned(VLayout) then
    with Result do
    begin
      Source.ReadChunkData(VLayout);

      Chunk := VLayout^.Children;
      foundV3 := False;
      PortIndex := 0;
      while Assigned(Chunk) do
      begin
        case Chunk^.Tag of
          VIEWPORT_SIZE:
          begin
            Source.ReadChunkData(Chunk);
            Size.XPos := Chunk^.Data.ViewportSize^.XPos;
            Size.YPos := Chunk^.Data.ViewportSize^.YPos;
            Size.Width := Chunk^.Data.ViewportSize^.Width;
            Size.Height := Chunk^.Data.ViewportSize^.Height;
            FreeChunkData(Chunk);
          end;
          VIEWPORT_DATA_3:
          begin
            foundV3 := True;
            if PortIndex = VLayout^.Data.ViewportLayout^.Top then
            begin
              Source.ReadChunkData(Chunk);
              case Chunk^.Data.ViewportData^.View of
                1:
                  AType := vtTopView3DS;
                2:
                  AType := vtBottomView3DS;
                3:
                  AType := vtLeftView3DS;
                4:
                  AType := vtRightView3DS;
                5:
                  AType := vtFrontView3DS;
                6:
                  AType := vtBackView3DS;
                7:
                  AType := vtUserView3DS;
                18:
                  AType := vtSpotlightView3DS;
                $FFFF:
                  AType := vtCameraView3DS;
                else
                  AType := vtNoView3DS;
              end;

              Ortho.Zoom := Chunk^.Data.ViewportData^.ZoomFactor;
              User.Zoom := Chunk^.Data.ViewportData^.ZoomFactor;
              Ortho.Center.X := Chunk^.Data.ViewportData^.Center.X;
              User.Center.X := Chunk^.Data.ViewportData^.Center.X;
              Ortho.Center.Y := Chunk^.Data.ViewportData^.Center.Y;
              User.Center.y := Chunk^.Data.ViewportData^.Center.Y;
              Ortho.Center.Z := Chunk^.Data.ViewportData^.Center.Z;
              User.Center.z := Chunk^.Data.ViewportData^.Center.Z;
              User.HorAng := Chunk^.Data.ViewportData^.HorizAng;
              User.VerAng := Chunk^.Data.ViewportData^.VertAng;
              CameraStr := string(Chunk^.Data.ViewportData^.CamNameStr);
            end;
            Inc(PortIndex);
          end;
          VIEWPORT_DATA:
            if not foundV3 then
            begin
              if PortIndex = VLayout^.Data.ViewportLayout^.Top then
              begin
                Source.ReadChunkData(Chunk);
                case Chunk^.Data.ViewportData^.View of
                  1:
                    AType := vtTopView3DS;
                  2:
                    AType := vtBottomView3DS;
                  3:
                    AType := vtLeftView3DS;
                  4:
                    AType := vtRightView3DS;
                  5:
                    AType := vtFrontView3DS;
                  6:
                    AType := vtBackView3DS;
                  7:
                    AType := vtUserView3DS;
                  18:
                    AType := vtSpotlightView3DS;
                  $FFFF:
                    AType := vtCameraView3DS;
                  else
                    AType := vtNoView3DS;
                end;

                Ortho.Zoom := Chunk^.Data.ViewportData^.ZoomFactor;
                User.Zoom := Chunk^.Data.ViewportData^.ZoomFactor;
                Ortho.Center.X := Chunk^.Data.ViewportData^.Center.X;
                User.Center.X := Chunk^.Data.ViewportData^.Center.X;
                Ortho.Center.Y := Chunk^.Data.ViewportData^.Center.Y;
                User.Center.y := Chunk^.Data.ViewportData^.Center.Y;
                Ortho.Center.Z := Chunk^.Data.ViewportData^.Center.Z;
                User.Center.z := Chunk^.Data.ViewportData^.Center.Z;
                User.HorAng := Chunk^.Data.ViewportData^.HorizAng;
                User.VerAng := Chunk^.Data.ViewportData^.VertAng;
                CameraStr := string(Chunk^.Data.ViewportData^.CamNameStr);
              end;
              Inc(PortIndex);
            end;
        end;
        Chunk := Chunk^.Sibling;
      end;
    end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetViewport(const Source: TFile3DS; var DB: TDatabase3DS): TViewport3DS;

var
  Data: PChunk3DS;

begin
  FillChar(Result, SizeOf(Result), 0);
  if (DB.TopChunk^.Tag = M3DMAGIC) or (DB.TopChunk^.Tag = CMAGIC) then
  begin
    Data := FindNextChunk(DB.TopChunk^.Children, KFDATA);
    if Assigned(Data) then
      Result := GetViewportEntry(Source, Data)
    else
    begin
      Data := FindChunk(DB.TopChunk^.Children, MDATA);
      if Assigned(Data) then
        Result := GetViewportEntry(Source, Data);
    end;
  end;
end;

//----------------- helper funcs for text output ----------------------------------------------------------------------

function ChunkTagToString(Tag: word): string;

begin
  case Tag of
    NULL_CHUNK: Result := 'NULL_CHUNK';
    ChunkType: Result := 'ChunkType';
    ChunkUnique: Result := 'ChunkUnique';
    NotChunk: Result := 'NotChunk';
    Container: Result := 'Container';
    IsChunk: Result := 'IsChunk';

    // Dummy Chunk that sometimes appears in 3DS files created by prerelease 3D Studio R2
    DUMMY: Result := 'DUMMY';

    // Trick Chunk Types
    POINT_ARRAY_ENTRY: Result := 'POINT_ARRAY_ENTRY';
    POINT_FLAG_ARRAY_ENTRY: Result := 'POINT_FLAG_ARRAY_ENTRY';
    FACE_ARRAY_ENTRY: Result := 'FACE_ARRAY_ENTRY';
    MSH_MAT_GROUP_ENTRY: Result := 'MSH_MAT_GROUP_ENTRY';
    TEX_VERTS_ENTRY: Result := 'TEX_VERTS_ENTRY';
    SMOOTH_GROUP_ENTRY: Result := 'SMOOTH_GROUP_ENTRY';
    POS_TRACK_TAG_KEY: Result := 'POS_TRACK_TAG_KEY';
    ROT_TRACK_TAG_KEY: Result := 'ROT_TRACK_TAG_KEY';
    SCL_TRACK_TAG_KEY: Result := 'SCL_TRACK_TAG_KEY';
    FOV_TRACK_TAG_KEY: Result := 'FOV_TRACK_TAG_KEY';
    ROLL_TRACK_TAG_KEY: Result := 'ROLL_TRACK_TAG_KEY';
    COL_TRACK_TAG_KEY: Result := 'COL_TRACK_TAG_KEY';
    MORPH_TRACK_TAG_KEY: Result := 'MORPH_TRACK_TAG_KEY';
    HOT_TRACK_TAG_KEY: Result := 'HOT_TRACK_TAG_KEY';
    FALL_TRACK_TAG_KEY: Result := 'FALL_TRACK_TAG_KEY';

    // 3DS File Chunk IDs
    M3DMAGIC: Result := 'M3DMAGIC';
    SMAGIC: Result := 'SMAGIC';
    LMAGIC: Result := 'LMAGIC';
    MLIBMAGIC: Result := 'MLIBMAGIC';
    MATMAGIC: Result := 'MATMAGIC';
    M3D_VERSION: Result := 'M3D_VERSION';
    M3D_KFVERSION: Result := 'M3D_KFVERSION';

    // Mesh Chunk Ids
    MDATA: Result := 'MDATA';
    MESH_VERSION: Result := 'MESH_VERSION';
    COLOR_F: Result := 'COLOR_F';
    COLOR_24: Result := 'COLOR_24';
    LIN_COLOR_24: Result := 'LIN_COLOR_24';
    LIN_COLOR_F: Result := 'LIN_COLOR_F';
    INT_PERCENTAGE: Result := 'INT_PERCENTAGE';
    FLOAT_PERCENTAGE: Result := 'FLOAT_PERCENTAGE';

    MASTER_SCALE: Result := 'MASTER_SCALE';

    BIT_MAP: Result := 'BIT_MAP';
    USE_BIT_MAP: Result := 'USE_BIT_MAP';
    SOLID_BGND: Result := 'SOLID_BGND';
    USE_SOLID_BGND: Result := 'USE_SOLID_BGND';
    V_GRADIENT: Result := 'V_GRADIENT';
    USE_V_GRADIENT: Result := 'USE_V_GRADIENT';

    LO_SHADOW_BIAS: Result := 'LO_SHADOW_BIAS';
    HI_SHADOW_BIAS: Result := 'HI_SHADOW_BIAS';
    SHADOW_MAP_SIZE: Result := 'SHADOW_MAP_SIZE';
    SHADOW_SAMPLES: Result := 'SHADOW_SAMPLES';
    SHADOW_RANGE: Result := 'SHADOW_RANGE';
    SHADOW_FILTER: Result := 'SHADOW_FILTER';
    RAY_BIAS: Result := 'RAY_BIAS';

    O_CONSTS: Result := 'O_CONSTS';

    AMBIENT_LIGHT: Result := 'AMBIENT_LIGHT';

    FOG: Result := 'FOG';
    USE_FOG: Result := 'USE_FOG';
    FOG_BGND: Result := 'FOG_BGND';
    DISTANCE_CUE: Result := 'DISTANCE_CUE';
    USE_DISTANCE_CUE: Result := 'USE_DISTANCE_CUE';
    LAYER_FOG: Result := 'LAYER_FOG';
    USE_LAYER_FOG: Result := 'USE_LAYER_FOG';
    DCUE_BGND: Result := 'DCUE_BGND';

    DEFAULT_VIEW: Result := 'DEFAULT_VIEW';
    VIEW_TOP: Result := 'VIEW_TOP';
    VIEW_BOTTOM: Result := 'VIEW_BOTTOM';
    VIEW_LEFT: Result := 'VIEW_LEFT';
    VIEW_RIGHT: Result := 'VIEW_RIGHT';
    VIEW_FRONT: Result := 'VIEW_FRONT';
    VIEW_BACK: Result := 'VIEW_BACK';
    VIEW_USER: Result := 'VIEW_USER';
    VIEW_CAMERA: Result := 'VIEW_CAMERA';
    VIEW_WINDOW: Result := 'VIEW_WINDOW';

    NAMED_OBJECT: Result := 'NAMED_OBJECT';
    OBJ_HIDDEN: Result := 'OBJ_HIDDEN';
    OBJ_VIS_LOFTER: Result := 'OBJ_VIS_LOFTER';
    OBJ_DOESNT_CAST: Result := 'OBJ_DOESNT_CAST';
    OBJ_MATTE: Result := 'OBJ_MATTE';
    OBJ_FAST: Result := 'OBJ_FAST';
    OBJ_PROCEDURAL: Result := 'OBJ_PROCEDURAL';
    OBJ_FROZEN: Result := 'OBJ_FROZEN';
    OBJ_DONT_RCVSHADOW: Result := 'OBJ_DONT_RCVSHADOW';

    N_TRI_OBJECT: Result := 'N_TRI_OBJECT';

    POINT_ARRAY: Result := 'POINT_ARRAY';
    POINT_FLAG_ARRAY: Result := 'POINT_FLAG_ARRAY';
    FACE_ARRAY: Result := 'FACE_ARRAY';
    MSH_MAT_GROUP: Result := 'MSH_MAT_GROUP';
    OLD_MAT_GROUP: Result := 'OLD_MAT_GROUP';
    TEX_VERTS: Result := 'TEX_VERTS';
    SMOOTH_GROUP: Result := 'SMOOTH_GROUP';
    MESH_MATRIX: Result := 'MESH_MATRIX';
    MESH_COLOR: Result := 'MESH_COLOR';
    MESH_TEXTURE_INFO: Result := 'MESH_TEXTURE_INFO';
    PROC_NAME: Result := 'PROC_NAME';
    PROC_DATA: Result := 'PROC_DATA';
    MSH_BOXMAP: Result := 'MSH_BOXMAP';

    N_D_L_OLD: Result := 'N_D_L_OLD';

    N_CAM_OLD: Result := 'N_CAM_OLD';

    N_DIRECT_LIGHT: Result := 'N_DIRECT_LIGHT';
    DL_SPOTLIGHT: Result := 'DL_SPOTLIGHT';
    DL_OFF: Result := 'DL_OFF';
    DL_ATTENUATE: Result := 'DL_ATTENUATE';
    DL_RAYSHAD: Result := 'DL_RAYSHAD';
    DL_SHADOWED: Result := 'DL_SHADOWED';
    DL_LOCAL_SHADOW: Result := 'DL_LOCAL_SHADOW';
    DL_LOCAL_SHADOW2: Result := 'DL_LOCAL_SHADOW2';
    DL_SEE_CONE: Result := 'DL_SEE_CONE';
    DL_SPOT_RECTANGULAR: Result := 'DL_SPOT_RECTANGULAR';
    DL_SPOT_OVERSHOOT: Result := 'DL_SPOT_OVERSHOOT';
    DL_SPOT_PROJECTOR: Result := 'DL_SPOT_PROJECTOR';
    DL_EXCLUDE: Result := 'DL_EXCLUDE';
    DL_RANGE: Result := 'DL_RANGE';
    DL_SPOT_ROLL: Result := 'DL_SPOT_ROLL';
    DL_SPOT_ASPECT: Result := 'DL_SPOT_ASPECT';
    DL_RAY_BIAS: Result := 'DL_RAY_BIAS';
    DL_INNER_RANGE: Result := 'DL_INNER_RANGE';
    DL_OUTER_RANGE: Result := 'DL_OUTER_RANGE';
    DL_MULTIPLIER: Result := 'DL_MULTIPLIER';

    N_AMBIENT_LIGHT: Result := 'N_AMBIENT_LIGHT';

    N_CAMERA: Result := 'N_CAMERA';
    CAM_SEE_CONE: Result := 'CAM_SEE_CONE';
    CAM_RANGES: Result := 'CAM_RANGES';

    HIERARCHY: Result := 'HIERARCHY';
    PARENT_OBJECT: Result := 'PARENT_OBJECT';
    PIVOT_OBJECT: Result := 'PIVOT_OBJECT';
    PIVOT_LIMITS: Result := 'PIVOT_LIMITS';
    PIVOT_ORDER: Result := 'PIVOT_ORDER';
    XLATE_RANGE: Result := 'XLATE_RANGE';

    POLY_2D: Result := 'POLY_2D';

    // Flags in shaper file that tell whether polys make up an ok shape
    SHAPE_OK: Result := 'SHAPE_OK';
    SHAPE_NOT_OK: Result := 'SHAPE_NOT_OK';

    SHAPE_HOOK: Result := 'SHAPE_HOOK';

    PATH_3D: Result := 'PATH_3D';
    PATH_MATRIX: Result := 'PATH_MATRIX';
    SHAPE_2D: Result := 'SHAPE_2D';
    M_SCALE: Result := 'M_SCALE';
    M_TWIST: Result := 'M_TWIST';
    M_TEETER: Result := 'M_TEETER';
    M_FIT: Result := 'M_FIT';
    M_BEVEL: Result := 'M_BEVEL';
    XZ_CURVE: Result := 'XZ_CURVE';
    YZ_CURVE: Result := 'YZ_CURVE';
    INTERPCT: Result := 'INTERPCT';
    DEFORM_LIMIT: Result := 'DEFORM_LIMIT';

    // Flags for Modeler options
    USE_CONTOUR: Result := 'USE_CONTOUR';
    USE_TWEEN: Result := 'USE_TWEEN';
    USE_SCALE: Result := 'USE_SCALE';
    USE_TWIST: Result := 'USE_TWIST';
    USE_TEETER: Result := 'USE_TEETER';
    USE_FIT: Result := 'USE_FIT';
    USE_BEVEL: Result := 'USE_BEVEL';

    // Viewport description chunks
    VIEWPORT_LAYOUT_OLD: Result := 'VIEWPORT_LAYOUT_OLD';
    VIEWPORT_DATA_OLD: Result := 'VIEWPORT_DATA_OLD';
    VIEWPORT_LAYOUT: Result := 'VIEWPORT_LAYOUT';
    VIEWPORT_DATA: Result := 'VIEWPORT_DATA';
    VIEWPORT_DATA_3: Result := 'VIEWPORT_DATA_3';
    VIEWPORT_SIZE: Result := 'VIEWPORT_SIZE';
    NETWORK_VIEW: Result := 'NETWORK_VIEW';

    // External Application Data
    XDATA_SECTION: Result := 'XDATA_SECTION';
    XDATA_ENTRY: Result := 'XDATA_ENTRY';
    XDATA_APPNAME: Result := 'XDATA_APPNAME';
    XDATA_STRING: Result := 'XDATA_STRING';
    XDATA_FLOAT: Result := 'XDATA_FLOAT';
    XDATA_DOUBLE: Result := 'XDATA_DOUBLE';
    XDATA_SHORT: Result := 'XDATA_SHORT';
    XDATA_LONG: Result := 'XDATA_LONG';
    XDATA_VOID: Result := 'XDATA_procedure';
    XDATA_GROUP: Result := 'XDATA_GROUP';
    XDATA_RFU6: Result := 'XDATA_RFU6';
    XDATA_RFU5: Result := 'XDATA_RFU5';
    XDATA_RFU4: Result := 'XDATA_RFU4';
    XDATA_RFU3: Result := 'XDATA_RFU3';
    XDATA_RFU2: Result := 'XDATA_RFU2';
    XDATA_RFU1: Result := 'XDATA_RFU1';

    // Material Chunk IDs
    MAT_ENTRY: Result := 'MAT_ENTRY';
    MAT_NAME: Result := 'MAT_NAME';
    MAT_AMBIENT: Result := 'MAT_AMBIENT';
    MAT_DIFFUSE: Result := 'MAT_DIFFUSE';
    MAT_SPECULAR: Result := 'MAT_SPECULAR';
    MAT_SHININESS: Result := 'MAT_SHININESS';
    MAT_SHIN2PCT: Result := 'MAT_SHIN2PCT';
    MAT_SHIN3PCT: Result := 'MAT_SHIN3PCT';
    MAT_TRANSPARENCY: Result := 'MAT_TRANSPARENCY';
    MAT_XPFALL: Result := 'MAT_XPFALL';
    MAT_REFBLUR: Result := 'MAT_REFBLUR';

    MAT_SELF_ILLUM: Result := 'MAT_SELF_ILLUM';
    MAT_TWO_SIDE: Result := 'MAT_TWO_SIDE';
    MAT_DECAL: Result := 'MAT_DECAL';
    MAT_ADDITIVE: Result := 'MAT_ADDITIVE';
    MAT_SELF_ILPCT: Result := 'MAT_SELF_ILPCT';
    MAT_WIRE: Result := 'MAT_WIRE';
    MAT_SUPERSMP: Result := 'MAT_SUPERSMP';
    MAT_WIRESIZE: Result := 'MAT_WIRESIZE';
    MAT_FACEMAP: Result := 'MAT_FACEMAP';
    MAT_XPFALLIN: Result := 'MAT_XPFALLIN';
    MAT_PHONGSOFT: Result := 'MAT_PHONGSOFT';
    MAT_WIREABS: Result := 'MAT_WIREABS';

    MAT_SHADING: Result := 'MAT_SHADING';

    MAT_TEXMAP: Result := 'MAT_TEXMAP';
    MAT_OPACMAP: Result := 'MAT_OPACMAP';
    MAT_REFLMAP: Result := 'MAT_REFLMAP';
    MAT_BUMPMAP: Result := 'MAT_BUMPMAP';
    MAT_SPECMAP: Result := 'MAT_SPECMAP';
    MAT_USE_XPFALL: Result := 'MAT_USE_XPFALL';
    MAT_USE_REFBLUR: Result := 'MAT_USE_REFBLUR';
    MAT_BUMP_PERCENT: Result := 'MAT_BUMP_PERCENT';

    MAT_MAPNAME: Result := 'MAT_MAPNAME';
    MAT_ACUBIC: Result := 'MAT_ACUBIC';

    MAT_SXP_TEXT_DATA: Result := 'MAT_SXP_TEXT_DATA';
    MAT_SXP_TEXT2_DATA: Result := 'MAT_SXP_TEXT2_DATA';
    MAT_SXP_OPAC_DATA: Result := 'MAT_SXP_OPAC_DATA';
    MAT_SXP_BUMP_DATA: Result := 'MAT_SXP_BUMP_DATA';
    MAT_SXP_SPEC_DATA: Result := 'MAT_SXP_SPEC_DATA';
    MAT_SXP_SHIN_DATA: Result := 'MAT_SXP_SHIN_DATA';
    MAT_SXP_SELFI_DATA: Result := 'MAT_SXP_SELFI_DATA';
    MAT_SXP_TEXT_MASKDATA: Result := 'MAT_SXP_TEXT_MASKDATA';
    MAT_SXP_TEXT2_MASKDATA: Result := 'MAT_SXP_TEXT2_MASKDATA';
    MAT_SXP_OPAC_MASKDATA: Result := 'MAT_SXP_OPAC_MASKDATA';
    MAT_SXP_BUMP_MASKDATA: Result := 'MAT_SXP_BUMP_MASKDATA';
    MAT_SXP_SPEC_MASKDATA: Result := 'MAT_SXP_SPEC_MASKDATA';
    MAT_SXP_SHIN_MASKDATA: Result := 'MAT_SXP_SHIN_MASKDATA';
    MAT_SXP_SELFI_MASKDATA: Result := 'MAT_SXP_SELFI_MASKDATA';
    MAT_SXP_REFL_MASKDATA: Result := 'MAT_SXP_REFL_MASKDATA';
    MAT_TEX2MAP: Result := 'MAT_TEX2MAP';
    MAT_SHINMAP: Result := 'MAT_SHINMAP';
    MAT_SELFIMAP: Result := 'MAT_SELFIMAP';
    MAT_TEXMASK: Result := 'MAT_TEXMASK';
    MAT_TEX2MASK: Result := 'MAT_TEX2MASK';
    MAT_OPACMASK: Result := 'MAT_OPACMASK';
    MAT_BUMPMASK: Result := 'MAT_BUMPMASK';
    MAT_SHINMASK: Result := 'MAT_SHINMASK';
    MAT_SPECMASK: Result := 'MAT_SPECMASK';
    MAT_SELFIMASK: Result := 'MAT_SELFIMASK';
    MAT_REFLMASK: Result := 'MAT_REFLMASK';
    MAT_MAP_TILINGOLD: Result := 'MAT_MAP_TILINGOLD';
    MAT_MAP_TILING: Result := 'MAT_MAP_TILING';
    MAT_MAP_TEXBLUR_OLD: Result := 'MAT_MAP_TEXBLUR_OLD';
    MAT_MAP_TEXBLUR: Result := 'MAT_MAP_TEXBLUR';
    MAT_MAP_USCALE: Result := 'MAT_MAP_USCALE';
    MAT_MAP_VSCALE: Result := 'MAT_MAP_VSCALE';
    MAT_MAP_UOFFSET: Result := 'MAT_MAP_UOFFSET';
    MAT_MAP_VOFFSET: Result := 'MAT_MAP_VOFFSET';
    MAT_MAP_ANG: Result := 'MAT_MAP_ANG';
    MAT_MAP_COL1: Result := 'MAT_MAP_COL1';
    MAT_MAP_COL2: Result := 'MAT_MAP_COL2';
    MAT_MAP_RCOL: Result := 'MAT_MAP_RCOL';
    MAT_MAP_GCOL: Result := 'MAT_MAP_GCOL';
    MAT_MAP_BCOL: Result := 'MAT_MAP_BCOL';

    // Keyframe Chunk IDs
    KFDATA: Result := 'KFDATA';
    KFHDR: Result := 'KFHDR';
    AMBIENT_NODE_TAG: Result := 'AMBIENT_NODE_TAG';
    OBJECT_NODE_TAG: Result := 'OBJECT_NODE_TAG';
    CAMERA_NODE_TAG: Result := 'CAMERA_NODE_TAG';
    TARGET_NODE_TAG: Result := 'TARGET_NODE_TAG';
    LIGHT_NODE_TAG: Result := 'LIGHT_NODE_TAG';
    L_TARGET_NODE_TAG: Result := 'L_TARGET_NODE_TAG';
    SPOTLIGHT_NODE_TAG: Result := 'SPOTLIGHT_NODE_TAG';

    KFSEG: Result := 'KFSEG';
    KFCURTIME: Result := 'KFCURTIME';
    NODE_HDR: Result := 'NODE_HDR';
    PARENT_NAME: Result := 'PARENT_NAME';
    INSTANCE_NAME: Result := 'INSTANCE_NAME';
    PRESCALE: Result := 'PRESCALE';
    PIVOT: Result := 'PIVOT';
    BOUNDBOX: Result := 'BOUNDBOX';
    MORPH_SMOOTH: Result := 'MORPH_SMOOTH';
    POS_TRACK_TAG: Result := 'POS_TRACK_TAG';
    ROT_TRACK_TAG: Result := 'ROT_TRACK_TAG';
    SCL_TRACK_TAG: Result := 'SCL_TRACK_TAG';
    FOV_TRACK_TAG: Result := 'FOV_TRACK_TAG';
    ROLL_TRACK_TAG: Result := 'ROLL_TRACK_TAG';
    COL_TRACK_TAG: Result := 'COL_TRACK_TAG';
    MORPH_TRACK_TAG: Result := 'MORPH_TRACK_TAG';
    HOT_TRACK_TAG: Result := 'HOT_TRACK_TAG';
    FALL_TRACK_TAG: Result := 'FALL_TRACK_TAG';
    HIDE_TRACK_TAG: Result := 'HIDE_TRACK_TAG';
    NODE_ID: Result := 'NODE_ID';

    CMAGIC: Result := 'CMAGIC';

    C_MDRAWER: Result := 'C_MDRAWER';
    C_TDRAWER: Result := 'C_TDRAWER';
    C_SHPDRAWER: Result := 'C_SHPDRAWER';
    C_MODDRAWER: Result := 'C_MODDRAWER';
    C_RIPDRAWER: Result := 'C_RIPDRAWER';
    C_TXDRAWER: Result := 'C_TXDRAWER';
    C_PDRAWER: Result := 'C_PDRAWER';
    C_MTLDRAWER: Result := 'C_MTLDRAWER';
    C_FLIDRAWER: Result := 'C_FLIDRAWER';
    C_CUBDRAWER: Result := 'C_CUBDRAWER';
    C_MFILE: Result := 'C_MFILE';
    C_SHPFILE: Result := 'C_SHPFILE';
    C_MODFILE: Result := 'C_MODFILE';
    C_RIPFILE: Result := 'C_RIPFILE';
    C_TXFILE: Result := 'C_TXFILE';
    C_PFILE: Result := 'C_PFILE';
    C_MTLFILE: Result := 'C_MTLFILE';
    C_FLIFILE: Result := 'C_FLIFILE';
    C_PALFILE: Result := 'C_PALFILE';
    C_TX_STRING: Result := 'C_TX_STRING';
    C_CONSTS: Result := 'C_CONSTS';
    C_SNAPS: Result := 'C_SNAPS';
    C_GRIDS: Result := 'C_GRIDS';
    C_ASNAPS: Result := 'C_ASNAPS';
    C_GRID_RANGE: Result := 'C_GRID_RANGE';
    C_RENDTYPE: Result := 'C_RENDTYPE';
    C_PROGMODE: Result := 'C_PROGMODE';
    C_PREVMODE: Result := 'C_PREVMODE';
    C_MODWMODE: Result := 'C_MODWMODE';
    C_MODMODEL: Result := 'C_MODMODEL';
    C_ALL_LINES: Result := 'C_ALL_LINES';
    C_BACK_TYPE: Result := 'C_BACK_TYPE';
    C_MD_CS: Result := 'C_MD_CS';
    C_MD_CE: Result := 'C_MD_CE';
    C_MD_SML: Result := 'C_MD_SML';
    C_MD_SMW: Result := 'C_MD_SMW';
    C_LOFT_WITH_TEXTURE: Result := 'C_LOFT_WITH_TEXTURE';
    C_LOFT_L_REPEAT: Result := 'C_LOFT_L_REPEAT';
    C_LOFT_W_REPEAT: Result := 'C_LOFT_W_REPEAT';
    C_LOFT_UV_NORMALIZE: Result := 'C_LOFT_UV_NORMALIZE';
    C_WELD_LOFT: Result := 'C_WELD_LOFT';
    C_MD_PDET: Result := 'C_MD_PDET';
    C_MD_SDET: Result := 'C_MD_SDET';
    C_RGB_RMODE: Result := 'C_RGB_RMODE';
    C_RGB_HIDE: Result := 'C_RGB_HIDE';
    C_RGB_MAPSW: Result := 'C_RGB_MAPSW';
    C_RGB_TWOSIDE: Result := 'C_RGB_TWOSIDE';
    C_RGB_SHADOW: Result := 'C_RGB_SHADOW';
    C_RGB_AA: Result := 'C_RGB_AA';
    C_RGB_OVW: Result := 'C_RGB_OVW';
    C_RGB_OVH: Result := 'C_RGB_OVH';
    C_RGB_PICTYPE: Result := 'C_RGB_PICTYPE';
    C_RGB_OUTPUT: Result := 'C_RGB_OUTPUT';
    C_RGB_TODISK: Result := 'C_RGB_TODISK';
    C_RGB_COMPRESS: Result := 'C_RGB_COMPRESS';
    C_JPEG_COMPRESSION: Result := 'C_JPEG_COMPRESSION';
    C_RGB_DISPDEV: Result := 'C_RGB_DISPDEV';
    C_RGB_HARDDEV: Result := 'C_RGB_HARDDEV';
    C_RGB_PATH: Result := 'C_RGB_PATH';
    C_BITMAP_DRAWER: Result := 'C_BITMAP_DRAWER';
    C_RGB_FILE: Result := 'C_RGB_FILE';
    C_RGB_OVASPECT: Result := 'C_RGB_OVASPECT';

    C_RGB_ANIMTYPE: Result := 'C_RGB_ANIMTYPE';
    C_RENDER_ALL: Result := 'C_RENDER_ALL';
    C_REND_FROM: Result := 'C_REND_FROM';
    C_REND_TO: Result := 'C_REND_TO';
    C_REND_NTH: Result := 'C_REND_NTH';
    C_REND_TSTEP: Result := 'C_REND_TSTEP';
    C_VP_TSTEP: Result := 'C_VP_TSTEP';

    C_PAL_TYPE: Result := 'C_PAL_TYPE';
    C_RND_TURBO: Result := 'C_RND_TURBO';
    C_RND_MIP: Result := 'C_RND_MIP';
    C_BGND_METHOD: Result := 'C_BGND_METHOD';
    C_AUTO_REFLECT: Result := 'C_AUTO_REFLECT';
    C_VP_FROM: Result := 'C_VP_FROM';
    C_VP_TO: Result := 'C_VP_TO';
    C_VP_NTH: Result := 'C_VP_NTH';

    C_SRDIAM: Result := 'C_SRDIAM';
    C_SRDEG: Result := 'C_SRDEG';
    C_SRSEG: Result := 'C_SRSEG';
    C_SRDIR: Result := 'C_SRDIR';
    C_HETOP: Result := 'C_HETOP';
    C_HEBOT: Result := 'C_HEBOT';
    C_HEHT: Result := 'C_HEHT';
    C_HETURNS: Result := 'C_HETURNS';
    C_HEDEG: Result := 'C_HEDEG';
    C_HESEG: Result := 'C_HESEG';
    C_HEDIR: Result := 'C_HEDIR';
    C_QUIKSTUFF: Result := 'C_QUIKSTUFF';
    C_SEE_LIGHTS: Result := 'C_SEE_LIGHTS';
    C_SEE_CAMERAS: Result := 'C_SEE_CAMERAS';
    C_SEE_3D: Result := 'C_SEE_3D';
    C_MESHSEL: Result := 'C_MESHSEL';
    C_MESHUNSEL: Result := 'C_MESHUNSEL';
    C_POLYSEL: Result := 'C_POLYSEL';
    C_POLYUNSEL: Result := 'C_POLYUNSEL';
    C_SHPLOCAL: Result := 'C_SHPLOCAL';
    C_MSHLOCAL: Result := 'C_MSHLOCAL';
    C_NUM_FORMAT: Result := 'C_NUM_FORMAT';
    C_ARCH_DENOM: Result := 'C_ARCH_DENOM';
    C_IN_DEVICE: Result := 'C_IN_DEVICE';
    C_MSCALE: Result := 'C_MSCALE';
    C_COMM_PORT: Result := 'C_COMM_PORT';
    C_TAB_BASES: Result := 'C_TAB_BASES';
    C_TAB_DIVS: Result := 'C_TAB_DIVS';
    C_MASTER_SCALES: Result := 'C_MASTER_SCALES';
    C_SHOW_1STVERT: Result := 'C_SHOW_1STVERT';
    C_SHAPER_OK: Result := 'C_SHAPER_OK';
    C_LOFTER_OK: Result := 'C_LOFTER_OK';
    C_EDITOR_OK: Result := 'C_EDITOR_OK';
    C_KEYFRAMER_OK: Result := 'C_KEYFRAMER_OK';
    C_PICKSIZE: Result := 'C_PICKSIZE';
    C_MAPTYPE: Result := 'C_MAPTYPE';
    C_MAP_DISPLAY: Result := 'C_MAP_DISPLAY';
    C_TILE_XY: Result := 'C_TILE_XY';
    C_MAP_XYZ: Result := 'C_MAP_XYZ';
    C_MAP_SCALE: Result := 'C_MAP_SCALE';
    C_MAP_MATRIX_OLD: Result := 'C_MAP_MATRIX_OLD';
    C_MAP_MATRIX: Result := 'C_MAP_MATRIX';
    C_MAP_WID_HT: Result := 'C_MAP_WID_HT';
    C_OBNAME: Result := 'C_OBNAME';
    C_CAMNAME: Result := 'C_CAMNAME';
    C_LTNAME: Result := 'C_LTNAME';
    C_CUR_MNAME: Result := 'C_CUR_MNAME';
    C_CURMTL_FROM_MESH: Result := 'C_CURMTL_FROM_MESH';
    C_GET_SHAPE_MAKE_FACES: Result := 'C_GET_SHAPE_MAKE_FACES';
    C_DETAIL: Result := 'C_DETAIL';
    C_VERTMARK: Result := 'C_VERTMARK';
    C_MSHAX: Result := 'C_MSHAX';
    C_MSHCP: Result := 'C_MSHCP';
    C_USERAX: Result := 'C_USERAX';
    C_SHOOK: Result := 'C_SHOOK';
    C_RAX: Result := 'C_RAX';
    C_STAPE: Result := 'C_STAPE';
    C_LTAPE: Result := 'C_LTAPE';
    C_ETAPE: Result := 'C_ETAPE';
    C_KTAPE: Result := 'C_KTAPE';
    C_SPHSEGS: Result := 'C_SPHSEGS';
    C_GEOSMOOTH: Result := 'C_GEOSMOOTH';
    C_HEMISEGS: Result := 'C_HEMISEGS';
    C_PRISMSEGS: Result := 'C_PRISMSEGS';
    C_PRISMSIDES: Result := 'C_PRISMSIDES';
    C_TUBESEGS: Result := 'C_TUBESEGS';
    C_TUBESIDES: Result := 'C_TUBESIDES';
    C_TORSEGS: Result := 'C_TORSEGS';
    C_TORSIDES: Result := 'C_TORSIDES';
    C_CONESIDES: Result := 'C_CONESIDES';
    C_CONESEGS: Result := 'C_CONESEGS';
    C_NGPARMS: Result := 'C_NGPARMS';
    C_PTHLEVEL: Result := 'C_PTHLEVEL';
    C_MSCSYM: Result := 'C_MSCSYM';
    C_MFTSYM: Result := 'C_MFTSYM';
    C_MTTSYM: Result := 'C_MTTSYM';
    C_SMOOTHING: Result := 'C_SMOOTHING';
    C_MODICOUNT: Result := 'C_MODICOUNT';
    C_FONTSEL: Result := 'C_FONTSEL';
    C_TESS_TYPE: Result := 'C_TESS_TYPE';
    C_TESS_TENSION: Result := 'C_TESS_TENSION';

    C_SEG_START: Result := 'C_SEG_START';
    C_SEG_END: Result := 'C_SEG_END';
    C_CURTIME: Result := 'C_CURTIME';
    C_ANIMLENGTH: Result := 'C_ANIMLENGTH';
    C_PV_FROM: Result := 'C_PV_FROM';
    C_PV_TO: Result := 'C_PV_TO';
    C_PV_DOFNUM: Result := 'C_PV_DOFNUM';
    C_PV_RNG: Result := 'C_PV_RNG';
    C_PV_NTH: Result := 'C_PV_NTH';
    C_PV_TYPE: Result := 'C_PV_TYPE';
    C_PV_METHOD: Result := 'C_PV_METHOD';
    C_PV_FPS: Result := 'C_PV_FPS';
    C_VTR_FRAMES: Result := 'C_VTR_FRAMES';
    C_VTR_HDTL: Result := 'C_VTR_HDTL';
    C_VTR_HD: Result := 'C_VTR_HD';
    C_VTR_TL: Result := 'C_VTR_TL';
    C_VTR_IN: Result := 'C_VTR_IN';
    C_VTR_PK: Result := 'C_VTR_PK';
    C_VTR_SH: Result := 'C_VTR_SH';

    // Material chunks
    C_WORK_MTLS: Result := 'C_WORK_MTLS';
    C_WORK_MTLS_2: Result := 'C_WORK_MTLS_2';
    C_WORK_MTLS_3: Result := 'C_WORK_MTLS_3';
    C_WORK_MTLS_4: Result := 'C_WORK_MTLS_4';
    C_WORK_MTLS_5: Result := 'C_WORK_MTLS_5';
    C_WORK_MTLS_6: Result := 'C_WORK_MTLS_6';
    C_WORK_MTLS_7: Result := 'C_WORK_MTLS_7';
    C_WORK_MTLS_8: Result := 'C_WORK_MTLS_8';
    C_WORKMTL: Result := 'C_WORKMTL';
    C_SXP_TEXT_DATA: Result := 'C_SXP_TEXT_DATA';
    C_SXP_TEXT2_DATA: Result := 'C_SXP_TEXT2_DATA';
    C_SXP_OPAC_DATA: Result := 'C_SXP_OPAC_DATA';
    C_SXP_BUMP_DATA: Result := 'C_SXP_BUMP_DATA';
    C_SXP_SPEC_DATA: Result := 'C_SXP_SPEC_DATA';
    C_SXP_SHIN_DATA: Result := 'C_SXP_SHIN_DATA';
    C_SXP_SELFI_DATA: Result := 'C_SXP_SELFI_DATA';
    C_SXP_TEXT_MASKDATA: Result := 'C_SXP_TEXT_MASKDATA';
    C_SXP_TEXT2_MASKDATA: Result := 'C_SXP_TEXT2_MASKDATA';
    C_SXP_OPAC_MASKDATA: Result := 'C_SXP_OPAC_MASKDATA';
    C_SXP_BUMP_MASKDATA: Result := 'C_SXP_BUMP_MASKDATA';
    C_SXP_SPEC_MASKDATA: Result := 'C_SXP_SPEC_MASKDATA';
    C_SXP_SHIN_MASKDATA: Result := 'C_SXP_SHIN_MASKDATA';
    C_SXP_SELFI_MASKDATA: Result := 'C_SXP_SELFI_MASKDATA';
    C_SXP_REFL_MASKDATA: Result := 'C_SXP_REFL_MASKDATA';

    C_BGTYPE: Result := 'C_BGTYPE';
    C_MEDTILE: Result := 'C_MEDTILE';

    // Contrast
    C_LO_CONTRAST: Result := 'C_LO_CONTRAST';
    C_HI_CONTRAST: Result := 'C_HI_CONTRAST';

    // 3D frozen display
    C_FROZ_DISPLAY: Result := 'C_FROZ_DISPLAY';

    // Booleans
    C_BOOLWELD: Result := 'C_BOOLWELD';
    C_BOOLTYPE: Result := 'C_BOOLTYPE';

    C_ANG_THRESH: Result := 'C_ANG_THRESH';
    C_SS_THRESH: Result := 'C_SS_THRESH';
    C_TEXTURE_BLUR_DEFAULT: Result := 'C_TEXTURE_BLUR_DEFAULT';

    C_MAPDRAWER: Result := 'C_MAPDRAWER';
    C_MAPDRAWER1: Result := 'C_MAPDRAWER1';
    C_MAPDRAWER2: Result := 'C_MAPDRAWER2';
    C_MAPDRAWER3: Result := 'C_MAPDRAWER3';
    C_MAPDRAWER4: Result := 'C_MAPDRAWER4';
    C_MAPDRAWER5: Result := 'C_MAPDRAWER5';
    C_MAPDRAWER6: Result := 'C_MAPDRAWER6';
    C_MAPDRAWER7: Result := 'C_MAPDRAWER7';
    C_MAPDRAWER8: Result := 'C_MAPDRAWER8';
    C_MAPDRAWER9: Result := 'C_MAPDRAWER9';
    C_MAPDRAWER_ENTRY: Result := 'C_MAPDRAWER_ENTRY';

    // system options
    C_BACKUP_FILE: Result := 'C_BACKUP_FILE';
    C_DITHER_256: Result := 'C_DITHER_256';
    C_SAVE_LAST: Result := 'C_SAVE_LAST';
    C_USE_ALPHA: Result := 'C_USE_ALPHA';
    C_TGA_DEPTH: Result := 'C_TGA_DEPTH';
    C_REND_FIELDS: Result := 'C_REND_FIELDS';
    C_REFLIP: Result := 'C_REFLIP';
    C_SEL_ITEMTOG: Result := 'C_SEL_ITEMTOG';
    C_SEL_RESET: Result := 'C_SEL_RESET';
    C_STICKY_KEYINF: Result := 'C_STICKY_KEYINF';
    C_WELD_THRESHOLD: Result := 'C_WELD_THRESHOLD';
    C_ZCLIP_POINT: Result := 'C_ZCLIP_POINT';
    C_ALPHA_SPLIT: Result := 'C_ALPHA_SPLIT';
    C_KF_SHOW_BACKFACE: Result := 'C_KF_SHOW_BACKFACE';
    C_OPTIMIZE_LOFT: Result := 'C_OPTIMIZE_LOFT';
    C_TENS_DEFAULT: Result := 'C_TENS_DEFAULT';
    C_CONT_DEFAULT: Result := 'C_CONT_DEFAULT';
    C_BIAS_DEFAULT: Result := 'C_BIAS_DEFAULT';

    C_DXFNAME_SRC: Result := 'C_DXFNAME_SRC ';
    C_AUTO_WELD: Result := 'C_AUTO_WELD ';
    C_AUTO_UNIFY: Result := 'C_AUTO_UNIFY ';
    C_AUTO_SMOOTH: Result := 'C_AUTO_SMOOTH ';
    C_DXF_SMOOTH_ANG: Result := 'C_DXF_SMOOTH_ANG ';
    C_SMOOTH_ANG: Result := 'C_SMOOTH_ANG ';

    // Special network-use chunks
    C_NET_USE_VPOST: Result := 'C_NET_USE_VPOST';
    C_NET_USE_GAMMA: Result := 'C_NET_USE_GAMMA';
    C_NET_FIELD_ORDER: Result := 'C_NET_FIELD_ORDER';

    C_BLUR_FRAMES: Result := 'C_BLUR_FRAMES';
    C_BLUR_SAMPLES: Result := 'C_BLUR_SAMPLES';
    C_BLUR_DUR: Result := 'C_BLUR_DUR';
    C_HOT_METHOD: Result := 'C_HOT_METHOD';
    C_HOT_CHECK: Result := 'C_HOT_CHECK';
    C_PIXEL_SIZE: Result := 'C_PIXEL_SIZE';
    C_DISP_GAMMA: Result := 'C_DISP_GAMMA';
    C_FBUF_GAMMA: Result := 'C_FBUF_GAMMA';
    C_FILE_OUT_GAMMA: Result := 'C_FILE_OUT_GAMMA';
    C_FILE_IN_GAMMA: Result := 'C_FILE_IN_GAMMA';
    C_GAMMA_CORRECT: Result := 'C_GAMMA_CORRECT';
    C_APPLY_DISP_GAMMA: Result := 'C_APPLY_DISP_GAMMA';
    C_APPLY_FBUF_GAMMA: Result := 'C_APPLY_FBUF_GAMMA';
    C_APPLY_FILE_GAMMA: Result := 'C_APPLY_FILE_GAMMA';
    C_FORCE_WIRE: Result := 'C_FORCE_WIRE';
    C_RAY_SHADOWS: Result := 'C_RAY_SHADOWS';
    C_MASTER_AMBIENT: Result := 'C_MASTER_AMBIENT';
    C_SUPER_SAMPLE: Result := 'C_SUPER_SAMPLE';
    C_OBJECT_MBLUR: Result := 'C_OBJECT_MBLUR';
    C_MBLUR_DITHER: Result := 'C_MBLUR_DITHER';
    C_DITHER_24: Result := 'C_DITHER_24';
    C_SUPER_BLACK: Result := 'C_SUPER_BLACK';
    C_SAFE_FRAME: Result := 'C_SAFE_FRAME';
    C_VIEW_PRES_RATIO: Result := 'C_VIEW_PRES_RATIO';
    C_BGND_PRES_RATIO: Result := 'C_BGND_PRES_RATIO';
    C_NTH_SERIAL_NUM: Result := 'C_NTH_SERIAL_NUM';

    VPDATA: Result := 'VPDATA';

    P_QUEUE_ENTRY: Result := 'P_QUEUE_ENTRY';
    P_QUEUE_IMAGE: Result := 'P_QUEUE_IMAGE';
    P_QUEUE_USEIGAMMA: Result := 'P_QUEUE_USEIGAMMA';
    P_QUEUE_PROC: Result := 'P_QUEUE_PROC';
    P_QUEUE_SOLID: Result := 'P_QUEUE_SOLID';
    P_QUEUE_GRADIENT: Result := 'P_QUEUE_GRADIENT';
    P_QUEUE_KF: Result := 'P_QUEUE_KF';
    P_QUEUE_MOTBLUR: Result := 'P_QUEUE_MOTBLUR';
    P_QUEUE_MB_REPEAT: Result := 'P_QUEUE_MB_REPEAT';
    P_QUEUE_NONE: Result := 'P_QUEUE_NONE';

    P_QUEUE_RESIZE: Result := 'P_QUEUE_RESIZE';
    P_QUEUE_OFFSET: Result := 'P_QUEUE_OFFSET';
    P_QUEUE_ALIGN: Result := 'P_QUEUE_ALIGN';

    P_CUSTOM_SIZE: Result := 'P_CUSTOM_SIZE';

    P_ALPH_NONE: Result := 'P_ALPH_NONE';
    P_ALPH_PSEUDO: Result := 'P_ALPH_PSEUDO';
    P_ALPH_OP_PSEUDO: Result := 'P_ALPH_OP_PSEUDO';
    P_ALPH_BLUR: Result := 'P_ALPH_BLUR';
    P_ALPH_PCOL: Result := 'P_ALPH_PCOL';
    P_ALPH_C0: Result := 'P_ALPH_C0';
    P_ALPH_OP_KEY: Result := 'P_ALPH_OP_KEY';
    P_ALPH_KCOL: Result := 'P_ALPH_KCOL';
    P_ALPH_OP_NOCONV: Result := 'P_ALPH_OP_NOCONV';
    P_ALPH_IMAGE: Result := 'P_ALPH_IMAGE';
    P_ALPH_ALPHA: Result := 'P_ALPH_ALPHA';
    P_ALPH_QUES: Result := 'P_ALPH_QUES';
    P_ALPH_QUEIMG: Result := 'P_ALPH_QUEIMG';
    P_ALPH_CUTOFF: Result := 'P_ALPH_CUTOFF';
    P_ALPHANEG: Result := 'P_ALPHANEG';

    P_TRAN_NONE: Result := 'P_TRAN_NONE';
    P_TRAN_IMAGE: Result := 'P_TRAN_IMAGE';
    P_TRAN_FRAMES: Result := 'P_TRAN_FRAMES';
    P_TRAN_FADEIN: Result := 'P_TRAN_FADEIN';
    P_TRAN_FADEOUT: Result := 'P_TRAN_FADEOUT';
    P_TRANNEG: Result := 'P_TRANNEG';

    P_RANGES: Result := 'P_RANGES';

    P_PROC_DATA: Result := 'P_PROC_DATA'
    else
      Result := 'UNKNOWN_CHUNK';
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

const
  IndentString: string = #9#9#9#9#9#9#9#9#9#9#9#9;

function Indent(Level: integer): string;
begin
  Result := Copy(IndentString, 1, Level);
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ChunkHeaderReport(var Strings: TStrings; Chunk: PChunk3DS;
  IndentLevel: integer);

var
  OutString: string;

begin
  OutString := Format('%sChunk %s ($%x), Length is %d ($%3:x)',
    [Indent(IndentLevel), ChunkTagToString(Chunk^.Tag), Chunk^.Tag, Chunk^.Size]);
  Strings.Add(OutString);
end;

//---------------------------------------------------------------------------------------------------------------------

procedure DumpKeyHeader(Strings: TStrings; const Key: TKeyHeader3DS; IndentLevel: integer);

var
  Output: string;

begin
  Output := Format('%sFrame %d', [Indent(IndentLevel), Key.Time]);
  if (Key.rflags and KeyUsesTension3DS) <> 0 then
    Output := Output + Format(', Tens %.2f', [Key.Tension]);
  if (Key.rflags and KeyUsesCont3DS) <> 0 then
    Output := Output + Format(', Cont %.2f', [Key.Continuity]);
  if (Key.rflags and KeyUsesBias3DS) <> 0 then
    Output := Output + Format(', Bias %.2f', [Key.Bias]);
  if (Key.rflags and KeyUsesEaseTo3DS) <> 0 then
    Output := Output + Format(', Ease to %.2f', [Key.EaseTo]);
  if (Key.rflags and KeyUsesEaseFrom3DS) <> 0 then
    Output := Output + Format(', Ease from %.2f', [Key.EaseFrom]);
  Strings.Add(Output);
end;

//---------------------------------------------------------------------------------------------------------------------

procedure DumpChunk(const Source: TFile3DS; var Strings: TStrings;
  Chunk: PChunk3DS; IndentLevel: integer; DumpLevel: TDumpLevel);

// retrieves the Data for a Chunk from the given Source, formats the Data into
// one or more lines of text and puts the lines into the given Strings parameter

var
  Child: PChunk3DS;
  Output: string;
  ID: string;
  I: integer;

begin
  ChunkHeaderReport(Strings, Chunk, IndentLevel);
  ID := Indent(IndentLevel) + #9;

  if DumpLevel <> dlTerseDump then
  begin
    case Chunk^.Tag of
      MESH_VERSION:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sVersion %d', [ID, Chunk^.Data.MeshVersion^]);
        Strings.Add(Output);
      end;
      M3D_VERSION:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sVersion %d', [ID, Chunk^.Data.M3DVersion^]);
        Strings.Add(Output);
      end;
      COLOR_F:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sColor R:  %f, ', [ID, Chunk^.Data.ColorF^.Red]);
        Output := Output + Format(' G:  %f, ', [Chunk^.Data.ColorF^.Green]);
        Output := Output + Format(' B:  %f', [Chunk^.Data.ColorF^.Blue]);
        Strings.Add(Output);
      end;
      LIN_COLOR_F:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sColor R:  %f, ', [ID, Chunk^.Data.LinColorF^.Red]);
        Output := Output + Format(' G:  %f, ', [Chunk^.Data.LinColorF^.Green]);
        Output := Output + Format(' B:  %f', [Chunk^.Data.LinColorF^.Blue]);
        Strings.Add(Output);
      end;
      COLOR_24:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sColor R: %d, ', [ID, Chunk^.Data.Color24^.Red]);
        Output := Output + Format(' G: %d, ', [Chunk^.Data.Color24^.Green]);
        Output := Output + Format(' B: %d', [Chunk^.Data.Color24^.Blue]);
        Strings.Add(Output);
      end;
      LIN_COLOR_24:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sColor R: %d, ', [ID, Chunk^.Data.LinColor24^.Red]);
        Output := Output + Format(' G: %d, ', [Chunk^.Data.LinColor24^.Green]);
        Output := Output + Format(' B: %d', [Chunk^.Data.LinColor24^.Blue]);
        Strings.Add(Output);
      end;
      INT_PERCENTAGE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sPercentage of %d%%', [ID, Chunk^.Data.IntPercentage^]);
        Strings.Add(Output);
      end;
      FLOAT_PERCENTAGE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sPercentage of  %f%%', [ID, Chunk^.Data.FloatPercentage^]);
        Strings.Add(Output);
      end;
      MASTER_SCALE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMaster Scale  %f', [ID, Chunk^.Data.MasterScale^]);
        Strings.Add(Output);
      end;
      BIT_MAP:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sBitmap Name %s', [ID, Chunk^.Data.BitMapName^]);
        Strings.Add(Output);
      end;
      V_GRADIENT:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMidpoint  %f', [ID, Chunk^.Data.VGradient^]);
        Strings.Add(Output);
      end;
      LO_SHADOW_BIAS:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sBias of  %f', [ID, Chunk^.Data.LoShadowBias^]);
        Strings.Add(Output);
      end;
      HI_SHADOW_BIAS:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sBias of  %f', [ID, Chunk^.Data.HiShadowBias^]);
        Strings.Add(Output);
      end;
      RAY_BIAS:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sBias of  %f', [ID, Chunk^.Data.RayBias^]);
        Strings.Add(Output);
      end;
      SHADOW_MAP_SIZE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sSize of %d', [ID, Chunk^.Data.ShadowMapSize^]);
        Strings.Add(Output);
      end;
      SHADOW_SAMPLES:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sSize of %d', [ID, Chunk^.Data.ShadowSamples^]);
        Strings.Add(Output);
      end;
      SHADOW_RANGE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sRange of %d', [ID, Chunk^.Data.ShadowRange^]);
        Strings.Add(Output);
      end;
      SHADOW_FILTER:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sFilter of  %f', [ID, Chunk^.Data.ShadowFilter^]);
        Strings.Add(Output);
      end;
      O_CONSTS:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sPlane at  %f,  %f,  %f',
          [ID, Chunk^.Data.OConsts^.X, Chunk^.Data.OConsts^.Y, Chunk^.Data.OConsts^.Z]);
        Strings.Add(Output);
      end;
      FOG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sNear plane at  %f', [ID, Chunk^.Data.Fog^.NearPlaneDist]);
        Strings.Add(Output);
        Output := Format('%sNear Density of  %f',
          [ID, Chunk^.Data.Fog^.NearPlaneDensity]);
        Strings.Add(Output);
        Output := Format('%sFar plane at  %f', [ID, Chunk^.Data.Fog^.FarPlaneDist]);
        Strings.Add(Output);
        Output := Format('%sFar Density of  %f',
          [ID, Chunk^.Data.Fog^.FarPlaneDensity]);
        Strings.Add(Output);
      end;
      LAYER_FOG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sFog Z range is  %f to  %f',
          [ID, Chunk^.Data.LayerFog^.ZMin, Chunk^.Data.LayerFog^.ZMax]);
        Strings.Add(Output);
        Output := Format('%sFog Density is  %f', [ID, Chunk^.Data.LayerFog^.Density]);
        Strings.Add(Output);
        Output := Format('%sFog type of $%x', [ID, Chunk^.Data.LayerFog^.AType]);
        Strings.Add(Output);
      end;
      DISTANCE_CUE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sNear plane at  %f',
          [ID, Chunk^.Data.DistanceCue^.NearPlaneDist]);
        Strings.Add(Output);
        Output := Format('%sNear Density of  %f',
          [ID, Chunk^.Data.DistanceCue^.NearPlaneDimming]);
        Strings.Add(Output);
        Output := Format('%sFar plane at  %f',
          [ID, Chunk^.Data.DistanceCue^.FarPlaneDist]);
        Strings.Add(Output);
        Output := Format('%sFar Density of  %f',
          [ID, Chunk^.Data.DistanceCue^.FarPlaneDimming]);
        Strings.Add(Output);
      end;
      VIEW_TOP,
      VIEW_BOTTOM,
      VIEW_LEFT,
      VIEW_RIGHT,
      VIEW_FRONT,
      VIEW_BACK:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sTarget at  %f,  %f,  %f',
          [ID, Chunk^.Data.ViewStandard^.ViewTargetCoord.X,
          Chunk^.Data.ViewStandard^.ViewTargetCoord.Y,
          Chunk^.Data.ViewStandard^.ViewTargetCoord.Z]);
        Strings.Add(Output);
        Output := Format('%sView Width of  %f',
          [ID, Chunk^.Data.ViewStandard^.ViewWidth]);
        Strings.Add(Output);
      end;
      VIEW_USER:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sTarget at  %f,  %f,  %f',
          [ID, Chunk^.Data.ViewUser^.ViewTargetCoord.X,
          Chunk^.Data.ViewUser^.ViewTargetCoord.Y,
          Chunk^.Data.ViewUser^.ViewTargetCoord.Z]);
        Strings.Add(Output);
        Output := Format('%sView Width of  %f', [ID, Chunk^.Data.ViewUser^.ViewWidth]);
        Strings.Add(Output);
        Output := Format('%sHorizontal View angle of  %f',
          [ID, Chunk^.Data.ViewUser^.XYViewAngle]);
        Strings.Add(Output);
        Output := Format('%sVertical View angle of  %f',
          [ID, Chunk^.Data.ViewUser^.YZViewAngle]);
        Strings.Add(Output);
        Output := Format('%sBank angle of  %f', [ID, Chunk^.Data.ViewUser^.BankAngle]);
        Strings.Add(Output);
      end;
      VIEW_CAMERA:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sCamera Name %s', [ID, Chunk^.Data.ViewCamera^]);
        Strings.Add(Output);
      end;
      NAMED_OBJECT:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sName: %s', [ID, Chunk^.Data.NamedObject^]);
        Strings.Add(Output);
      end;
      POINT_ARRAY:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Vertices', [ID, Chunk^.Data.PointArray^.Vertices]);
        Strings.Add(Output);
        if DumpLevel = dlMaximumDump then
          for I := 0 to Chunk^.Data.PointArray^.Vertices - 1 do
          begin
            Output := Format('%sVertex %d at  %f,  %f,  %f',
              [ID, I, Chunk^.Data.PointArray^.PointList^[I].X,
              Chunk^.Data.PointArray^.PointList^[I].Y,
              Chunk^.Data.PointArray^.PointList^[I].Z]);
            Strings.Add(Output);
          end;
      end;
      POINT_FLAG_ARRAY:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sFlags: %d', [ID, Chunk^.Data.PointFlagArray^.Flags]);
        Strings.Add(Output);
        if DumpLevel = dlMaximumDump then
          for I := 0 to Chunk^.Data.PointFlagArray^.Flags - 1 do
          begin
            Output := Format('%sFlag %d is %d',
              [ID, I, Chunk^.Data.PointFlagArray^.FlagList^[I]]);
            Strings.Add(Output);
          end;
      end;
      FACE_ARRAY:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Faces', [ID, Chunk^.Data.FaceArray^.Faces]);
        Strings.Add(Output);
        if DumpLevel = dlMaximumDump then
          for I := 0 to Chunk^.Data.FaceArray^.Faces - 1 do
          begin
            Output := Format('%sFace %d Vertices %d, %d, %d and flag $%x',
              [ID, I, Chunk^.Data.FaceArray^.FaceList^[I].V1,
              Chunk^.Data.FaceArray^.FaceList^[I].V2,
              Chunk^.Data.FaceArray^.FaceList^[I].V3,
              Chunk^.Data.FaceArray^.FaceList^[I].Flag]);
            Strings.Add(Output);
          end;
      end;
      MSH_MAT_GROUP:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMaterial Name of %s',
          [ID, Chunk^.Data.MshMatGroup^.MatNameStr]);
        Strings.Add(Output);
        Output := Format('%sAssigned to %d Faces',
          [ID, Chunk^.Data.MshMatGroup^.Faces]);
        Strings.Add(Output);
      end;
      MSH_BOXMAP:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sBoxmap consists of the following materials:', [ID]);
        Strings.Add(Output);
        for I := 0 to 5 do
        begin
          Output := Format('%s%s', [ID, Chunk^.Data.MshBoxmap^[I]]);
          Strings.Add(Output);
        end;
      end;
      TEX_VERTS:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Vertices', [ID, Chunk^.Data.TexVerts^.NumCoords]);
        Strings.Add(Output);
        if DumpLevel = dlMaximumDump then
        begin
          for I := 0 to Chunk^.Data.TexVerts^.NumCoords - 1 do
          begin
            Output := Format('%sVertex %d with tex vert of  %f,  %f',
              [ID, I, Chunk^.Data.TexVerts^.TextVertList^[I].U,
              Chunk^.Data.TexVerts^.TextVertList^[I].V]);
            Strings.Add(Output);
          end;
        end;
      end;
      MESH_TEXTURE_INFO:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMap Type of %d',
          [ID, Chunk^.Data.MeshTextureInfo^.MapType]);
        Strings.Add(Output);
        Output := Format('%sX Tiling of  %f',
          [ID, Chunk^.Data.MeshTextureInfo^.XTiling]);
        Strings.Add(Output);
        Output := Format('%sY Tiling of  %f',
          [ID, Chunk^.Data.MeshTextureInfo^.YTiling]);
        Strings.Add(Output);
        Output := Format('%sIcon position of  %f,  %f,  %f',
          [ID, Chunk^.Data.MeshTextureInfo^.IconPos.X,
          Chunk^.Data.MeshTextureInfo^.IconPos.Y,
          Chunk^.Data.MeshTextureInfo^.IconPos.Z]);
        Strings.Add(Output);
        I := 0;
        while I < 12 do
        begin
          Output := Format('%s[%d]  %f [%d]  %f [%d]  %f',
            [ID, I, Chunk^.Data.MeshTextureInfo^.XMatrix[I], I +
            1, Chunk^.Data.MeshTextureInfo^.XMatrix[I + 1], I +
            2, Chunk^.Data.MeshTextureInfo^.XMatrix[I + 2]]);
          Strings.Add(Output);
          Inc(I, 3);
        end;
        Output := Format('%sScaling Value of  %f',
          [ID, Chunk^.Data.MeshTextureInfo^.IconScaling]);
        Strings.Add(Output);
        Output := Format('%sPlanar Icon Width of  %f',
          [ID, Chunk^.Data.MeshTextureInfo^.IconWidth]);
        Strings.Add(Output);
        Output := Format('%sPlanar Icon Height of  %f',
          [ID, Chunk^.Data.MeshTextureInfo^.IconHeight]);
        Strings.Add(Output);
        Output := Format('%sCylinder Icon Height of  %f',
          [ID, Chunk^.Data.MeshTextureInfo^.CylIconHeight]);
        Strings.Add(Output);
      end;
      MESH_MATRIX:
      begin
        Source.ReadChunkData(Chunk);
        I := 0;
        while I < 12 do
        begin
          Output := Format('%s[%d]  %f [%d]  %f [%d]  %f',
            [ID, I, Chunk^.Data.MeshMatrix^[I], I + 1,
            Chunk^.Data.MeshMatrix^[I + 1], I + 2, Chunk^.Data.MeshMatrix^[I + 2]]);
          Strings.Add(Output);
          Inc(I, 3);
        end;
      end;
      PROC_NAME:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sProcedure Name of %s', [ID, Chunk^.Data.ProcName^]);
        Strings.Add(Output);
      end;
      MESH_COLOR:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sColor index of %d', [ID, Chunk^.Data.MeshColor^]);
        Strings.Add(Output);
      end;
      N_DIRECT_LIGHT:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sLight at  %f,  %f,  %f',
          [ID, Chunk^.Data.NDirectLight^.X, Chunk^.Data.NDirectLight^.Y,
          Chunk^.Data.NDirectLight^.Z]);
        Strings.Add(Output);
      end;
      DL_EXCLUDE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sExclude %s', [ID, Chunk^.Data.DLExclude^]);
        Strings.Add(Output);
      end;
      DL_OUTER_RANGE,
      DL_INNER_RANGE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sRange of  %f', [ID, Chunk^.Data.DlOuterRange^]);
        Strings.Add(Output);
      end;
      DL_MULTIPLIER:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMultiple of  %f', [ID, Chunk^.Data.DlMultiplier^]);
        Strings.Add(Output);
      end;
      DL_SPOT_ROLL:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sRoll angle of  %f', [ID, Chunk^.Data.DlSpotRoll^]);
        Strings.Add(Output);
      end;
      DL_SPOT_ASPECT:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sSpot aspect of  %f', [ID, Chunk^.Data.DlSpotAspect^]);
        Strings.Add(Output);
      end;
      DL_SPOT_PROJECTOR:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sFilename of projector is %s',
          [ID, Chunk^.Data.DlSpotProjector^]);
        Strings.Add(Output);
      end;
      DL_RAY_BIAS:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sBias of  %f', [ID, Chunk^.Data.DlRayBias^]);
        Strings.Add(Output);
      end;
      DL_SPOTLIGHT:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sTarget at  %f, %f, %f',
          [ID, Chunk^.Data.DlSpotlight^.SpotlightTarg.X,
          Chunk^.Data.DlSpotlight^.SpotlightTarg.Y,
          Chunk^.Data.DlSpotlight^.SpotlightTarg.Z]);
        Strings.Add(Output);
        Output := Format('%sHotspot cone of  %f, ',
          [ID, Chunk^.Data.DlSpotlight^.HotspotAngle]);
        Output := Output + Format(' Falloff cone of  %f',
          [Chunk^.Data.DlSpotlight^.FalloffAngle]);
        Strings.Add(Output);
      end;
      DL_LOCAL_SHADOW2:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sShadow bias of  %f',
          [ID, Chunk^.Data.DlLocalShadow2^.LocalShadowBias]);
        Strings.Add(Output);
        Output := Format('%sShadow filter of  %f',
          [ID, Chunk^.Data.DlLocalShadow2^.LocalShadowFilter]);
        Strings.Add(Output);
        Output := Format('%sShadow Map Size of  %f',
          [ID, Chunk^.Data.DlLocalShadow2^.LocalShadowMapSize]);
        Strings.Add(Output);
      end;
      N_CAMERA:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sCamera at  %f,  %f,  %f',
          [ID, Chunk^.Data.NCamera^.CameraPos.X, Chunk^.Data.NCamera^.CameraPos.Y,
          Chunk^.Data.NCamera^.CameraPos.Z]);
        Strings.Add(Output);
        Output := Format('%sTarget at  %f,  %f,  %f',
          [ID, Chunk^.Data.NCamera^.TargetPos.X, Chunk^.Data.NCamera^.TargetPos.Y,
          Chunk^.Data.NCamera^.TargetPos.Z]);
        Strings.Add(Output);
        Output := Format('%sBank angle of  %f', [ID, Chunk^.Data.NCamera^.CameraBank]);
        Output := Output + Format(' and a foc of  %f',
          [Chunk^.Data.NCamera^.CameraFocalLength]);
        Strings.Add(Output);
      end;
      CAM_RANGES:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sCamera near range is  %f and far range is  %f',
          [ID, Chunk^.Data.CamRanges^.NearPlane, Chunk^.Data.CamRanges^.FarPlane]);
        Strings.Add(Output);
      end;
      VIEWPORT_LAYOUT:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sForm of %d', [ID, Chunk^.Data.ViewportLayout^.Form]);
        Strings.Add(Output);
        Output := Format('%sTop of %d', [ID, Chunk^.Data.ViewportLayout^.Top]);
        Strings.Add(Output);
        Output := Format('%sReady of %d', [ID, Chunk^.Data.ViewportLayout^.Ready]);
        Strings.Add(Output);
        Output := Format('%sWState of %d', [ID, Chunk^.Data.ViewportLayout^.WState]);
        Strings.Add(Output);
        Output := Format('%sSwap WS of %d', [ID, Chunk^.Data.ViewportLayout^.SwapWS]);
        Strings.Add(Output);
        Output := Format('%sSwap Port of %d',
          [ID, Chunk^.Data.ViewportLayout^.SwapPort]);
        Strings.Add(Output);
        Output := Format('%sSwap Cur of %d',
          [ID, Chunk^.Data.ViewportLayout^.SwapCur]);
        Strings.Add(Output);
      end;
      VIEWPORT_SIZE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sWork Area X: %d Y: %d W: %d H: %d',
          [ID, Chunk^.Data.ViewportSize^.XPos, Chunk^.Data.ViewportSize^.YPos,
          Chunk^.Data.ViewportSize^.Width,
          Chunk^.Data.ViewportSize^.Height]);
        Strings.Add(Output);
      end;
      VIEWPORT_DATA_3,
      VIEWPORT_DATA:
      begin
        Source.ReadChunkData(Chunk);
        with Chunk^.Data.ViewportData^ do
        begin
          Output := Format('%sFlags: $%x', [ID, Flags]);
          Strings.Add(Output);
          Output := Format('%sAxis Lockouts of $%x', [ID, AxisLockout]);
          Strings.Add(Output);
          Output := Format('%sWindow Position of %d, %d', [ID, WinXPos, WinYPos]);
          Strings.Add(Output);
          Output := Format('%sWindow Size of %d, %d', [ID, WinWidth, WinHeight]);
          Strings.Add(Output);
          Output := Format('%sWindow View of %d', [ID, View]);
          Strings.Add(Output);
          Output := Format('%sZoom Factor of  %f', [ID, ZoomFactor]);
          Strings.Add(Output);
          Output := Format('%sWorld Center of  %f, %f, %f',
            [ID, Center.X, Center.Y, Center.Z]);
          Strings.Add(Output);
          Output := Format('%sHorizontal Angle of  %f', [ID, HorizAng]);
          Strings.Add(Output);
          Output := Format('%sVertical Angle of  %f', [ID, VertAng]);
          Strings.Add(Output);
          Output := Format('%sCamera Name of %s', [ID, CamNameStr]);
          Strings.Add(Output);
        end;
      end;
      XDATA_APPNAME:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sApplication Name %s', [ID, Chunk^.Data.XDataAppName^]);
        Strings.Add(Output);
      end;
      XDATA_STRING:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sString value of %s', [ID, Chunk^.Data.XDataString^]);
        Strings.Add(Output);
      end;
      MAT_NAME:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMaterial Name %s', [ID, Chunk^.Data.MatName^]);
        Strings.Add(Output);
      end;
      MAT_SHADING:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sShading value of %d', [ID, Chunk^.Data.MatShading^]);
        Strings.Add(Output);
      end;
      MAT_ACUBIC:
      begin
        Source.ReadChunkData(Chunk);
        with Chunk^.Data.MatAcubic^ do
        begin
          Output := Format('%sShade level of %d', [ID, ShadeLevel]);
          Strings.Add(Output);
          Output := Format('%sAntialias level of %d', [ID, AntiAlias]);
          Strings.Add(Output);
          Output := Format('%sFlags: %d', [ID, Flags]);
          Strings.Add(Output);
          Output := Format('%sMap Size of %d', [ID, MapSize]);
          Strings.Add(Output);
          Output := Format('%sFrame skip of %d', [ID, FrameInterval]);
          Strings.Add(Output);
        end;
      end;
      MAT_MAPNAME:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMap Name %s', [ID, Chunk^.Data.MatMapname^]);
        Strings.Add(Output);
      end;
      MAT_WIRESIZE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sWire frame Size of  %f', [ID, Chunk^.Data.MatWireSize^]);
        Strings.Add(Output);
      end;
      MAT_MAP_TILING:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMap Flags: ', [ID]);
        if (Chunk^.Data.MatMapTiling^ = 0) then
          Output := Output + ' NONE'
        else
        begin
          if (Chunk^.Data.MatMapTiling^ and TEX_DECAL) <> 0 then
            Output := Output + ' TEX_DECAL, ';
          if (Chunk^.Data.MatMapTiling^ and TEX_MIRROR) <> 0 then
            Output := Output + ' TEX_MIRROR, ';
          if (Chunk^.Data.MatMapTiling^ and TEX_UNUSED1) <> 0 then
            Output := Output + ' TEX_UNUSED1, ';
          if (Chunk^.Data.MatMapTiling^ and TEX_INVERT) <> 0 then
            Output := Output + ' TEX_INVERT, ';
          if (Chunk^.Data.MatMapTiling^ and TEX_NOWRAP) <> 0 then
            Output := Output + ' TEX_NOWRAP, ';
          if (Chunk^.Data.MatMapTiling^ and TEX_SAT) <> 0 then
            Output := Output + ' TEX_SAT, ';
          if (Chunk^.Data.MatMapTiling^ and TEX_ALPHA_SOURCE) <> 0 then
            Output := Output + ' TEX_ALPHA_SOURCE, ';
          if (Chunk^.Data.MatMapTiling^ and TEX_TINT) <> 0 then
            Output := Output + ' TEX_TINT, ';
          if (Chunk^.Data.MatMapTiling^ and TEX_DONT_USE_ALPHA) <> 0 then
            Output := Output + ' TEX_DONT_USE_ALPHA, ';
          if (Chunk^.Data.MatMapTiling^ and TEX_RGB_TINT) <> 0 then
            Output := Output + ' TEX_RGB_TINT, ';
          Delete(Output, Length(Output) - 1, 2); // take the last comma out
        end;
        Strings.Add(Output);
      end;
      MAT_MAP_COL1:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sColor R: %d, ', [ID, Chunk^.Data.MatMapCol1^.Red]);
        Output := Output + Format(' G: %d, ', [Chunk^.Data.MatMapCol1^.Green]);
        Output := Output + Format(' B: %d', [Chunk^.Data.MatMapCol1^.Blue]);
        Strings.Add(Output);
      end;
      MAT_MAP_COL2:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sColor R: %d, ', [ID, Chunk^.Data.MatMapCol2^.Red]);
        Output := Output + Format(' G: %d, ', [Chunk^.Data.MatMapCol2^.Green]);
        Output := Output + Format(' B: %d', [Chunk^.Data.MatMapCol2^.Blue]);
        Strings.Add(Output);
      end;
      MAT_MAP_RCOL:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sColor R: %d, ', [ID, Chunk^.Data.MatMapRCol^.Red]);
        Output := Output + Format(' G: %d, ', [Chunk^.Data.MatMapRCol^.Green]);
        Output := Output + Format(' B: %d', [Chunk^.Data.MatMapRCol^.Blue]);
        Strings.Add(Output);
      end;
      MAT_MAP_GCOL:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sColor R: %d, ', [ID, Chunk^.Data.MatMapGCol^.Red]);
        Output := Output + Format(' G: %d, ', [Chunk^.Data.MatMapGCol^.Green]);
        Output := Output + Format(' B: %d', [Chunk^.Data.MatMapGCol^.Blue]);
        Strings.Add(Output);
      end;
      MAT_MAP_BCOL:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sColor R: %d, ', [ID, Chunk^.Data.MatMapBCol^.Red]);
        Output := Output + Format(' G: %d, ', [Chunk^.Data.MatMapBCol^.Green]);
        Output := Output + Format(' B: %d', [Chunk^.Data.MatMapBCol^.Blue]);
        Strings.Add(Output);
      end;
      MAT_MAP_TEXBLUR:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMap bluring of  %f', [ID, Chunk^.Data.MatMapTexblur^]);
        Strings.Add(Output);
      end;
      MAT_MAP_USCALE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMap U scale of  %f', [ID, Chunk^.Data.MatMapUScale^]);
        Strings.Add(Output);
      end;
      MAT_MAP_VSCALE:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMap V scale of  %f', [ID, Chunk^.Data.MatMapVScale^]);
        Strings.Add(Output);
      end;
      MAT_MAP_UOFFSET:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMap U offset of  %f', [ID, Chunk^.Data.MatMapUOffset^]);
        Strings.Add(Output);
      end;
      MAT_MAP_VOFFSET:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMap V offset of  %f', [ID, Chunk^.Data.MatMapVOffset^]);
        Strings.Add(Output);
      end;
      MAT_MAP_ANG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMap rotation angle of  %f', [ID, Chunk^.Data.MatMapAng^]);
        Strings.Add(Output);
      end;
      MAT_BUMP_PERCENT:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sPercentage of %d%%', [ID, Chunk^.Data.MatBumpPercent^]);
        Strings.Add(Output);
      end;
      KFHDR:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sRevision level of $%x', [ID, Chunk^.Data.KFHdr^.Revision]);
        Strings.Add(Output);
        Output := Format('%sFilename %s', [ID, Chunk^.Data.KFHdr^.FileName]);
        Strings.Add(Output);
        Output := Format('%sAnimation length of %d',
          [ID, Chunk^.Data.KFHdr^.AnimLength]);
        Strings.Add(Output);
      end;
      KFSEG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sSegment starts at %d and ends at %d',
          [ID, Chunk^.Data.KFSeg^.First, Chunk^.Data.KFSeg^.Last]);
        Strings.Add(Output);
      end;
      KFCURTIME:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sCurrent frame is %d', [ID, Chunk^.Data.KFCurtime^]);
        Strings.Add(Output);
      end;
      NODE_ID:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sNode ID: %d', [ID, Chunk^.Data.KFID^]);
        Strings.Add(Output);
      end;
      NODE_HDR:
      begin
        Source.ReadChunkData(Chunk);
        Strings.Add(Format('%sObject Name: %s',
          [ID, Chunk^.Data.NodeHdr^.ObjNameStr]));
        //--- Flags 1
        Strings.Add(Format('%sFlags 1: $%x', [ID, Chunk^.Data.NodeHdr^.Flags1]));
        if DumpLevel = dlMaximumDump then
          with Chunk^.Data.NodeHdr^ do
          begin
            if (Flags1 and NODE_RENDOB_HIDE) <> 0 then
              Strings.Add(Format('%sNODE_RENDOB_HIDE', [ID]));
            if (Flags1 and NODE_OFF) <> 0 then
              Strings.Add(Format('%sNODE_OFF', [ID]));
            if (Flags1 and ATKEY1) <> 0 then
              Strings.Add(Format('%sATKEY1', [ID]));
            if (Flags1 and ATKEY2) <> 0 then
              Strings.Add(Format('%sATKEY2', [ID]));
            if (Flags1 and ATKEY3) <> 0 then
              Strings.Add(Format('%sATKEY3', [ID]));
            if (Flags1 and ATKEY4) <> 0 then
              Strings.Add(Format('%sATKEY4', [ID]));
            if (Flags1 and ATKEY5) <> 0 then
              Strings.Add(Format('%sATKEY5', [ID]));
            if (Flags1 and ATKEYFLAGS) <> 0 then
              Strings.Add(Format('%sATKEYFLAGS', [ID]));
            if (Flags1 and MARK_NODE) <> 0 then
              Strings.Add(Format('%sMARK_NODE', [ID]));
            if (Flags1 and DISABLE_NODE) <> 0 then
              Strings.Add(Format('%sDISABLE_NODE', [ID]));
            if (Flags1 and HIDE_NODE) <> 0 then
              Strings.Add(Format('%sHIDE_NODE', [ID]));
            if (Flags1 and FAST_NODE) <> 0 then
              Strings.Add(Format('%sFAST_NODE', [ID]));
            if (Flags1 and PRIMARY_NODE) <> 0 then
              Strings.Add(Format('%sPRIMARY_NODE', [ID]));
            if (Flags1 and NODE_CALC_PATH) <> 0 then
              Strings.Add(Format('%sNODE_CALC_PATH', [ID]));
          end;

        //--- Flags 2
        Strings.Add(Format('%sFlags 2: $%x', [ID, Chunk^.Data.NodeHdr^.Flags2]));
        if DumpLevel = dlMaximumDump then
          with Chunk^.Data.NodeHdr^ do
          begin
            if (Flags2 and NODE_HAS_PATH) <> 0 then
              Strings.Add(Format('%sNODE_HAS_PATH', [ID]));
            if (Flags2 and NODE_AUTO_SMOOTH) <> 0 then
              Strings.Add(Format('%sNODE_AUTO_SMOOTH', [ID]));
            if (Flags2 and NODE_FROZEN) <> 0 then
              Strings.Add(Format('%sNODE_FROZEN', [ID]));
            if (Flags2 and NODE_ANI_HIDDEN) <> 0 then
              Strings.Add(Format('%sNODE_ANI_HIDDEN', [ID]));
            if (Flags2 and NODE_MOTION_BLUR) <> 0 then
              Strings.Add(Format('%sNODE_MOTION_BLUR', [ID]));
            if (Flags2 and NODE_BLUR_BRANCH) <> 0 then
              Strings.Add(Format('%sNODE_BLUR_BRANCH', [ID]));
            if (Flags2 and NODE_MORPH_MTL) <> 0 then
              Strings.Add(Format('%sNODE_MORPH_MTL', [ID]));
            if (Flags2 and NODE_MORPH_OB) <> 0 then
              Strings.Add(Format('%sNODE_MORPH_OB', [ID]));
          end;

        if Chunk^.Data.NodeHdr^.ParentIndex = -1 then
          Strings.Add(Format('%sNo Parent', [ID]))
        else
          Strings.Add(Format('%sParent %d', [ID, Chunk^.Data.NodeHdr^.ParentIndex]));
      end;
      INSTANCE_NAME:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sInstance Name: %s', [ID, Chunk^.Data.InstanceName^]);
        Strings.Add(Output);
      end;
      PARENT_NAME:
      begin
        Source.ReadChunkData(Chunk);
        if Chunk^.Data.InstanceName = nil then
          Strings.Add(Format('%sNo Parent', [ID]))
        else
          Strings.Add(Format('%sParent Name: %s', [ID, Chunk^.Data.InstanceName^]));
      end;
      PIVOT:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sPivot at  %f,  %f,  %f',
          [ID, Chunk^.Data.Pivot^.X, Chunk^.Data.Pivot^.Y,
          Chunk^.Data.Pivot^.Z]);
        Strings.Add(Output);
      end;
      BOUNDBOX:
        if Assigned(Chunk^.Data.Dummy) then
        begin
          Output := Format('%sMinimum at  %f,  %f,  %f',
            [ID, Chunk^.Data.BoundBox^.Min.X, Chunk^.Data.BoundBox^.Min.Y,
            Chunk^.Data.BoundBox^.Min.Z]);
          Strings.Add(Output);
          Output := Format('%sMaximum at  %f,  %f,  %f',
            [ID, Chunk^.Data.BoundBox^.Max.X, Chunk^.Data.BoundBox^.Max.Y,
            Chunk^.Data.BoundBox^.Max.Z]);
          Strings.Add(Output);
        end;
      MORPH_SMOOTH:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%sMorph Smoothing Angle of  %f',
          [ID, Chunk^.Data.MorphSmooth^]);
        Strings.Add(Output);
      end;
      POS_TRACK_TAG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Keys, Flags: $%x',
          [ID, Chunk^.Data.PosTrackTag^.TrackHdr.KeyCount,
          Chunk^.Data.PosTrackTag^.TrackHdr.Flags]);
        Strings.Add(Output);
        for I := 0 to Chunk^.Data.PosTrackTag^.TrackHdr.KeyCount - 1 do
        begin
          DumpKeyHeader(Strings, Chunk^.Data.PosTrackTag^.KeyHdrList^[I],
            IndentLevel + 1);
          Output := Format('%sObject at  %f,  %f,  %f',
            [ID, Chunk^.Data.PosTrackTag^.PositionList^[I].X,
            Chunk^.Data.PosTrackTag^.PositionList^[I].Y,
            Chunk^.Data.PosTrackTag^.PositionList^[I].Z]);
          Strings.Add(Output);
        end;
      end;
      ROT_TRACK_TAG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Keys, Flags: $%x',
          [ID, Chunk^.Data.RotTrackTag^.TrackHdr.KeyCount,
          Chunk^.Data.RotTrackTag^.TrackHdr.Flags]);
        Strings.Add(Output);
        for I := 0 to Chunk^.Data.RotTrackTag^.TrackHdr.KeyCount - 1 do
        begin
          DumpKeyHeader(Strings, Chunk^.Data.RotTrackTag^.KeyHdrList^[I],
            IndentLevel + 1);
          Output := Format('%sRotation of  %f',
            [ID, Chunk^.Data.RotTrackTag^.RotationList^[I].Angle]);
          Strings.Add(Output);
          Output := Format('%sAxis of  %f,  %f,  %f',
            [ID, Chunk^.Data.RotTrackTag^.RotationList^[I].X,
            Chunk^.Data.RotTrackTag^.RotationList^[I].Y,
            Chunk^.Data.RotTrackTag^.RotationList^[I].Z]);
          Strings.Add(Output);
        end;
      end;
      SCL_TRACK_TAG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Keys, Flags: $%x',
          [ID, Chunk^.Data.ScaleTrackTag^.TrackHdr.KeyCount,
          Chunk^.Data.ScaleTrackTag^.TrackHdr.Flags]);
        Strings.Add(Output);
        for I := 0 to Chunk^.Data.ScaleTrackTag^.TrackHdr.KeyCount - 1 do
        begin
          DumpKeyHeader(Strings, Chunk^.Data.ScaleTrackTag^.KeyHdrList^[I],
            IndentLevel + 1);
          Output := Format('%sScale of  %f,  %f,  %f',
            [ID, Chunk^.Data.ScaleTrackTag^.ScaleList^[I].X,
            Chunk^.Data.ScaleTrackTag^.ScaleList^[I].Y,
            Chunk^.Data.ScaleTrackTag^.ScaleList^[I].Z]);
          Strings.Add(Output);
        end;
      end;
      FOV_TRACK_TAG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Keys, Flags: $%x',
          [ID, Chunk^.Data.FovTrackTag^.TrackHdr.KeyCount,
          Chunk^.Data.FovTrackTag^.TrackHdr.Flags]);
        Strings.Add(Output);
        for I := 0 to Chunk^.Data.FovTrackTag^.TrackHdr.KeyCount - 1 do
        begin
          DumpKeyHeader(Strings, Chunk^.Data.FovTrackTag^.KeyHdrList^[I],
            IndentLevel + 1);
          Output := Format('%sCamera FOV of  %f',
            [ID, Chunk^.Data.FovTrackTag^.FOVAngleList^[I]]);
          Strings.Add(Output);
        end;
      end;
      ROLL_TRACK_TAG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Keys, Flags: $%x',
          [ID, Chunk^.Data.RollTrackTag^.TrackHdr.KeyCount,
          Chunk^.Data.RollTrackTag^.TrackHdr.Flags]);
        Strings.Add(Output);
        for I := 0 to Chunk^.Data.RollTrackTag^.TrackHdr.KeyCount - 1 do
        begin
          DumpKeyHeader(Strings, Chunk^.Data.RollTrackTag^.KeyHdrList^[I],
            IndentLevel + 1);
          Output := Format('%sCamera Roll of  %f',
            [ID, Chunk^.Data.RollTrackTag^.RollAngleList^[I]]);
          Strings.Add(Output);
        end;
      end;
      COL_TRACK_TAG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Keys, Flags: $%x',
          [ID, Chunk^.Data.ColTrackTag^.TrackHdr.KeyCount,
          Chunk^.Data.ColTrackTag^.TrackHdr.Flags]);
        Strings.Add(Output);
        for I := 0 to Chunk^.Data.ColTrackTag^.TrackHdr.KeyCount - 1 do
        begin
          DumpKeyHeader(Strings, Chunk^.Data.ColTrackTag^.KeyHdrList^[I],
            IndentLevel + 1);
          Output := Format('%sColor R:  %f, ',
            [ID, Chunk^.Data.ColTrackTag^.ColorList^[I].B]);
          Output := Output + Format(' G:  %f, ',
            [Chunk^.Data.ColTrackTag^.ColorList^[I].G]);
          Output := Output + Format(' B:  %f',
            [Chunk^.Data.ColTrackTag^.ColorList^[I].B]);
          Strings.Add(Output);
        end;
      end;
      MORPH_TRACK_TAG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Keys, Flags: $%x',
          [ID, Chunk^.Data.MorphTrackTag^.TrackHdr.KeyCount,
          Chunk^.Data.MorphTrackTag^.TrackHdr.Flags]);
        Strings.Add(Output);
        for I := 0 to Chunk^.Data.MorphTrackTag^.TrackHdr.KeyCount - 1 do
        begin
          DumpKeyHeader(Strings, Chunk^.Data.MorphTrackTag^.KeyHdrList^[I],
            IndentLevel + 1);
          Output := Format('%sMorph to %s',
            [ID, Chunk^.Data.MorphTrackTag^.MorphList^[I]]);
          Strings.Add(Output);
        end;
      end;
      HOT_TRACK_TAG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Keys, Flags: $%x',
          [ID, Chunk^.Data.HotTrackTag^.TrackHdr.KeyCount,
          Chunk^.Data.HotTrackTag^.TrackHdr.Flags]);
        Strings.Add(Output);
        for I := 0 to Chunk^.Data.HotTrackTag^.TrackHdr.KeyCount - 1 do
        begin
          DumpKeyHeader(Strings, Chunk^.Data.HotTrackTag^.KeyHdrList^[I],
            IndentLevel + 1);
          Output := Format('%sHotspot angle of  %f',
            [ID, Chunk^.Data.HotTrackTag^.HotspotAngleList^[I]]);
          Strings.Add(Output);
        end;
      end;
      FALL_TRACK_TAG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Keys, Flags: $%x',
          [ID, Chunk^.Data.FallTrackTag^.TrackHdr.KeyCount,
          Chunk^.Data.FallTrackTag^.TrackHdr.Flags]);
        Strings.Add(Output);
        for I := 0 to Chunk^.Data.FallTrackTag^.TrackHdr.KeyCount - 1 do
        begin
          DumpKeyHeader(Strings, Chunk^.Data.FallTrackTag^.KeyHdrList^[I],
            IndentLevel + 1);
          Output := Format('%sFalloff Angle of  %f',
            [ID, Chunk^.Data.FallTrackTag^.FalloffAngleList^[I]]);
          Strings.Add(Output);
        end;
      end;
      HIDE_TRACK_TAG:
      begin
        Source.ReadChunkData(Chunk);
        Output := Format('%s%d Keys, Flags: $%x',
          [ID, Chunk^.Data.HideTrackTag^.TrackHdr.KeyCount,
          Chunk^.Data.HideTrackTag^.TrackHdr.Flags]);
        Strings.Add(Output);
        for I := 0 to Chunk^.Data.HideTrackTag^.TrackHdr.KeyCount - 1 do
          DumpKeyHeader(Strings, Chunk^.Data.HideTrackTag^.KeyHdrList^[I],
            IndentLevel + 1);
      end;
    end; // end case
  end;

  Child := Chunk^.Children;

  while Assigned(Child) do
  begin
    DumpChunk(Source, Strings, Child, IndentLevel + 1, DumpLevel);
    Child := Child^.Sibling;
  end;
end;

//----------------- common support function ---------------------------------------------------------------------------

procedure AddChild(Parent, Child: PChunk3DS);

// AddChild puts the chunk at the end of the Sibling list

var
  Current: PChunk3DS;

begin
  if Parent^.Children = nil then
    Parent^.Children := Child
  else
  begin
    Current := Parent^.Children;
    while Assigned(Current^.Sibling) do
      Current := Current^.Sibling;
    Current^.Sibling := Child;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure AddChildOrdered(Parent, Child: PChunk3DS);

// AddChildOrdered will insert the child among its siblings depending
// on the order of occurance set by the 3DS file.

var
  Current, Prev: PChunk3DS;
  ChildValue: integer;

begin
  ChildValue := GetChunkValue(Child^.Tag);

  if Parent^.Children = nil then
    Parent^.Children := Child
  else
  begin
    Current := Parent^.Children;
    Prev := nil;
    while Assigned(Current^.Sibling) do
    begin
      if ChildValue > GetChunkValue(Current^.Tag) then
        break;
      Prev := Current;
      Current := Current^.Sibling;
    end;

    if ChildValue > GetChunkValue(Current^.Tag) then
    begin
      Child^.Sibling := Current;
      if Assigned(Prev) then
        Prev^.Sibling := Child
      else
        Parent^.Children := Child;
    end
    else
    begin
      Child^.Sibling := Current^.Sibling;
      Current^.Sibling := Child;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function FindChunk(Top: PChunk3DS; Tag: word): PChunk3DS;

  // searchs the given top Chunk and its children for a match

var
  Child, Match: PChunk3DS;

begin
  Result := nil;
  if Assigned(Top) then
    if Top^.Tag = Tag then
      Result := Top
    else
    begin
      Child := Top^.Children;
      while Assigned(Child) do
      begin
        Match := FindChunk(Child, Tag);
        if Assigned(Match) then
        begin
          Result := Match;
          Break;
        end;
        Child := Child^.Sibling;
      end;
    end;
end;

//---------------------------------------------------------------------------------------------------------------------

function FindNextChunk(Local: PChunk3DS; Tag: word): PChunk3DS;

var
  Current: PChunk3DS;

begin
  Result := nil;
  Current := Local;
  while Assigned(Current) and (Result = nil) do
  begin
    if Current^.Tag = Tag then
      Result := Current;
    Current := Current^.Sibling;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure FreeChunkData(var Chunk: PChunk3DS);

begin
  if Assigned(Chunk^.Data.Dummy) then
  begin
    // do only care about Chunk^.Data fields that contain other pointers
    // that need to be free
    case Chunk^.Tag of
      MAT_SXP_TEXT_DATA,
      MAT_SXP_TEXT2_DATA,
      MAT_SXP_OPAC_DATA,
      MAT_SXP_BUMP_DATA,
      MAT_SXP_SPEC_DATA,
      MAT_SXP_SHIN_DATA,
      MAT_SXP_SELFI_DATA,
      MAT_SXP_TEXT_MASKDATA,
      MAT_SXP_TEXT2_MASKDATA,
      MAT_SXP_OPAC_MASKDATA,
      MAT_SXP_BUMP_MASKDATA,
      MAT_SXP_SPEC_MASKDATA,
      MAT_SXP_SHIN_MASKDATA,
      MAT_SXP_SELFI_MASKDATA,
      MAT_SXP_REFL_MASKDATA,
      PROC_DATA:
        FreeMem(Chunk^.Data.IpasData^.Data);
      POINT_ARRAY:
        FreeMem(Chunk^.Data.PointArray^.PointList);
      POINT_FLAG_ARRAY:
        FreeMem(Chunk^.Data.PointFlagArray^.FlagList);
      FACE_ARRAY:
        Freemem(Chunk^.Data.FaceArray^.FaceList);
      MSH_MAT_GROUP:
      begin
        Dispose(Chunk^.Data.MshMatGroup);
        Chunk^.Data.MshMatGroup := nil;
      end;
      SMOOTH_GROUP:
        FreeMem(Chunk^.Data.SmoothGroup^.GroupList);
      TEX_VERTS:
        FreeMem(Chunk^.Data.TexVerts^.TextVertList);
      XDATA_ENTRY:
        FreeMem(Chunk^.Data.XDataEntry^.Data);
      POS_TRACK_TAG:
      begin
        FreeMem(Chunk^.Data.PosTrackTag^.KeyHdrList);
        Freemem(Chunk^.Data.PosTrackTag^.PositionList);
      end;
      COL_TRACK_TAG:
      begin
        FreeMem(Chunk^.Data.ColTrackTag^.KeyHdrList);
        FreeMem(Chunk^.Data.ColTrackTag^.ColorList);
      end;
      ROT_TRACK_TAG:
      begin
        FreeMem(Chunk^.Data.RotTrackTag^.KeyHdrList);
        FreeMem(Chunk^.Data.RotTrackTag^.RotationList);
      end;
      SCL_TRACK_TAG:
      begin
        FreeMem(Chunk^.Data.ScaleTrackTag^.KeyHdrList);
        FreeMem(Chunk^.Data.ScaleTrackTag^.ScaleList);
      end;
      MORPH_TRACK_TAG:
      begin
        FreeMem(Chunk^.Data.MorphTrackTag^.KeyHdrList);
        FreeMem(Chunk^.Data.MorphTrackTag^.MorphList);
      end;
      FOV_TRACK_TAG:
      begin
        FreeMem(Chunk^.Data.FovTrackTag^.KeyHdrList);
        FreeMem(Chunk^.Data.FovTrackTag^.FOVAngleList);
      end;
      ROLL_TRACK_TAG:
      begin
        FreeMem(Chunk^.Data.RollTrackTag^.KeyHdrList);
        FreeMem(Chunk^.Data.RollTrackTag^.RollAngleList);
      end;
      HOT_TRACK_TAG:
      begin
        FreeMem(Chunk^.Data.HotTrackTag^.KeyHdrList);
        FreeMem(Chunk^.Data.HotTrackTag^.HotspotAngleList);
      end;
      FALL_TRACK_TAG:
      begin
        FreeMem(Chunk^.Data.FallTrackTag^.KeyHdrList);
        FreeMem(Chunk^.Data.FallTrackTag^.FalloffAngleList);
      end;
      KFHDR:
      begin
        Dispose(Chunk^.Data.KFHdr);
        Chunk^.Data.KFHdr := nil;
      end;
      NODE_HDR:
      begin
        Dispose(Chunk^.Data.NodeHdr);
        Chunk^.Data.NodeHdr := nil;
      end;
      HIDE_TRACK_TAG:
        FreeMem(Chunk^.Data.HideTrackTag^.KeyHdrList);
    end; // case end
    // finally free the data chunk
    FreeMem(Chunk^.Data.Dummy);
    Chunk^.Data.Dummy := nil;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure InitChunk(var Chunk: PChunk3DS);

// initializes and allocates memory for a chunk

begin
  New(Chunk);
  if Chunk = nil then
    ShowError(strError3DS_NO_MEM);

  // set default values
  with Chunk^ do
  begin
    Tag := NULL_CHUNK;
    Size := 0;
    Position := 0;
    Data.Dummy := nil;
    Sibling := nil;
    Children := nil;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure InitChunkList(var List: PChunklist3DS; Count: integer);

begin
  if List = nil then
  begin
    List := AllocMem(SizeOf(TChunklist3DS));
    if List = nil then
      ShowError(strError3DS_NO_MEM);
  end;

  List^.Count := Count;

  if Count > 0 then
  begin
    List^.List := AllocMem(Count * SizeOf(TChunkListEntry3DS));
    if List^.List = nil then
      ShowError(strError3DS_NO_MEM);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function PutGenericNode(TagID: word; ParentChunk: PChunk3DS): PChunk3DS;

  // put a tag into database as a child of ParentChunk

begin
  InitChunk(Result);
  Result^.Tag := TagID;
  AddChildOrdered(ParentChunk, Result);
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ReleaseChunk(var Chunk: PChunk3DS);

var
  Sibling: PChunk3DS;

begin
  // free memory associated with chunk and substructure
  while Assigned(Chunk) do
  begin
    Sibling := Chunk^.Sibling;
    ReleaseChunk(Chunk^.Children);
    FreeChunkData(Chunk);
    FreeMem(Chunk);
    Chunk := Sibling;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ReleaseChunkList(var List: PChunkList3DS);

var
  I: integer;

begin
  if Assigned(List) then
  begin
    // tell the string management that we don't need these strings any longer
    for I := 0 to List^.Count - 1 do
      List^.List^[I].NameStr := '';
    if Assigned(List^.List) then
      FreeMem(List^.List);
    FreeMem(List);
    List := nil;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function CopyChunk(Chunk: PChunk3DS): PChunk3DS;

  // copies the structure of Chunk to Result, assigned data of Chunk will not be copied, but
  // moved to Result (actually references will be moved)

var
  ChildIn: PChunk3DS;
  ChildOut: ^PChunk3DS;

begin
  if Chunk = nil then
    ShowError(strERROR3DS_INVALID_ARG);

  InitChunk(Result);
  with Result^ do
  begin
    Tag := Chunk^.Tag;
    Size := Chunk^.Size;
    Position := Chunk^.Position;

    if Assigned(Chunk^.Data.Dummy) then
    begin
      Data.Dummy := Chunk^.Data.Dummy;
      Chunk^.Data.Dummy := nil;
    end;

    ChildIn := Chunk^.Children;
    ChildOut := @Children;
    while Assigned(ChildIn) do
    begin
      ChildOut^ := CopyChunk(ChildIn);
      ChildIn := ChildIn^.Sibling;
      ChildOut := @ChildOut^.Sibling;
    end;
  end;
end;

//----------------- list update routines ------------------------------------------------------------------------------

procedure UpdateMatEntryList(const Source: TFile3DS; var DB: TDatabase3DS);

var
  Parent, MatName, MatEntry: PChunk3DS;
  I, MatCount: integer;

begin
  if DB.MatlistDirty then
  begin
    ReleaseChunkList(DB.MatList);

    Parent := FindChunk(DB.TopChunk, MDATA);
    if Parent = nil then
      Parent := FindChunk(DB.TopChunk, MLIBMAGIC);

    MatCount := 0;
    if Assigned(Parent) then
    begin
      MatEntry := FindChunk(Parent, MAT_ENTRY);
      while Assigned(MatEntry) do
      begin
        MatEntry := FindNextChunk(MatEntry^.Sibling, MAT_ENTRY);
        Inc(MatCount);
      end;
    end;

    InitChunkList(DB.MatList, MatCount);
    if Parent = nil then
      Exit;

    I := 0;
    MatEntry := FindChunk(Parent, MAT_ENTRY);
    while Assigned(MatEntry) do
    begin
      MatName := FindChunk(MatEntry, MAT_NAME);
      Source.ReadChunkData(MatName);
      DB.MatList^.List^[I].Chunk := MatEntry;
      DB.MatList^.List^[I].NameStr := string(MatName^.Data.MatName);
      MatEntry := FindNextChunk(MatEntry^.Sibling, MAT_ENTRY);
      Inc(I);
    end;
    DB.MatlistDirty := False;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure UpdateNamedObjectList(Source: TFile3DS; var DB: TDatabase3DS);

var
  MDataChunk, Current: PChunk3DS;
  I: integer;

begin
  if DB.ObjListDirty then
  begin
    ReleaseChunkList(DB.ObjList);

    MDataChunk := FindChunk(DB.TopChunk, MDATA);

    I := 0;
    if Assigned(MDataChunk) then
    begin
      Current := FindChunk(MDataChunk, NAMED_OBJECT);
      while Assigned(Current) do
      begin
        Inc(I);
        Current := FindNextChunk(Current^.Sibling, NAMED_OBJECT);
      end;
    end;

    InitChunkList(DB.ObjList, I);
    if MDataChunk = nil then
      Exit;

    I := 0;
    Current := FindChunk(MDataChunk, NAMED_OBJECT);
    while Assigned(Current) do
    begin
      Source.ReadChunkData(Current);
      DB.ObjList^.List^[I].Chunk := Current;
      DB.ObjList^.List^[I].NameStr := string(Current^.Data.NamedObject);
      Current := FindNextChunk(Current^.Sibling, NAMED_OBJECT);
      Inc(I);
    end;
    DB.ObjListDirty := False;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure UpdateNodeTagList(Source: TFile3DS; var DB: TDatabase3DS);

var
  KFDataChunk, Chunk, Current: PChunk3DS;
  I: integer;

begin
  if DB.NodeListDirty then
  begin
    ReleaseChunkList(DB.NodeList);
    KFDataChunk := FindChunk(DB.TopChunk, KFDATA);

    I := 0;
    // if there is a keyframe section then count the number of node tags
    if Assigned(KFDataChunk) then
    begin
      Current := KFDataChunk^.Children;
      while Assigned(Current) do
      begin
        case Current^.Tag of
          AMBIENT_NODE_TAG,
          OBJECT_NODE_TAG,
          CAMERA_NODE_TAG,
          TARGET_NODE_TAG,
          LIGHT_NODE_TAG,
          L_TARGET_NODE_TAG,
          SPOTLIGHT_NODE_TAG:
            Inc(I);
        end;
        Current := Current^.Sibling;
      end;
    end;

    InitChunkList(DB.NodeList, I);
    if I = 0 then
      Exit;

    I := 0;
    Current := KFDataChunk^.Children;
    while Assigned(Current) do
    begin
      case Current^.Tag of
        AMBIENT_NODE_TAG,
        OBJECT_NODE_TAG,
        CAMERA_NODE_TAG,
        TARGET_NODE_TAG,
        LIGHT_NODE_TAG,
        L_TARGET_NODE_TAG,
        SPOTLIGHT_NODE_TAG:
        begin
          Chunk := FindNextChunk(Current^.Children, NODE_HDR);
          if Assigned(Chunk) then
          begin
            Source.ReadChunkData(Chunk);
            DB.NodeList^.List^[I].Chunk := Current;
            DB.NodeList^.List^[I].NameStr := Chunk^.Data.NodeHdr^.ObjNameStr;
            FreeChunkData(Chunk);
          end;

          // Object tags may have an instance name as well, which gets appended to
          // the object name with a "." seperator
          if Current^.Tag = OBJECT_NODE_TAG then
          begin
            Chunk := FindNextChunk(Current^.Children, INSTANCE_NAME);
            if Assigned(Chunk) then
            begin
              Source.ReadChunkData(Chunk);
              DB.NodeList^.List^[I].NameStr :=
                DB.NodeList^.List^[I].NameStr + '.' + string(Chunk^.Data.InstanceName);
              FreeChunkData(Chunk);
            end;
          end;
          Inc(I); // Increment index counter
        end;
      end;
      Current := Current^.Sibling;
    end;

    DB.NodeListDirty := False;
  end;
end;

//----------------- other support function ----------------------------------------------------------------------------

function GetGenericNodeCount(const Source: TFile3DS; var DB: TDatabase3DS;
  Tag: word): integer;

var
  I: integer;

begin
  UpdateNodeTagList(Source, DB);

  Result := 0;
  for I := 0 to DB.NodeList^.Count - 1 do
    if DB.NodeList^.List^[I].Chunk^.Tag = Tag then
      Inc(Result);
end;

//---------------------------------------------------------------------------------------------------------------------

procedure GetGenericNodeNameList(const Source: TFile3DS; var DB: TDatabase3DS;
  TagID: word; List: TStringList);

var
  I: cardinal;

begin
  UpdateNodeTagList(Source, DB);
  List.Clear;
  for I := 0 to DB.NodeList^.Count - 1 do
    if DB.NodeList^.List^[I].Chunk^.Tag = TagID then
      List.Add(DB.NodeList^.List^[I].NameStr);
end;

//---------------------------------------------------------------------------------------------------------------------

function FindNamedAndTaggedChunk(const Source: TFile3DS; var DB: TDatabase3DS;
  const Name: string; TagID: word): PChunk3DS;

  // Look through the keyframer stuff and find named chunk of the tag type TagID.
  // Has to be a chunk that has a node header: CAMERA_NODE, LIGHT_NODE, , .

var
  KfChunk, NodeHdrChunk: PChunk3DS;

begin
  // find Keyframe Chunk
  KfChunk := FindChunk(DB.TopChunk, KFDATA);

  // look for the target tag
  Result := FindChunk(KfChunk, TagID);
  while Assigned(Result) do
  begin
    NodeHdrChunk := FindNextChunk(Result^.Children, NODE_HDR);
    if Assigned(NodeHdrChunk) then
    begin
      Source.ReadChunkData(NodeHdrChunk);
      // match name, set pointer (case sensitive comparation!)
      if CompareStr(Name, NodeHdrChunk^.Data.NodeHdr^.ObjNameStr) = 0 then
      begin
        FreeChunkData(NodeHdrChunk);
        Break;
      end;
      FreeChunkData(NodeHdrChunk);
    end;
    Result := FindNextChunk(Result^.Sibling, TagID);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function FindNodeTagByIndexAndType(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: cardinal; AType: word): PChunk3DS;

var
  I, Count: cardinal;

begin
  Result := nil;
  Count := 0;
  UpdateNodeTagList(Source, DB);
  for I := 0 to DB.NodeList^.Count - 1 do
    if DB.NodeList^.List^[I].Chunk^.Tag = AType then
    begin
      if Count = Index then
      begin
        Result := DB.NodeList^.List^[I].Chunk;
        Break;
      end;
      Inc(Count);
    end;
end;

//---------------------------------------------------------------------------------------------------------------------

function FindNodeTagByNameAndType(const Source: TFile3DS; DB: TDatabase3DS;
  const Name: string; AType: word): PChunk3DS;

var
  I: integer;

begin
  Result := nil;
  UpdateNodeTagList(Source, DB);
  for I := 0 to DB.NodeList^.Count - 1 do
    if (DB.NodeList^.List^[I].Chunk^.Tag = AType) and
      (CompareStr(Name, DB.NodeList^.List^[I].NameStr) = 0) then
    begin
      Result := DB.NodeList^.List^[I].Chunk;
      Exit;
    end;
end;

//----------------- material handling ---------------------------------------------------------------------------------

function GetMaterialCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;

begin
  UpdateMatEntryList(Source, DB);
  if DB.MatList = nil then
    Result := 0
  else
    Result := DB.MatList^.Count;
end;

//---------------------------------------------------------------------------------------------------------------------

function FindMatEntryByIndex(Source: TFile3DS; DB: TDatabase3DS;
  Index: integer): PChunk3DS;

begin
  if DB.TopChunk = nil then
    ShowError(strError3DS_INVALID_DATABASE);
  if (DB.TopChunk^.Tag <> MLIBMAGIC) and (DB.TopChunk^.Tag <> M3DMAGIC) and
    (DB.TopChunk^.Tag <> CMAGIC) then
    ShowError(strError3DS_WRONG_DATABASE);

  UpdateMatEntryList(Source, DB);
  if Index < DB.MatList^.Count then
    Result := DB.MatList^.List^[Index].Chunk
  else
    Result := nil;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure InitBitmap(var Map: TBitmap3DS);

begin
  FillChar(Map, SizeOf(Map), 0);
  with Map do
  begin
    UScale := 1;
    VScale := 1;
    Tint2.R := 1;
    Tint2.G := 1;
    Tint2.B := 1;
    RedTint.R := 1;
    GreenTint.G := 1;
    BlueTint.B := 1;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure InitMaterial(var Mat: TMaterial3DS);

begin
  FillChar(Mat, SizeOf(Mat), 0);
  with Mat do
  begin
    WireSize := 1;
    Shading := stPhong;
    Reflect.AutoMap.Size := 100;
    Reflect.AutoMap.nthFrame := 1;
    InitBitmap(Texture.Map);
    InitBitmap(Texture.Mask);
    InitBitmap(Texture2.Map);
    InitBitmap(Texture2.Mask);
    InitBitmap(Opacity.Map);
    InitBitmap(Opacity.Mask);
    InitBitmap(Reflect.Map);
    InitBitmap(Reflect.Mask);
    InitBitmap(Bump.Map);
    InitBitmap(Bump.Mask);
    InitBitmap(SpecMap.Map);
    InitBitmap(SpecMap.Mask);
    InitBitmap(ShinMap.Map);
    InitBitmap(ShinMap.Mask);
    InitBitmap(IllumMap.Map);
    InitBitmap(IllumMap.Mask);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ReleaseMaterial(Mat: PMaterial3DS);

begin
  if Assigned(Mat) then
  begin
    FreeMem(Mat^.Texture.Map.Data);
    FreeMem(Mat^.Texture.Mask.Data);
    FreeMem(Mat^.Texture2.Map.Data);
    FreeMem(Mat^.Texture2.Mask.Data);
    FreeMem(Mat^.Opacity.Map.Data);
    FreeMem(Mat^.Opacity.Mask.Data);
    FreeMem(Mat^.Reflect.Mask.Data);
    FreeMem(Mat^.Bump.Map.Data);
    FreeMem(Mat^.Bump.Mask.Data);
    FreeMem(Mat^.Specmap.Map.Data);
    FreeMem(Mat^.SpecMap.Mask.Data);
    FreeMem(Mat^.ShinMap.Map.Data);
    FreeMem(Mat^.ShinMap.Mask.Data);
    FreeMem(Mat^.IllumMap.Map.Data);
    FreeMem(Mat^.IllumMap.Mask.Data);
    Dispose(Mat);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function FindNamedObjectByIndex(Source: TFile3DS; DB: TDatabase3DS;
  AType: word; Index: integer): PChunk3DS;

  // searches the database for a named object by index position and object type
  // returns the NAMED_OBJECT chunk if found, nil otherwise

var
  Chunk: PChunk3DS;
  I, Count: integer;

begin
  UpdateNamedObjectList(Source, DB);

  Count := 0;
  Result := nil;
  for I := 0 to DB.ObjList^.Count - 1 do
  begin
    if AType = DL_SPOTLIGHT then
    begin
      Chunk := FindChunk(DB.ObjList^.List^[I].Chunk, N_DIRECT_LIGHT);
      if Assigned(Chunk) then
        Chunk := FindChunk(Chunk, AType);
    end
    else
      Chunk := FindChunk(DB.ObjList^.List^[I].Chunk, AType);

    if Assigned(Chunk) then
    begin
      if Count = Index then
      begin
        Result := DB.ObjList^.List^[I].Chunk;
        Break;
      end
      else
        Inc(Count);
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure DeleteChunk(var Chunk: PChunk3DS);

// returns a chunk to its untagged state state, but leaves it
// connected to any siblings it might have

begin
  if Assigned(Chunk) then
  begin
    // release any children
    if Assigned(Chunk^.Children) then
      ReleaseChunk(Chunk^.Children);
    // release any data
    if Assigned(Chunk^.Data.Dummy) then
      FreeChunkData(Chunk);
    // return to a semi-uninitialized state
    Chunk^.Tag := NULL_CHUNK;
    Chunk^.Size := 0;
    Chunk^.Position := 0;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function ReadPercentageChunk(Source: TFile3DS; Chunk: PChunk3DS): single;

var
  DataChunk: PChunk3DS;

begin
  DataChunk := FindChunk(Chunk, INT_PERCENTAGE);
  if Assigned(DataChunk) then
  begin
    Source.ReadChunkData(DataChunk);
    Result := DataChunk^.Data.IntPercentage^ / 100;
    FreeChunkData(DataChunk);
  end
  else
  begin
    DataChunk := FindChunk(Chunk, FLOAT_PERCENTAGE);
    if Assigned(DataChunk) then
    begin
      Source.ReadChunkData(DataChunk);
      Result := DataChunk^.Data.FloatPercentage^;
      FreeChunkData(DataChunk);
    end
    else
      Result := 0;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure GetBitmapChunk(const DataSource: TFile3DS; Chunk: PChunk3DS;
  var Bitmap: TBitmap3DS);

var
  Current: PChunk3DS;

begin
  Current := Chunk^.Children;
  while Assigned(Current) do
  begin
    with Bitmap do
    begin
      case Current^.Tag of
        INT_PERCENTAGE:
        begin
          DataSource.ReadChunkData(Current);
          Percent := Current^.Data.IntPercentage^ / 100;
          FreeChunkData(Current);
        end;
        FLOAT_PERCENTAGE:
        begin
          DataSource.ReadChunkData(Current);
          Percent := Current^.Data.FloatPercentage^;
          FreeChunkData(Current);
        end;
        MAT_MAPNAME:
        begin
          DataSource.ReadChunkData(Current);
          NameStr := StrPas(Current^.Data.MatMapname);
          FreeChunkData(Current);
        end;
        MAT_MAP_TILING:
        begin
          DataSource.ReadChunkData(Current);
          if (Current^.Data.MatMapTiling^ and TEX_DECAL) <> 0 then
            if (Current^.Data.MatMapTiling^ and TEX_NOWRAP) <> 0 then
              Tiling := ttDecal
            else
              Tiling := ttBoth
          else
            tiling := ttTile;
          IgnoreAlpha := (Current^.Data.MatMapTiling^ and TEX_DONT_USE_ALPHA) <> 0;
          if (Current^.Data.MatMapTiling^ and TEX_SAT) <> 0 then
            Filter := ftSummedArea
          else
            Filter := ftPyramidal;
          Mirror := (Current^.Data.MatMapTiling^ and TEX_MIRROR) <> 0;
          Negative := (Current^.Data.MatMapTiling^ and TEX_INVERT) <> 0;
          if (Current^.Data.MatMapTiling^ and TEX_TINT) <> 0 then
            if (Current^.Data.MatMapTiling^ and TEX_ALPHA_SOURCE) <> 0 then
              Source := ttAlphaTint
            else
              Source := ttRGBLumaTint
          else if (Current^.Data.MatMapTiling^ and TEX_RGB_TINT) <> 0 then
            Source := ttRGBTint
          else if (Current^.Data.MatMapTiling^ and TEX_ALPHA_SOURCE) <> 0 then
            Source := ttAlpha
          else
            Source := ttRGB;
          FreeChunkData(Current);
        end;
        MAT_MAP_USCALE:
        begin
          DataSource.ReadChunkData(Current);
          UScale := Current^.Data.MatMapUScale^;
          FreeChunkData(Current);
        end;
        MAT_MAP_VSCALE:
        begin
          DataSource.ReadChunkData(Current);
          VScale := Current^.Data.MatMapVScale^;
          FreeChunkData(Current);
        end;
        MAT_MAP_UOFFSET:
        begin
          DataSource.ReadChunkData(Current);
          UOffset := Current^.Data.MatMapUOffset^;
          FreeChunkData(Current);
        end;
        MAT_MAP_VOFFSET:
        begin
          DataSource.ReadChunkData(Current);
          VOffset := Current^.Data.MatMapVOffset^;
          FreeChunkData(Current);
        end;
        MAT_MAP_ANG:
        begin
          DataSource.ReadChunkData(Current);
          Rotation := Current^.Data.MatMapAng^;
          FreeChunkData(Current);
        end;
        MAT_BUMP_PERCENT:
          ; // value is really stored in TMaterial3DS structure
        MAT_MAP_COL1:
        begin
          DataSource.ReadChunkData(Current);
          Tint1.R := Current^.Data.MatMapCol1^.Red / 255;
          Tint1.G := Current^.Data.MatMapCol1^.Green / 255;
          Tint1.B := Current^.Data.MatMapCol1^.Blue / 255;
          FreeChunkData(Current);
        end;
        MAT_MAP_COL2:
        begin
          DataSource.ReadChunkData(Current);
          Tint2.R := Current^.Data.MatMapCol2^.Red / 255;
          Tint2.G := Current^.Data.MatMapCol2^.Green / 255;
          Tint2.B := Current^.Data.MatMapCol2^.Blue / 255;
          FreeChunkData(Current);
        end;
        MAT_MAP_RCOL:
        begin
          DataSource.ReadChunkData(Current);
          RedTint.R := Current^.Data.MatMapRCol^.Red / 255;
          RedTint.G := Current^.Data.MatMapRCol^.Green / 255;
          RedTint.B := Current^.Data.MatMapRCol^.Blue / 255;
          FreeChunkData(Current);
        end;
        MAT_MAP_GCOL:
        begin
          DataSource.ReadChunkData(Current);
          GreenTint.R := Current^.Data.MatMapGCol^.Red / 255;
          GreenTint.G := Current^.Data.MatMapGCol^.Green / 255;
          GreenTint.B := Current^.Data.MatMapGCol^.Blue / 255;
          FreeChunkData(Current);
        end;
        MAT_MAP_BCOL:
        begin
          DataSource.ReadChunkData(Current);
          BlueTint.R := Current^.Data.MatMapBCol^.Red / 255;
          BlueTint.G := Current^.Data.MatMapBCol^.Green / 255;
          BlueTint.B := Current^.Data.MatMapBCol^.Blue / 255;
          FreeChunkData(Current);
        end;
        MAT_MAP_TEXBLUR:
        begin
          DataSource.ReadChunkData(Current);
          Blur := Current^.Data.MatMapTexBlur^; // float percents
          FreeChunkData(Current);
        end;
      end; // case Current^.Tag of
      Current := Current^.Sibling;
    end; // with Bitmap do
  end; // while Assigned(Current) do
end;

//---------------------------------------------------------------------------------------------------------------------

function ReadMatEntryChunk(Source: TFile3DS; MatEntry: PChunk3DS): TMaterial3DS;

var
  Current, DataChunk, Color: PChunk3DS;
  MatColor: PFColor3DS;

begin
  if MatEntry^.Tag <> MAT_ENTRY then
    ShowError(strError3DS_INVALID_CHUNK);
  InitMaterial(Result);

  with Result do
  begin
    Current := MatEntry^.Children;
    while Assigned(Current) do
    begin
      if (Current^.Tag and $FF00) <> $8000 then // ignore xdata
        case Current^.Tag of
          MAT_NAME:
          begin
            Source.ReadChunkData(Current);
            NameStr := StrPas(Current^.Data.MatName);
            FreeChunkData(Current);
          end;
          MAT_AMBIENT,
          MAT_DIFFUSE,
          MAT_SPECULAR:
          begin
            case Current^.Tag of
              MAT_DIFFUSE:
                MatColor := @Diffuse;
              MAT_SPECULAR:
                MatColor := @Specular;
              else
                MatColor := @Ambient; // MAT_AMBIENT
            end;
            Color := FindChunk(Current, COLOR_24);
            if Assigned(color) then
            begin
              Source.ReadChunkData(Color);
              MatColor^.R := Color^.Data.Color24^.Red / 255;
              MatColor^.G := Color^.Data.Color24^.Green / 255;
              MatColor^.B := Color^.Data.Color24^.Blue / 255;
              FreeChunkData(Color);
            end;
            Color := FindChunk(Current, COLOR_F);
            if Assigned(Color) then
            begin
              Source.ReadChunkData(Color);
              MatColor^.R := Color^.Data.ColorF^.Red;
              MatColor^.G := Color^.Data.ColorF^.Green;
              MatColor^.B := Color^.Data.ColorF^.Blue;
              FreeChunkData(Color);
            end;
            Color := FindChunk(Current, LIN_COLOR_24);
            if Assigned(Color) then
            begin
              Source.ReadChunkData(Color);
              MatColor^.R := Color^.Data.LinColor24^.Red / 255;
              MatColor^.G := Color^.Data.LinColor24^.Green / 255;
              MatColor^.B := Color^.Data.LinColor24^.Blue / 255;
              FreeChunkData(Color);
            end;
          end;
          MAT_SHININESS:
            Shininess := ReadPercentageChunk(Source, Current);
          MAT_SHIN2PCT:
            ShinStrength := ReadPercentageChunk(Source, Current);
          MAT_SHIN3PCT:
            ; // just skip for now
          MAT_REFBLUR:
            Blur := ReadPercentageChunk(Source, Current);
          MAT_TRANSPARENCY:
            Transparency := ReadPercentageChunk(Source, Current);
          MAT_XPFALL:
            TransFalloff := ReadPercentageChunk(Source, Current);
          MAT_SELF_ILPCT:
            SelfIllumPct := ReadPercentageChunk(Source, Current);
          MAT_WIRE:
            Shading := stWire;
          MAT_WIREABS:
            UseWireAbs := True;
          MAT_XPFALLIN:
            Transparency := -Transparency;
          MAT_WIRESIZE:
          begin
            Source.ReadChunkData(Current);
            WireSize := Current^.Data.MatWireSize^;
            FreeChunkData(Current);
          end;
          MAT_USE_XPFALL:
            UseFall := True;
          MAT_USE_REFBLUR:
            Useblur := True;
          MAT_SELF_ILLUM:
            SelfIllum := True;
          MAT_TWO_SIDE:
            TwoSided := True;
          MAT_ADDITIVE:
            Additive := True;
          MAT_SHADING:
          begin
            Source.ReadChunkData(Current);
            Shading := TShadeType3DS(Current^.Data.MatShading^);
            FreeChunkData(Current);
          end;
          MAT_FACEMAP:
            FaceMap := True;
          MAT_PHONGSOFT:
            Soften := True;
          MAT_TEXMAP:
            GetBitmapChunk(Source, Current, Texture.Map);
          MAT_TEXMASK:
            GetBitmapChunk(Source, Current, Texture.Mask);
          MAT_TEX2MAP:
            GetBitmapChunk(Source, Current, Texture2.Map);
          MAT_TEX2MASK:
            GetBitmapChunk(Source, Current, Texture2.Mask);
          MAT_OPACMAP:
            GetBitmapChunk(Source, Current, Opacity.Map);
          MAT_OPACMASK:
            GetBitmapChunk(Source, Current, Opacity.Mask);
          MAT_REFLMAP:
            GetBitmapChunk(Source, Current, Reflect.Map);
          MAT_ACUBIC:
          begin
            Source.ReadChunkData(Current);
            Reflect.UseAuto := True;
            Reflect.AutoMap.FirstFrame :=
              (Current^.Data.MatAcubic^.Flags and ACubicFirst3DS) <> 0;
            Reflect.AutoMap.Flat :=
              (Current^.Data.MatAcubic^.Flags and ACubicFlat3DS) <> 0;
            Reflect.AutoMap.Size := Current^.Data.MatAcubic^.MapSize;
            Reflect.AutoMap.nthFrame := Current^.Data.MatAcubic^.FrameInterval;
            FreeChunkData(Current);
          end;
          MAT_REFLMASK:
            GetBitmapChunk(Source, Current, Reflect.Mask);
          MAT_BUMPMAP:
          begin
            GetBitmapChunk(Source, Current, Bump.Map);
            DataChunk := FindChunk(Current, MAT_BUMP_PERCENT);
            if Assigned(DataChunk) then
            begin
              Source.ReadChunkData(DataChunk);
              Bump.Map.Percent := DataChunk^.Data.MatBumpPercent^ / 100;
              FreeChunkData(DataChunk);
            end;
          end;
          MAT_BUMPMASK:
            GetBitmapChunk(Source, Current, Bump.Mask);
          MAT_SPECMAP:
            GetBitmapChunk(Source, Current, SpecMap.Map);
          MAT_SPECMASK:
            GetBitmapChunk(Source, Current, SpecMap.Mask);
          MAT_SHINMAP:
            GetBitmapChunk(Source, Current, ShinMap.Map);
          MAT_SHINMASK:
            GetBitmapChunk(Source, Current, Shinmap.Mask);
          MAT_SELFIMAP:
            GetBitmapChunk(Source, Current, IllumMap.Map);
          MAT_SELFIMASK:
            GetBitmapChunk(Source, Current, IllumMap.Mask);
          MAT_SXP_TEXT_DATA:
          begin
            Source.ReadChunkData(Current);
            Texture.Map.DataSize := Current^.Data.IpasData^.Size;
            Texture.Map.Data := Current^.Data.IpasData^.Data;
            // avoid releasing the data memory
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_TEXT_MASKDATA:
          begin
            Source.ReadChunkData(Current);
            Texture.Mask.DataSize := Current^.Data.IpasData^.Size;
            Texture.Mask.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_TEXT2_DATA:
          begin
            Source.ReadChunkData(Current);
            Texture2.Map.DataSize := Current^.Data.IpasData^.Size;
            Texture2.Map.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_TEXT2_MASKDATA:
          begin
            Source.ReadChunkData(Current);
            Texture2.Mask.DataSize := Current^.Data.IpasData^.Size;
            Texture2.Mask.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_OPAC_DATA:
          begin
            Source.ReadChunkData(Current);
            Opacity.Map.DataSize := Current^.Data.IpasData^.Size;
            Opacity.Map.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_OPAC_MASKDATA:
          begin
            Source.ReadChunkData(Current);
            Opacity.Mask.DataSize := Current^.Data.IpasData^.Size;
            Opacity.Mask.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_REFL_MASKDATA:
          begin
            Source.ReadChunkData(Current);
            Reflect.Mask.DataSize := Current^.Data.IpasData^.Size;
            Reflect.Mask.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_BUMP_DATA:
          begin
            Source.ReadChunkData(Current);
            Bump.Map.DataSize := Current^.Data.IpasData^.Size;
            Bump.Map.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_BUMP_MASKDATA:
          begin
            Source.ReadChunkData(Current);
            Bump.Mask.DataSize := Current^.Data.IpasData^.Size;
            Bump.Mask.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_SPEC_DATA:
          begin
            Source.ReadChunkData(Current);
            SpecMap.Map.DataSize := Current^.Data.IpasData^.Size;
            SpecMap.Map.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_SPEC_MASKDATA:
          begin
            Source.ReadChunkData(Current);
            Specmap.Mask.DataSize := Current^.Data.IpasData^.Size;
            Specmap.Mask.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_SHIN_DATA:
          begin
            Source.ReadChunkData(Current);
            ShinMap.Map.DataSize := Current^.Data.IpasData^.Size;
            ShinMap.Map.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_SHIN_MASKDATA:
          begin
            Source.ReadChunkData(Current);
            ShinMap.Mask.DataSize := Current^.Data.IpasData^.Size;
            ShinMap.Mask.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_SELFI_DATA:
          begin
            Source.ReadChunkData(Current);
            IllumMap.Map.DataSize := Current^.Data.IpasData^.Size;
            IllumMap.Map.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_SXP_SELFI_MASKDATA:
          begin
            Source.ReadChunkData(Current);
            IllumMap.Mask.DataSize := Current^.Data.IpasData^.Size;
            IllumMap.Mask.Data := Current^.Data.IpasData^.Data;
            Current^.Data.IpasData^.Data := nil;
            FreeChunkData(Current);
          end;
          MAT_DECAL:
            ; // don't know what do to with it
          else
            ShowError(strError3DS_INVALID_CHUNK)
        end;
      Current := Current^.Sibling;
    end; // while Assigned(Current) do
  end; // with Result do
end;

//---------------------------------------------------------------------------------------------------------------------

function GetChunkValue(Tag: word): integer;

  // Computes a chunk weighting used to determine proper chunk order,
  // higher values appear earlier in the parent than lower values

begin
  // only chunks where an explicit order matters are handled
  Result := 0;

  case Tag of
    NULL_CHUNK:
      Inc(Result); // These should just be ignored
    SMAGIC:
      Inc(Result, 2);
    LMAGIC:
      Inc(Result, 3);
    M3DMAGIC:
      Inc(Result, 4);
    M3D_VERSION:
      Inc(Result, 5);
    MDATA:
      Inc(Result, 6);
    KFDATA:
      Inc(Result, 7);
    COLOR_24:
      Inc(Result, 8);
    LIN_COLOR_24:
      Inc(Result, 9);
    MESH_VERSION:
      Inc(Result, 10);
    MAT_ENTRY:
      Inc(Result, 11);
    KFHDR:
      Inc(Result, 12);
    MASTER_SCALE:
      Inc(Result, 13);
    VIEWPORT_LAYOUT:
      Inc(Result, 14);
    LO_SHADOW_BIAS:
      Inc(Result, 15);
    SHADOW_MAP_SIZE:
      Inc(Result, 16);
    SHADOW_FILTER:
      Inc(Result, 17);
    RAY_BIAS:
      Inc(Result, 18);
    O_CONSTS:
      Inc(Result, 19);
    AMBIENT_LIGHT:
      Inc(Result, 20);
    SOLID_BGND:
      Inc(Result, 21);
    BIT_MAP:
      Inc(Result, 22);
    V_GRADIENT:
      Inc(Result, 23);
    USE_BIT_MAP:
      Inc(Result, 24);
    USE_SOLID_BGND:
      Inc(Result, 25);
    USE_V_GRADIENT:
      Inc(Result, 26);
    FOG:
      Inc(Result, 27);
    LAYER_FOG:
      Inc(Result, 28);
    DISTANCE_CUE:
      Inc(Result, 29);
    DEFAULT_VIEW:
      Inc(Result, 30);
    NAMED_OBJECT:
      Inc(Result, 31);
    KFSEG:
      Inc(Result, 32);
    KFCURTIME:
      Inc(Result, 33);
    TARGET_NODE_TAG,
    L_TARGET_NODE_TAG,
    OBJECT_NODE_TAG,
    CAMERA_NODE_TAG,
    SPOTLIGHT_NODE_TAG:
      Inc(Result, 34);
    AMBIENT_NODE_TAG:
      Inc(Result, 35);
    N_TRI_OBJECT,
    N_CAMERA,
    N_DIRECT_LIGHT:
      Inc(Result);
    OBJ_HIDDEN:
      Inc(Result);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetMaterialByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TMaterial3DS;

var
  Chunk: PChunk3DS;

begin
  FillChar(Result, SizeOf(Result), 0);

  Chunk := FindMatEntryByIndex(Source, DB, Index);
  if Assigned(Chunk) then
    Result := ReadMatEntryChunk(Source, Chunk)
  else
    ShowErrorFormatted(strError3DS_INVALID_INDEX, [Index]);
end;

//----------------- mesh object handling ------------------------------------------------------------------------------

function GetMeshCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;

  // returns the number of mesh objects referenced in the chunk list

var
  I: integer;
  Chunk: PChunk3DS;

begin
  // update the index to named objects if the list has changed recently
  UpdateNamedObjectList(Source, DB);

  Result := 0;
  if DB.ObjList = nil then
    Exit;

  // scan through the list of named objects
  for I := 0 to DB.ObjList^.Count - 1 do
  begin
    Chunk := FindChunk(DB.ObjList^.List^[I].Chunk, N_TRI_OBJECT);
    if Assigned(Chunk) then
      Inc(Result);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetMeshMatCount(Current: PChunk3DS): integer;

  // aids the GetMeshEntryChunk3DS in determining
  // how many materials are defined within the mesh object

var
  Chunk: PChunk3DS;

begin
  Result := 0;
  Chunk := FindChunk(Current, MSH_MAT_GROUP);
  while Assigned(Chunk) do
  begin
    Chunk := FindNextChunk(Chunk^.Sibling, MSH_MAT_GROUP);
    Inc(Result);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure RelMeshObjField(var Mesh: TMesh3DS; Field: integer);

var
  I: integer;

begin
  if ((Field and RelVertexArray3DS) <> 0) and Assigned(Mesh.VertexArray) then
  begin
    FreeMem(Mesh.VertexArray);
    Mesh.VertexArray := nil;
  end;

  if ((Field and RelTextArray3DS) <> 0) and Assigned(Mesh.TextArray) then
  begin
    FreeMem(Mesh.TextArray);
    Mesh.TextArray := nil;
  end;

  if ((Field and RelFaceArray3DS) <> 0) and Assigned(Mesh.FaceArray) then
  begin
    FreeMem(Mesh.FaceArray);
    Mesh.FaceArray := nil;
  end;

  if ((Field and RelMatArray3DS) <> 0) and Assigned(Mesh.MatArray) then
  begin
    for I := 0 to Mesh.NMats - 1 do
    begin
      // name is always assigned
      Mesh.MatArray^[I].NameStr := '';
      if Assigned(Mesh.MatArray^[I].FaceIndex) then
      begin
        FreeMem(Mesh.MatArray^[I].FaceIndex);
        Mesh.MatArray^[I].FaceIndex := nil;
      end;
    end;
    FreeMem(Mesh.MatArray);
    Mesh.MatArray := nil;
  end;

  if ((Field and RelSmoothArray3DS) <> 0) and Assigned(Mesh.SmoothArray) then
  begin
    FreeMem(Mesh.SmoothArray);
    Mesh.SmoothArray := nil;
  end;

  if ((Field and RelProcData3DS) <> 0) and Assigned(Mesh.ProcData) then
  begin
    FreeMem(Mesh.ProcData);
    Mesh.ProcData := nil;
  end;

  if ((Field and RelVFlagArray3DS) <> 0) and Assigned(Mesh.VFlagArray) then
  begin
    FreeMem(Mesh.VFlagArray);
    Mesh.VFlagArray := nil;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure InitMeshObjField(var aMesh: TMesh3DS; Field: integer);

var
  I: integer;

begin
  with aMesh do
  begin
    // test to see if Vertices are being allocated
    if (Field and InitVertexArray3DS) <> 0 then
    begin
      // if the vertex count is 0 then free the array
      if NVertices = 0 then
        RelMeshObjField(aMesh, RelVertexArray3DS)
      else
      begin
        // if this is the very first allocation
        if VertexArray = nil then
        begin
          // allocate the new block of memory
          VertexArray := AllocMem(NVertices * SizeOf(TPoint3DS));
          if VertexArray = nil then
            ShowError(strError3DS_NO_MEM);

          // this is done by AllocMem already
          // initialize the new block
          //for I := 0 to NVertices - 1 do VertexArray[I] := DefPoint3DS;
        end
        else // else this is an existing block
        begin
          // just resize it
          ReallocMem(VertexArray, SizeOf(TPoint3DS) * NVertices);
          if VertexArray = nil then
            ShowError(strError3DS_NO_MEM);
        end;
      end;
    end;

    if (Field and InitTextArray3DS) <> 0 then
    begin
      if NTextVerts = 0 then
        RelMeshObjField(aMesh, RelTextArray3DS)
      else
      begin
        if TextArray = nil then
        begin
          TextArray := Allocmem(NTextVerts * SizeOf(TTexVert3DS));
          if TextArray = nil then
            ShowError(strError3DS_NO_MEM);

          for I := 0 to NTextVerts - 1 do
            TextArray^[I] := DefTextVert3DS;
        end
        else
        begin
          Reallocmem(TextArray, SizeOf(TTexVert3DS) * NTextVerts);
          if TextArray = nil then
            ShowError(strError3DS_NO_MEM);
        end;
      end;
    end;

    if (Field and InitFaceArray3DS) <> 0 then
    begin
      if NFaces = 0 then
        RelMeshObjField(aMesh, RelFaceArray3DS)
      else
      begin
        if FaceArray = nil then
        begin
          FaceArray := AllocMem(NFaces * SizeOf(TFace3DS));
          if FaceArray = nil then
            ShowError(strError3DS_NO_MEM);

          for I := 0 to NFaces - 1 do
            FaceArray^[I] := DefFace3DS;
        end
        else
        begin
          ReallocMem(FaceArray, SizeOf(TFace3DS) * NFaces);
          if FaceArray = nil then
            ShowError(strError3DS_NO_MEM);
        end;
      end;
    end;

    if (Field and InitMatArray3DS) <> 0 then
    begin
      if NMats = 0 then
        RelMeshObjField(aMesh, RelMatArray3DS)
      else
      begin
        if Matarray = nil then
        begin
          MatArray := AllocMem(NMats * SizeOf(TObjmat3DS));
          if MatArray = nil then
            ShowError(strError3DS_NO_MEM);

          for I := 0 to NMats - 1 do
            MatArray^[I] := DefObjMat3DS;
        end
        else
        begin
          ReallocMem(MatArray, SizeOf(TObjmat3DS) * NMats);
          if MatArray = nil then
            ShowError(strError3DS_NO_MEM);
        end;
      end;
    end;

    if (Field and InitSmoothArray3DS) <> 0 then
    begin
      if NFaces = 0 then
        RelMeshObjField(aMesh, RelSmoothArray3DS)
      else
      begin
        if SmoothArray = nil then
        begin
          SmoothArray := AllocMem(NFaces * SizeOf(integer));
          if SmoothArray = nil then
            ShowError(strError3DS_NO_MEM);

          // done by AllocMem
          // for I := 0 to NFaces - 1 do SmoothArray[I] := 0;
        end
        else
        begin
          ReallocMem(SmoothArray, SizeOf(integer) * NFaces);
          if SmoothArray = nil then
            ShowError(strError3DS_NO_MEM);
        end;
      end;
    end;

    if (Field and InitProcData3DS) <> 0 then
    begin
      if ProcSize = 0 then
        RelMeshObjField(aMesh, RelProcData3DS)
      else
      begin
        if ProcData = nil then
        begin
          ProcData := AllocMem(ProcSize * SizeOf(byte));
          if ProcData = nil then
            ShowError(strError3DS_NO_MEM);
        end
        else
        begin
          ReallocMem(ProcData, SizeOf(byte) * ProcSize);
          if ProcData = nil then
            ShowError(strError3DS_NO_MEM);
        end;
      end;
    end;

    if (Field and InitVFlagArray3DS) <> 0 then
    begin
      if NVertices = 0 then
        RelMeshObjField(aMesh, RelVFlagArray3DS)
      else
      begin
        if VFlagArray = nil then
        begin
          VFlagArray := AllocMem(NVertices * SizeOf(word));
          if VFlagArray = nil then
            ShowError(strError3DS_NO_MEM);
        end
        else
        begin
          ReallocMem(VFlagArray, SizeOf(word) * NVertices);
          if VFlagArray = nil then
            ShowError(strError3DS_NO_MEM);
        end;
      end;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function InitMeshObj(VertexCount, FaceCount, InitFlags: word): TMesh3DS;

begin
  FillChar(Result, SizeOf(Result), 0);
  with Result do
  begin
    NVertices := VertexCount;
    Map.TileX := 1;
    Map.TileY := 1;
    Map.Scale := 1;
    Map.Matrix[0] := 1;
    Map.Matrix[4] := 1;
    Map.PW := 1;
    Map.PH := 1;
    Map.CH := 1;

    NFaces := FaceCount;

    InitMeshObjField(Result, InitVertexArray3DS or InitFaceArray3DS);

    if (InitFlags and InitTextArray3DS) <> 0 then
    begin
      NTextVerts := VertexCount;
      InitMeshObjField(Result, InitTextArray3DS);
    end;

    if (InitFlags and InitVFlagArray3DS) <> 0 then
    begin
      NVFlags := VertexCount;
      InitMeshObjField(Result, InitVFlagArray3DS);
    end;

    if (InitFlags and InitSmoothArray3DS) <> 0 then
      InitMeshObjField(Result, InitSmoothArray3DS);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ReleaseMeshObj(Mesh: PMesh3DS);

begin
  if Assigned(Mesh) then
  begin
    RelMeshObjField(Mesh^, RelAll3DS);
    Dispose(Mesh);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetMeshEntryChunk(const Source: TFile3DS; Chunk: PChunk3DS): TMesh3DS;

var
  NTriChunk, FaceArrayChunk, DataChunk, Current: PChunk3DS;
  I: integer;

begin
  NTriChunk := FindNextChunk(Chunk^.Children, N_TRI_OBJECT);
  if NTriChunk = nil then
    ShowError(strError3DS_WRONG_OBJECT);

  Result := InitMeshObj(0, 0, 0);

  with Result do
  begin
    // get the mesh name
    Source.ReadChunkData(Chunk);
    NameStr := StrPas(Chunk^.Data.NamedObject);

    Current := NTriChunk^.Children;
    while Assigned(Current) do
    begin
      case Current^.Tag of
        POINT_ARRAY:
        begin
          Source.ReadChunkData(Current);
          NVertices := Current^.Data.PointArray^.Vertices;
          VertexArray := Current^.Data.PointArray^.PointList;
          // avoid freeing the just allocated memory
          Current^.Data.PointArray^.PointList := nil;
          FreeChunkData(Current);
        end;
        POINT_FLAG_ARRAY:
        begin
          Source.ReadChunkData(Current);
          NVFlags := Current^.Data.PointFlagArray^.Flags;
          VFlagArray := Current^.Data.PointFlagArray^.FlagList;
          Current^.Data.PointFlagArray^.FlagList := nil;
          FreeChunkData(Current);
        end;
        FACE_ARRAY:
        begin
          Source.ReadChunkData(Current);
          NFaces := Current^.Data.FaceArray^.Faces;
          FaceArray := Current^.Data.FaceArray^.FaceList;
          Current^.Data.FaceArray^.FaceList := nil;

          if Assigned(Current^.Children) then
          begin
            // begin search for MESH_MAT_GROUP and SMOOTH_GROUP
            FaceArrayChunk := Current;

            // creates a list of all mesh mat groups
            DataChunk := FindChunk(FaceArrayChunk, MSH_MAT_GROUP);
            if Assigned(DataChunk) then
            begin
              NMats := GetMeshMatCount(DataChunk);
              MatArray := AllocMem(NMats * SizeOf(TObjMat3DS));

              for I := 0 to NMats - 1 do
              begin
                Source.ReadChunkData(DataChunk);
                MatArray^[I].NameStr := DataChunk^.Data.MshMatGroup^.MatNameStr;
                MatArray^[I].NFaces := DataChunk^.Data.MshMatGroup^.Faces;
                MatArray^[I].FaceIndex := DataChunk^.Data.MshMatGroup^.FaceList;
                DataChunk^.Data.MshMatGroup^.FaceList := nil;
                FreeChunkData(DataChunk);
                DataChunk := FindNextChunk(DataChunk^.Sibling, MSH_MAT_GROUP);
              end;
            end;

            DataChunk := FindNextChunk(FaceArrayChunk^.Children, SMOOTH_GROUP);
            if Assigned(DataChunk) then
            begin
              Source.ReadChunkData(DataChunk);
              SmoothArray := DataChunk^.Data.SmoothGroup^.GroupList;
              DataChunk^.Data.SmoothGroup^.GroupList := nil;
              FreeChunkData(DataChunk);
            end;

            DataChunk := FindNextChunk(FaceArrayChunk^.Children, MSH_BOXMAP);
            if Assigned(DataChunk) then
            begin
              Source.ReadChunkData(DataChunk);
              for I := 0 to 5 do
                BoxMapStr[I] := string(DataChunk^.Data.MshBoxmap^[I]);
              UseBoxmap := True;
              FreeChunkData(DataChunk);
            end;
          end;
          FreeChunkData(Current);
        end;
        TEX_VERTS:
        begin
          Source.ReadChunkData(Current);
          ntextverts := Current^.Data.TexVerts^.NumCoords;
          TextArray := Current^.Data.TexVerts^.TextVertList;
          Current^.Data.TexVerts^.TextVertList := nil;
          FreeChunkData(Current);
        end;
        MESH_MATRIX:
        begin
          Source.ReadChunkData(Current);
          LocMatrix := Current^.Data.MeshMatrix^;
          FreeChunkData(Current);
        end;
        MESH_TEXTURE_INFO:
        begin
          UseMapInfo := True;
          Source.ReadChunkData(Current);
          Map.MapType := Current^.Data.MeshTextureInfo^.MapType;
          Map.TileX := Current^.Data.MeshTextureInfo^.XTiling;
          Map.TileY := Current^.Data.MeshTextureInfo^.YTiling;
          Map.CenX := Current^.Data.MeshTextureInfo^.IconPos.X;
          Map.CenY := Current^.Data.MeshTextureInfo^.IconPos.Y;
          Map.CenZ := Current^.Data.MeshTextureInfo^.IconPos.Z;
          Map.Scale := Current^.Data.MeshTextureInfo^.IconScaling;
          Map.Matrix := Current^.Data.MeshTextureInfo^.XMatrix;
          Map.PW := Current^.Data.MeshTextureInfo^.IconWidth;
          Map.PH := Current^.Data.MeshTextureInfo^.IconHeight;
          Map.CH := Current^.Data.MeshTextureInfo^.CylIconHeight;
          FreeChunkData(Current);
        end;
        PROC_NAME:
        begin
          Source.ReadChunkData(Current);
          ProcNameStr := string(StrPas(Current^.Data.ProcName));
          FreeChunkData(Current);
        end;
        PROC_DATA:
        begin
          Source.ReadChunkData(Current);
          ProcSize := Current^.Data.IpasData^.Size;
          ProcData := Current^.Data.IpasData^.Data;
          Current^.Data.IpasData^.Data := nil;
          FreeChunkData(Current);
        end;
        MESH_COLOR:
        begin
          Source.ReadChunkData(Current);
          MeshColor := Current^.Data.MeshColor^;
          FreeChunkData(Current);
        end;
      end;
      Current := Current^.Sibling;
    end;

    IsHidden := Assigned(FindNextChunk(Chunk^.Children, OBJ_HIDDEN));
    IsVisLofter := Assigned(FindNextChunk(Chunk^.Children, OBJ_VIS_LOFTER));
    IsNoCast := Assigned(FindNextChunk(Chunk^.Children, OBJ_DOESNT_CAST));
    IsMatte := Assigned(FindNextChunk(Chunk^.Children, OBJ_MATTE));
    IsFast := Assigned(FindNextChunk(Chunk^.Children, OBJ_FAST));
    IsFrozen := Assigned(FindNextChunk(Chunk^.Children, OBJ_FROZEN));
    IsNoRcvShad := Assigned(FindNextChunk(Chunk^.Children, OBJ_DONT_RCVSHADOW));
    UseProc := Assigned(FindNextChunk(Chunk^.Children, OBJ_PROCEDURAL));
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetMeshByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TMesh3DS;

  // fills a mesh structure from the (index)th mesh reference found in DB

var
  Current: PChunk3DS;
  I, Count: integer;

begin
  FillChar(Result, SizeOf(Result), 0);

  if DB.TopChunk = nil then
    ShowError(strError3DS_INVALID_DATABASE);
  if (DB.TopChunk^.Tag <> M3DMAGIC) and (DB.TopChunk^.Tag <> CMAGIC) then
    ShowError(strError3DS_WRONG_DATABASE);

  // update the index to named objects if the list has changed recently
  UpdateNamedObjectList(Source, DB);

  // scan through the list of named objects
  Count := 0;
  for I := 0 to DB.ObjList^.Count - 1 do
  begin
    // search each named object for a mesh chunk
    Current := FindChunk(DB.ObjList^.List^[I].Chunk, N_TRI_OBJECT);

    // if a mesh chunk is found
    if Assigned(Current) then
    begin
      // increment the running total
      Inc(Count);
      // if this is the (index)th mesh, fill out the structure
      if (Count - 1) = Index then
      begin
        Result := GetMeshEntryChunk(Source, DB.ObjList^.List^[I].Chunk);
        Break;
      end;
    end;
  end;
end;

//----------------- spot and omni light handling ----------------------------------------------------------------------

function GetOmnilightCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;

var
  DLite, SpotL: PChunk3DS;
  I: integer;

begin
  // update the index to named objects if the list has changed recently
  UpdateNamedObjectList(Source, DB);

  Result := 0;
  if DB.ObjList = nil then
    Exit;

  // scan through the list of named objects looking for lights
  for I := 0 to DB.ObjList^.Count - 1 do
  begin
    // search each object for a Light chunk
    DLite := FindChunk(DB.ObjList^.List^[I].chunk, N_DIRECT_LIGHT);

    // if one was found, check to see if its a spotlight
    if Assigned(DLite) then
    begin
      SpotL := FindChunk(DLite, DL_SPOTLIGHT);
      // if it isn't a spotlight then increment the count
      if SpotL = nil then
        Inc(Result);
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetSpotlightCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;

var
  DLite, SpotL: PChunk3DS;
  I: integer;

begin
  // update the index to named objects if the list has changed recently
  UpdateNamedObjectList(Source, DB);

  Result := 0;
  if DB.ObjList = nil then
    Exit;

  // scan through the list of named objects looking for lights
  for I := 0 to DB.ObjList^.Count - 1 do
  begin
    // search each object for a Light chunk
    DLite := FindChunk(DB.ObjList^.List^[I].Chunk, N_DIRECT_LIGHT);

    // if one was found, check to see if its a spotlight
    if Assigned(DLite) then
    begin
      SpotL := FindChunk(DLite, DL_SPOTLIGHT);
      // if it is a spotlight then increment the count
      if Assigned(SpotL) then
        Inc(Result);
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure InitLight(var Light: TLight3DS);

// Initializes the Light structure

begin
  FillChar(Light, SizeOf(Light), 0);
  with Light do
  begin
    NameStr := '';
    Color.R := 0.708852;
    Color.G := 0.708852;
    Color.B := 0.708852;
    Multiplier := 1;
    Attenuation.Inner := 10;
    Attenuation.Outer := 100;
    Exclude := TStringList.Create;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ReleaseLight(Light: PLight3DS);

begin
  Light^.Exclude.Free;
  Light^.Exclude := nil;
  if Assigned(Light^.Spot) then
  begin
    Light^.Spot^.Projector.BitmapStr := '';
    FreeMem(Light^.Spot);
  end;
  Dispose(Light);
end;

//---------------------------------------------------------------------------------------------------------------------

procedure InitSpotLight(var SpotLight: TLight3DS);

begin
  // do the common Light initialization
  InitLight(SpotLight);
  SpotLight.Spot := AllocMem(SizeOf(TSpotLight3DS));

  with SpotLight.Spot^ do
  begin
    Target.X := 1;
    Target.Y := 1;
    Target.Z := 1;
    Hotspot := 44;
    Falloff := 45;
    Aspect := 1;

    Shadows.AType := ssUseShadowMap;
    Shadows.Bias := 1;
    Shadows.Filter := 3;
    Shadows.Mapsize := 512;
    Shadows.RayBias := 1;

    Cone.AType := csCircular;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetLightEntryChunk(const Source: TFile3DS; Chunk: PChunk3DS): TLight3DS;

  // fills out the given Light structure with the Light pointed to by Chunk

var
  DLite, SpotChunk, Current: PChunk3DS;

begin
  DLite := FindNextChunk(Chunk^.Children, N_DIRECT_LIGHT);
  if DLite = nil then
    ShowError(strError3DS_WRONG_OBJECT);

  DLite := FindChunk(Chunk^.Children, N_DIRECT_LIGHT);
  SpotChunk := FindChunk(Chunk, DL_SPOTLIGHT);

  if Assigned(DLite) then
    with Result do
    begin
      // initilize Light
      if SpotChunk = nil then
        InitLight(Result)
      else
        InitSpotLight(Result);

      // read object name
      Source.ReadChunkData(Chunk);
      NameStr := StrPas(Chunk^.Data.NamedObject);
      FreeChunkData(Chunk);

      // read Light postion
      Source.ReadChunkData(DLite);
      Pos := DLite^.Data.NDirectLight^;

      // scan all the chunks the Light contains
      Current := DLite^.Children;
      while Assigned(Current) do
      begin
        case Current^.Tag of
          COLOR_F:
          begin
            Source.ReadChunkData(Current);
            Color.R := Current^.Data.ColorF^.Red;
            Color.G := Current^.Data.ColorF^.Green;
            Color.B := Current^.Data.ColorF^.Blue;
            FreeChunkData(Current);
          end;
          COLOR_24:
          begin
            Source.ReadChunkData(Current);
            Color.R := Current^.Data.Color24^.Red / 255;
            Color.G := Current^.Data.Color24^.Green / 255;
            Color.B := Current^.Data.Color24^.Blue / 255;
            FreeChunkData(Current);
          end;
          DL_MULTIPLIER:
          begin
            Source.ReadChunkData(Current);
            Multiplier := Current^.Data.DlMultiplier^;
            FreeChunkData(Current);
          end;
          DL_INNER_RANGE:
          begin
            Source.ReadChunkData(Current);
            // assuming since there is a value it is on
            Attenuation.Inner := Current^.Data.DlInnerRange^;
            FreeChunkData(Current);
          end;
          DL_OUTER_RANGE:
          begin
            Source.ReadChunkData(Current);
            // assuming since there is a value it is on
            Attenuation.Outer := Current^.Data.DlOuterRange^;
            FreeChunkData(Current);
          end;
          DL_EXCLUDE:
          begin
            Source.ReadChunkData(Current);
            Exclude.Add(string(Current^.Data.DlExclude^));
            FreeChunkData(Current);
          end;
          DL_OFF:
            DLOff := True;
          DL_ATTENUATE:
            Attenuation.IsOn := True;
        end;
        Current := Current^.Sibling;
      end;

      // DL_SPOTLIGHT chunk
      if Assigned(SpotChunk) then
      begin
        // read spotlight data
        Source.ReadChunkData(SpotChunk);
        Spot^.Target := SpotChunk^.Data.DlSpotlight^.SpotLightTarg;
        Spot^.Hotspot := SpotChunk^.Data.DlSpotlight^.HotspotAngle;
        Spot^.Falloff := SpotChunk^.Data.DlSpotlight^.FalloffAngle;

        // scan all the chunks the spotlight contains
        Current := SpotChunk^.Children;
        while Assigned(Current) do
        begin
          case Current^.Tag of
            DL_SPOT_ROLL:
            begin
              Source.ReadChunkData(Current);
              Spot^.Roll := Current^.Data.DlSpotRoll^;
              FreeChunkData(Current);
            end;
            DL_LOCAL_SHADOW:
              Spot^.Shadows.Cast := True;
            DL_LOCAL_SHADOW2:
            begin
              Source.ReadChunkData(Current);
              Spot^.Shadows.Bias := Current^.Data.DlLocalShadow2^.LocalShadowBias;
              Spot^.Shadows.Filter := Current^.Data.DlLocalShadow2^.LocalShadowFilter;
              Spot^.Shadows.Mapsize :=
                Current^.Data.DlLocalShadow2^.LocalShadowMapSize;
              Spot^.Shadows.Local := True;
              FreeChunkData(Current);
            end;
            DL_SHADOWED:
              Spot^.Shadows.Cast := True;
            DL_SPOT_RECTANGULAR:
              Spot^.Cone.AType := csRectangular;
            DL_SEE_CONE:
              Spot^.Cone.Show := True;
            DL_SPOT_OVERSHOOT:
              Spot^.Cone.Overshoot := True;
            DL_SPOT_ASPECT:
            begin
              Source.ReadChunkData(Current);
              Spot^.Aspect := Current^.Data.DlSpotAspect^;
              FreeChunkData(Current);
            end;
            DL_RAY_BIAS:
            begin
              Source.ReadChunkData(Current);
              Spot^.Shadows.RayBias := Current^.Data.DlRayBias^;
              FreeChunkData(Current);
            end;
            DL_RAYSHAD:
              Spot^.Shadows.AType := ssUseRayTraceShadow;
            DL_SPOT_PROJECTOR:
            begin
              Source.ReadChunkData(Current);
              Spot^.Projector.BitmapStr :=
                string(StrPas(Current^.Data.DlSpotProjector));
              Spot^.Projector.Use := True;
              FreeChunkData(Current);
            end;
          end;
          Current := Current^.Sibling;
        end;
      end;
    end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetOmnilightByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TLight3DS;

  // fills out the omnilight structure from the (index)th mesh reference found in DB

var
  LightChunk, SpotChunk: PChunk3DS;
  I, Count: integer;

begin
  FillChar(Result, SizeOf(Result), 0);

  if (DB.TopChunk = nil) then
    ShowError(strError3DS_INVALID_DATABASE);
  if not (DB.TopChunk^.Tag = M3DMAGIC) and not (DB.TopChunk^.Tag = CMAGIC) then
    ShowError(strError3DS_WRONG_DATABASE);

  // update the list if it's changed recently
  UpdateNamedObjectList(Source, DB);

  // Scan through the List
  Count := 0;
  for I := 0 to DB.ObjList^.Count - 1 do
  begin
    // search for a Light chunk
    LightChunk := FindChunk(DB.ObjList^.List^[I].Chunk, N_DIRECT_LIGHT);

    // if one was found check to see if its a spot
    if Assigned(LightChunk) then
    begin
      SpotChunk := FindChunk(LightChunk, DL_SPOTLIGHT);
      // if its not a spot then increment the count
      if SpotChunk = nil then
      begin
        Inc(Count);
        // if this is the (index)th Light file out the structure
        if (Count - 1) = Index then
        begin
          Result := GetLightEntryChunk(Source, DB.ObjList^.List^[I].Chunk);
          Break;
        end;
      end;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetSpotlightByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TLight3DS;

  // fills out the Spot structure from the (index)th spot reference found in DB

var
  LightChunk, SpotChunk: PChunk3DS;
  I, Count: integer;

begin
  FillChar(Result, SizeOf(Result), 0);

  if (DB.TopChunk = nil) then
    ShowError(strError3DS_INVALID_DATABASE);
  if not (DB.TopChunk^.Tag = M3DMAGIC) and not (DB.TopChunk^.Tag = CMAGIC) then
    ShowError(strError3DS_WRONG_DATABASE);

  // update the list if it's changed recently
  UpdateNamedObjectList(Source, DB);

  // Scan through the List
  Count := 0;
  for I := 0 to DB.ObjList^.Count - 1 do
  begin
    // search for a Light chunk
    LightChunk := FindChunk(DB.ObjList^.List^[I].Chunk, N_DIRECT_LIGHT);

    // if one was found check to see if its a spot
    if Assigned(LightChunk) then
    begin
      SpotChunk := FindChunk(LightChunk, DL_SPOTLIGHT);
      // if its not a spot then increment the count
      if Assigned(SpotChunk) then
      begin
        Inc(Count);
        // if this is the (index)th Light file out the structure
        if (Count - 1) = Index then
        begin
          Result := GetLightEntryChunk(Source, DB.ObjList^.List^[I].Chunk);
          Break;
        end;
      end;
    end;
  end;
end;

//----------------- camera handling -----------------------------------------------------------------------------------

procedure InitCamera(var Camera: TCamera3DS);

begin
  FillChar(Camera, SizeOf(Camera), 0);
  with Camera do
  begin
    Target.X := 1;
    Target.Y := 1;
    Target.Z := 1;
    FOV := 45;
    Ranges.CamNear := 10;
    Ranges.CamFar := 1000;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ReleaseCamera(Camera: PCamera3DS);

begin
  if Assigned(Camera) then
  begin
    Dispose(Camera);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetCameraCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;

var
  Chunk: PChunk3DS;
  I: integer;

begin
  UpdateNamedObjectList(Source, DB);
  Result := 0;
  if Assigned(DB.ObjList) then
    for I := 0 to DB.ObjList^.Count - 1 do
    begin
      Chunk := FindChunk(DB.ObjList^.List^[I].Chunk, N_CAMERA);
      if Assigned(Chunk) then
        Inc(Result);
    end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetCameraEntry(const Source: TFile3DS; Chunk: PChunk3DS): TCamera3DS;

var
  Current, Camera: PChunk3DS;

begin
  if Chunk^.Tag <> NAMED_OBJECT then
    ShowError(strError3DS_WRONG_OBJECT);

  Camera := FindNextChunk(Chunk^.Children, N_CAMERA);
  if Camera = nil then
    ShowError(strError3DS_WRONG_OBJECT);

  with Result do
  begin
    InitCamera(Result);
    Camera := FindNextChunk(Chunk^.Children, N_CAMERA);

    Source.ReadChunkData(Chunk);
    NameStr := StrPas(Chunk^.Data.NamedObject);
    FreeChunkData(Chunk);

    Source.ReadChunkData(Camera);
    Position.X := Camera^.Data.NCamera^.CameraPos.X;
    Position.Y := Camera^.Data.NCamera^.CameraPos.Y;
    Position.Z := Camera^.Data.NCamera^.CameraPos.Z;
    Target.X := Camera^.Data.NCamera^.TargetPos.X;
    Target.Y := Camera^.Data.NCamera^.TargetPos.Y;
    Target.Z := Camera^.Data.NCamera^.TargetPos.Z;
    Roll := Camera^.Data.NCamera^.CameraBank;
    FOV := 2400 / Camera^.Data.NCamera^.CameraFocalLength;
    FreeChunkData(Camera);

    Current := Camera^.Children;
    while Assigned(Current) do
    begin
      case Current^.Tag of
        CAM_SEE_CONE:
          ShowCone := True;
        CAM_RANGES:
        begin
          Source.ReadChunkData(Current);
          Ranges.CamNear := Current^.Data.CamRanges^.NearPlane;
          Ranges.CamFar := Current^.Data.CamRanges^.FarPlane;
          FreeChunkData(Current);
        end;
      end;
      Current := Current^.Sibling;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetCameraByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TCamera3DS;

var
  Camera: PChunk3DS;
  I, Count: integer;

begin
  FillChar(Result, SizeOf(Result), 0);

  UpdateNamedObjectList(Source, DB);

  Count := 0;
  for I := 0 to DB.ObjList^.Count - 1 do
  begin
    Camera := FindChunk(DB.ObjList^.List^[I].Chunk, N_CAMERA);
    if Assigned(Camera) then
    begin
      Inc(Count);
      if (Count - 1) = Index then
        Result := GetCameraEntry(Source, DB.ObjList^.List^[I].Chunk);
    end;
  end;
end;

//----------------- common animation settings -------------------------------------------------------------------------

procedure InitKfSets(var Key: TKFSets3DS);

begin
  Key.Anim.Length := 30;
  Key.Anim.CurFrame := 0;
  Key.Seg.Use := False;
  Key.Seg.SegBegin := 0;
  Key.Seg.SegEnd := 30;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetKFSeg(TopChunk: PChunk3DS): PChunk3DS;

  // all the keyframe information has to go in the appropriate segment KFDATA

begin
  // look for KFDATA
  Result := FindNextChunk(TopChunk^.Children, KFDATA);
  if Result = nil then
    Result := PutGenericNode(KFDATA, TopChunk);
end;

//---------------------------------------------------------------------------------------------------------------------

function GetKeyInfo(const Source: TFile3DS; var DB: TDatabase3DS): TKFKeyInfo3DS;

var
  KFData, KFHdrChunk, KFCTChunk: PChunk3DS;

begin
  KFData := GetKfSeg(DB.TopChunk);
  KFHdrChunk := FindNextChunk(KFData^.Children, KFHDR);

  if Assigned(KFHdrChunk) then
  begin
    Source.ReadChunkData(KFHdrChunk);
    Result.Length := KFHdrChunk^.Data.KFHdr^.AnimLength;
    FreeChunkData(KFHdrChunk);
  end;

  KFCTChunk := FindNextChunk(KFData^.Children, KFCURTIME);

  if Assigned(KFCTChunk) then
  begin
    Source.ReadChunkData(KFCTChunk);
    Result.CurFrame := KFCTChunk^.Data.KFCurTime^;
    FreeChunkData(KFCTChunk);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetKFSegment(const Source: TFile3DS; var DB: TDatabase3DS): TKFSegment3DS;
var
  DataChunk, SegChunk: PChunk3DS;
begin
  Result.SegBegin := 0;
  Result.SegEnd := 0;
  Result.Use := False;

  DataChunk := GetKFSeg(DB.TopChunk);
  SegChunk := FindNextChunk(DataChunk^.Children, KFSEG);

  if Assigned(SegChunk) then
  begin
    Source.ReadChunkData(SegChunk);
    Result.Use := True;
    Result.SegBegin := SegChunk^.Data.KFSeg^.First;
    Result.SegEnd := SegChunk^.Data.KFSeg^.Last;
    FreeChunkData(SegChunk);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetKFSettings(const Source: TFile3DS; var DB: TDatabase3DS): TKFSets3DS;

begin
  FillChar(Result, SizeOf(Result), 0);

  if DB.TopChunk = nil then
    ShowError(strError3DS_INVALID_DATABASE);
  if (DB.TopChunk^.Tag <> M3DMAGIC) and (DB.TopChunk^.Tag <> CMAGIC) then
    ShowError(strError3DS_WRONG_DATABASE);

  InitKFSets(Result);
  Result.Anim := GetKeyInfo(Source, DB);
  Result.Seg := GetKFSegment(Source, DB);
end;

//----------------- Camera animation ----------------------------------------------------------------------------------

procedure InitCameraMotion(var Camera: TKFCamera3DS;
  NewNPKeys, NewNFKeys, NewNRKeys, NewNTKeys: cardinal);

var
  I: integer;

begin
  with Camera do
  begin
    // free any previously allocated memory first
    if Assigned(PKeys) then
      FreeMem(PKeys);
    if Assigned(Pos) then
      FreeMem(Pos);
    if Assigned(FKeys) then
      FreeMem(FKeys);
    if Assigned(FOV) then
      FreeMem(FOV);
    if Assigned(RKeys) then
      FreeMem(RKeys);
    if Assigned(Roll) then
      FreeMem(Roll);
    if Assigned(TKeys) then
      FreeMem(TKeys);
    if Assigned(TPos) then
      FreeMem(TPos);

    FillChar(Camera, SizeOf(TKFCamera3DS), 0);
    NPKeys := NewNPKeys;
    NFKeys := NewNFKeys;
    NRKeys := NewNRKeys;
    NTKeys := NewNTKeys;

    if NPKeys <> 0 then
    begin
      NPFlag := TrackSingle3DS;

      PKeys := AllocMem(NPKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NPKeys - 1 do
        PKeys^[I] := DefKeyHeader3DS;

      Pos := AllocMem(NPKeys * SizeOf(TPoint3DS));
      for I := 0 to NPKeys - 1 do
        Pos^[I] := DefPoint3DS;
    end;

    if NFKeys <> 0 then
    begin
      NFFlag := TrackSingle3DS;

      FKeys := AllocMem(NFKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NFKeys - 1 do
        FKeys^[I] := DefKeyHeader3DS;

      FOV := AllocMem(NFKeys * SizeOf(single));
      for I := 0 to NFKeys - 1 do
        FOV^[I] := 60;
    end;

    if NRKeys <> 0 then
    begin
      NRFlag := TrackSingle3DS;

      RKeys := AllocMem(NRKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NRKeys - 1 do
        RKeys^[I] := DefKeyHeader3DS;

      Roll := AllocMem(NRKeys * SizeOf(single));
    end;

    if NTKeys <> 0 then
    begin
      NTFlag := TrackSingle3DS;
      TFlags1 := 0;
      TFlags2 := 0;

      TKeys := AllocMem(NTKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NTKeys - 1 do
        TKeys^[I] := DefKeyHeader3DS;

      TPos := AllocMem(NTKeys * SizeOf(TPoint3DS));
      for I := 0 to NTKeys - 1 do
        TPos^[I] := DefPoint3DS;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ReleaseCameraMotion(Camera: PKFCamera3DS);

begin
  if Assigned(Camera) then
  begin
    with Camera^ do
    begin
      if Assigned(PKeys) then
        FreeMem(PKeys);
      if Assigned(Pos) then
        FreeMem(Pos);
      if Assigned(FKeys) then
        FreeMem(FKeys);
      if Assigned(FOV) then
        FreeMem(FOV);
      if Assigned(RKeys) then
        FreeMem(RKeys);
      if Assigned(Roll) then
        FreeMem(Roll);
      if Assigned(TKeys) then
        FreeMem(TKeys);
      if Assigned(TPos) then
        FreeMem(TPos);
    end;
    Dispose(Camera);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure GetCameraNodeNameList(const Source: TFile3DS; var DB: TDatabase3DS;
  List: TStringList);

begin
  GetGenericNodeNameList(Source, DB, CAMERA_NODE_TAG, List);
end;

//---------------------------------------------------------------------------------------------------------------------

function GetCameraNodeCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;

begin
  Result := GetGenericNodeCount(Source, DB, CAMERA_NODE_TAG);
end;

//---------------------------------------------------------------------------------------------------------------------

function GetParentName(const Source: TFile3DS; Chunk: PChunk3DS): string;

  // get parent name if there is one

var
  NameChunk: PChunk3DS;

begin
  Result := '';
  NameChunk := FindChunk(Chunk, PARENT_NAME);
  if Assigned(NameChunk) then
  begin
    Source.ReadChunkData(NameChunk);
    Result := string(NameChunk^.Data.NamedObject);
    FreeChunkData(NameChunk);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetCameraMotion(const Source: TFile3DS;
  CamChunk, TargetChunk: PChunk3DS): TKFCamera3DS;

  // gets camera keyframe information from chunk
  // CamChunk    : CAMERA_NODE_TAG chunk to extract data from
  // TargetChunk : TARGET_NODE_TAG chunk to extract target data from
  // KFCamera    : Structure to fill in with chunk data

var
  NodeHdrChunk, PosChunk, FovChunk, RollChunk, TargetPosChunk,
  TargetHdrChunk: PChunk3DS;
  PosKeys, FovKeys, RollKeys, TargetKeys: integer;

begin
  FillChar(Result, SizeOf(Result), 0);

  TargetPosChunk := nil;
  TargetHdrChunk := nil;
  PosKeys := 0;
  FovKeys := 0;
  RollKeys := 0;
  TargetKeys := 0;

  // get information from chunks
  // search children of camera Chunk
  NodeHdrChunk := FindChunk(CamChunk, NODE_HDR);
  PosChunk := FindChunk(CamChunk, POS_TRACK_TAG);
  FovChunk := FindChunk(CamChunk, FOV_TRACK_TAG);
  RollChunk := FindChunk(CamChunk, ROLL_TRACK_TAG);

  Source.ReadChunkData(NodeHdrChunk);

  if Assigned(PosChunk) then
  begin
    Source.ReadChunkData(PosChunk);
    PosKeys := PosChunk^.Data.PosTrackTag^.TrackHdr.KeyCount;
  end;

  if Assigned(FOVChunk) then
  begin
    Source.ReadChunkData(FOVChunk);
    FovKeys := FOVChunk^.Data.FOVTrackTag^.TrackHdr.KeyCount;
  end;

  if Assigned(RollChunk) then
  begin
    Source.ReadChunkData(RollChunk);
    RollKeys := RollChunk^.Data.RollTrackTag^.TrackHdr.KeyCount;
  end;

  if Assigned(TargetChunk) then
  begin
    TargetHdrChunk := FindChunk(TargetChunk, NODE_HDR);
    if Assigned(TargetHdrChunk) then
      Source.ReadChunkData(TargetHdrChunk);

    TargetPosChunk := FindChunk(TargetChunk, POS_TRACK_TAG);
    if Assigned(TargetPosChunk) then
    begin
      Source.ReadChunkData(TargetPosChunk);
      TargetKeys := TargetPosChunk^.Data.PosTrackTag^.TrackHdr.KeyCount;
    end;
  end;

  // set-up and fill-in the kfcamera structure
  InitCameraMotion(Result, PosKeys, FOVKeys, RollKeys, TargetKeys);
  with Result do
  begin
    // header Information
    if Assigned(NodeHdrChunk) then
    begin
      NameStr := ansistring(NodeHdrChunk^.Data.NodeHdr^.ObjNameStr);
      Flags1 := NodeHdrChunk^.Data.NodeHdr^.Flags1;
      Flags2 := NodeHdrChunk^.Data.NodeHdr^.Flags2;
    end;
    // parents
    ParentStr := ansistring(GetParentName(Source, NodeHdrChunk));
    TParentStr := GetParentName(Source, TargetHdrChunk);

    // target information
    if TargetKeys <> 0 then
    begin
      NTFlag := TargetPosChunk^.Data.PosTrackTag^.TrackHdr.Flags;
      Move(TargetPosChunk^.Data.PosTrackTag^.KeyHdrList^, TKeys^,
        TargetKeys * SizeOf(TKeyHeader3DS));
      Move(TargetPosChunk^.Data.PosTrackTag^.PositionList^, TPos^,
        TargetKeys * SizeOf(TPoint3DS));
    end;
    if Assigned(TargetHdrChunk) then
    begin
      TFlags1 := TargetHdrChunk^.Data.NodeHdr^.Flags1;
      TFlags2 := TargetHdrChunk^.Data.NodeHdr^.Flags2;
    end;

    // position information
    if PosKeys <> 0 then
    begin
      NPFlag := PosChunk^.Data.PosTrackTag^.TrackHdr.Flags;
      Move(PosChunk^.Data.PosTrackTag^.KeyHdrList^, PKeys^, PosKeys *
        SizeOf(TKeyHeader3DS));
      Move(PosChunk^.Data.PosTrackTag^.PositionList^, Pos^, PosKeys * SizeOf(TPoint3DS));
    end;

    // field of view information
    if FOVKeys <> 0 then
    begin
      NFFlag := FOVChunk^.Data.FOVTrackTag^.TrackHdr.Flags;
      Move(FOVChunk^.Data.FOVTrackTag^.KeyHdrList^, FKeys^, FOVKeys *
        SizeOf(TKeyHeader3DS));
      Move(FOVChunk^.Data.FOVTrackTag^.FOVAngleList^, FOV^, FOVKeys * SizeOf(single));
    end;

    // roll track information
    if RollKeys <> 0 then
    begin
      NRFlag := RollChunk^.Data.RollTrackTag^.TrackHdr.Flags;
      Move(RollChunk^.Data.RollTrackTag^.KeyHdrList^, RKeys^, RollKeys *
        SizeOf(TKeyHeader3DS));
      Move(RollChunk^.Data.RollTrackTag^.RollangleList^, Roll^,
        RollKeys * SizeOf(single));
    end;

    // free chunk data
    if Assigned(PosChunk) then
      FreeChunkData(PosChunk);
    if Assigned(FovChunk) then
      FreeChunkData(FovChunk);
    if Assigned(RollChunk) then
      FreeChunkData(RollChunk);
    if Assigned(NodeHdrChunk) then
      FreeChunkData(NodeHdrChunk);
    if Assigned(TargetPosChunk) then
      FreeChunkData(TargetPosChunk);
    if Assigned(TargetHdrChunk) then
      FreeChunkData(TargetHdrChunk);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetCameraMotionByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: integer): TKFCamera3DS;

var
  CameraChunk, TargetChunk: PChunk3DS;
  List: TStringList;

begin
  FillChar(Result, SizeOf(Result), 0);

  List := TStringList.Create;
  try
    GetCameraNodeNameList(Source, DB, List);
    if Index < List.Count then
    begin
      CameraChunk := FindNamedAndTaggedChunk(Source, DB, List[Index], CAMERA_NODE_TAG);
      if Assigned(CameraChunk) then
      begin
        TargetChunk := FindNamedAndTaggedChunk(Source, DB, List[Index], TARGET_NODE_TAG);
        Result := GetCameraMotion(Source, CameraChunk, TargetChunk);
      end;
    end;
  finally
    List.Free;
  end;
end;

//----------------- Ambient Light animation ---------------------------------------------------------------------------

procedure InitAmbientLightMotion(var Light: TKFAmbient3DS; NewNCKeys: cardinal);

var
  I: integer;

begin
  with Light do
  begin
    if Assigned(Color) then
      FreeMem(Color);
    if Assigned(CKeys) then
      FreeMem(CKeys);
    FillChar(Light, SizeOf(Light), 0);
    NCKeys := NewNCKeys;

    if NCKeys <> 0 then
    begin
      NCFlag := TrackSingle3DS;
      CKeys := AllocMem(NCKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NCKeys - 1 do
        CKeys^[I] := DefKeyHeader3DS;

      Color := AllocMem(NCKeys * SizeOf(TFColor3DS));
      for I := 0 to NCKeys - 1 do
      begin
        Color^[I].R := 1;
        Color^[I].G := 1;
        Color^[I].B := 1;
      end;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ReleaseAmbientLightMotion(Light: PKFAmbient3DS);

begin
  if Assigned(Light) then
  begin
    with Light^ do
    begin
      if Assigned(CKeys) then
        FreeMem(CKeys);
      if Assigned(Color) then
        FreeMem(Color);
    end;
    Dispose(Light);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetAmbientLightMotionChunk(const Source: TFile3DS;
  AmbientChunk: PChunk3DS): TKFAmbient3DS;

  // AmbientChunk : SPOTAMBIENT_NODE_TAG chunk to extract data from
  // TargetChunk  : L_TARGET_NODE_TAG chunk to extract target data from
  // KFSpot       : Structure to fill in with chunk data

  // Gets AmbientLight keyframe information from chunk
  //  L_TARGET
  //   ...
  //  NODE_HDR
  //  APP_DATA
  //  COL_TRACK

var
  NodeHdrChunk, ColChunk: PChunk3DS;
  ColKeys: integer;

begin
  if AmbientChunk = nil then
    ShowError(strERROR3DS_INVALID_ARG);
  FillChar(Result, SizeOf(Result), 0);

  // get information from chunks
  // search children of AmbientLight chunk
  NodeHdrChunk := FindChunk(AmbientChunk, NODE_HDR);
  ColChunk := FindChunk(AmbientChunk, COL_TRACK_TAG);

  if Assigned(NodeHdrChunk) then
    Source.ReadChunkData(NodeHdrChunk);
  if Assigned(ColChunk) then
  begin
    Source.ReadChunkData(ColChunk);
    ColKeys := ColChunk^.Data.ColTrackTag^.TrackHdr.KeyCount;
  end
  else
    ColKeys := 0;

  // eat-up and fill-in the PKFAmbient3DS structure
  InitAmbientLightMotion(Result, ColKeys);

  // header information
  if Assigned(NodeHdrChunk) then
  begin
    Result.Flags1 := NodeHdrChunk^.Data.NodeHdr^.Flags1;
    Result.Flags2 := NodeHdrChunk^.Data.NodeHdr^.Flags2;
  end;

  // color information
  if Assigned(ColChunk) then
  begin
    if ColKeys <> 0 then
    begin
      Result.NCFlag := ColChunk^.Data.ColTrackTag^.TrackHdr.Flags;
      Move(ColChunk^.Data.ColTrackTag^.KeyHdrList^, Result.CKeys^,
        ColKeys * SizeOf(TKeyHeader3DS));
      Move(ColChunk^.Data.ColTrackTag^.ColorList^, Result.Color^,
        ColKeys * SizeOf(TFColor3DS));
    end;
  end;

  // free chunk data
  if Assigned(NodeHdrChunk) then
    FreeChunkData(NodeHdrChunk);
  if Assigned(ColChunk) then
    FreeChunkData(ColChunk);
end;

//---------------------------------------------------------------------------------------------------------------------

function GetAmbientLightMotion(const Source: TFile3DS;
  var DB: TDatabase3DS): TKFAmbient3DS;

  // Ambient Light a special case: only one ambient node per keyframe data Chunk^.

var
  KFChunk, Chunk: PChunk3DS;

begin
  FillChar(Result, SizeOf(Result), 0);
  // find keyframe chunk
  KFChunk := FindChunk(DB.TopChunk, KFDATA);
  if Assigned(KFChunk) then
  begin
    Chunk := FindChunk(KFChunk, AMBIENT_NODE_TAG);
    if Assigned(Chunk) then
      Result := GetAmbientLightMotionChunk(Source, Chunk);
  end;
end;

//----------------- Mesh object animation -----------------------------------------------------------------------------

procedure InitObjectMotion(var Obj: TKFMesh3DS; NewNPKeys, // Number of position keys
  NewNRKeys, // Number of rot keys
  NewNSKeys, // Number of scale keys
  NewNMKeys, // Number of morph keys
  NewNHKeys: cardinal); // Number of hide keys
var
  I: integer;

begin
  with Obj do
  begin
    if Assigned(PKeys) then
      FreeMem(PKeys);
    if Assigned(Pos) then
      FreeMem(Pos);
    if Assigned(RKeys) then
      FreeMem(RKeys);
    if Assigned(Rot) then
      FreeMem(Rot);
    if Assigned(SKeys) then
      FreeMem(SKeys);
    if Assigned(Scale) then
      FreeMem(Scale);
    if Assigned(MKeys) then
      FreeMem(MKeys);
    if Assigned(Morph) then
      FreeMem(Morph);
    if Assigned(HKeys) then
      FreeMem(HKeys);

    FillChar(Obj, SizeOf(Obj), 0);
    Pivot := DefPoint3DS;
    BoundMin := DefPoint3DS;
    BoundMax := DefPoint3DS;

    NPKeys := NewNPKeys;
    NRKeys := NewNRKeys;
    NSKeys := NewNSKeys;
    NMKeys := NewNMKeys;
    NHKeys := NewNHKeys;

    MSAngle := 24;

    if NPKeys <> 0 then
    begin
      NPFlag := TrackSingle3DS;

      PKeys := AllocMem(NPKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NPKeys - 1 do
        PKeys^[I] := DefKeyHeader3DS;

      Pos := AllocMem(NPKeys * SizeOf(TPoint3DS));
      for I := 0 to NPKeys - 1 do
        Pos^[I] := DefPoint3DS;
    end;

    if NRKeys <> 0 then
    begin
      NRFlag := TrackSingle3DS;

      RKeys := AllocMem(NRKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NRKeys - 1 do
        RKeys^[I] := DefKeyHeader3DS;

      Rot := AllocMem(NRKeys * SizeOf(TKFRotKey3DS));
      for I := 0 to NRKeys - 1 do
        Rot^[I] := DefKfRotKey3DS;
    end;

    if NSKeys <> 0 then
    begin
      NSFlag := TrackSingle3DS;

      SKeys := AllocMem(NSKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NSKeys - 1 do
        SKeys^[I] := DefKeyHeader3DS;

      Scale := AllocMem(NSKeys * SizeOf(TPoint3DS));
      for I := 0 to NSKeys - 1 do
      begin
        Scale^[I].X := 1;
        Scale^[I].Y := 1;
        Scale^[I].Z := 1;
      end;
    end;

    if NMKeys <> 0 then
    begin
      NMFlag := TrackSingle3DS;

      MKeys := AllocMem(NMKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NMKeys - 1 do
        MKeys^[I] := DefKeyHeader3DS;

      Morph := AllocMem(NMKeys * SizeOf(TKFMorphKey3DS));
      for I := 0 to NMKeys - 1 do
        Morph^[I] := ' ';
    end;

    if NHKeys <> 0 then
    begin
      NHFlag := TrackSingle3DS;

      HKeys := AllocMem(NHKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NMKeys - 1 do
        MKeys^[I] := DefKeyHeader3DS;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ReleaseObjectMotion(Obj: PKFMesh3DS);

begin
  if Assigned(Obj) then
  begin
    with Obj^ do
    begin
      if Assigned(PKeys) then
        FreeMem(PKeys);
      if Assigned(Pos) then
        FreeMem(Pos);
      if Assigned(RKeys) then
        FreeMem(RKeys);
      if Assigned(Rot) then
        FreeMem(Rot);
      if Assigned(SKeys) then
        FreeMem(SKeys);
      if Assigned(Scale) then
        FreeMem(Scale);
      if Assigned(MKeys) then
        FreeMem(MKeys);
      if Assigned(Morph) then
        FreeMem(Morph);
      if Assigned(HKeys) then
        FreeMem(HKeys);
    end;
    Dispose(Obj);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetObjectNodeCount(const Source: TFile3DS; var DB: TDatabase3DS): integer;

begin
  Result := GetGenericNodeCount(Source, DB, OBJECT_NODE_TAG);
end;

//---------------------------------------------------------------------------------------------------------------------

procedure GetObjectNodeNameList(const Source: TFile3DS; var DB: TDatabase3DS;
  List: TStringList);

begin
  GetGenericNodeNameList(Source, DB, OBJECT_NODE_TAG, List);
end;

//---------------------------------------------------------------------------------------------------------------------

function GetObjectMotion(const Source: TFile3DS; MeshChunk: PChunk3DS): TKFMesh3DS;

  // Gets mesh keyframe information from chunk

  //  NODE_ID
  //  NODE_HDR
  //  APP_DATA
  //  INSTANCE_NAME
  //  PRESCALE a no-op in 3DS code
  //  POS_TRACK
  //  ROT_TRACK
  //  SCL_TRACK
  //  MORPH_TRACK
  //  MORPH_SMOOTH
  //  HIDE_TRACK

  // This function is really confusing, because instead of loading the MeshChunk and its children, reading the
  // data out and freeing it, the chunk structure is copied, its data is moved to the copy (so MeshChunk is then without
  // any data), the copy is parsed and then it is freed. I don't know why this is so, but I don't want to change
  // the way it works in case this has (or will later have) side effects I don't see yet. ml

var
  NodeHdrChunk, InstChunk, PivotChunk, BboxChunk, MsChunk,
  PosChunk, RotChunk, ScaleChunk, MorphChunk, HideChunk,
  ObjTag: PChunk3DS;

  PosKeys, RotKeys, ScaleKeys, MorphKeys, HideKeys: integer;

  PivotData: PPivot;
  InstData: PInstanceName;
  BBoxData: PBoundBox;
  MsData: PMorphSmooth;
  PosData: PPosTrackTag;
  RotData: PRotTrackTag;
  ScaleData: PScaleTrackTag;
  MorphData: PMorphTrackTag;
  HideData: PHideTrackTag;

begin
  PosKeys := 0;
  RotKeys := 0;
  ScaleKeys := 0;
  MorphKeys := 0;
  HideKeys := 0;
  PivotData := nil;
  InstData := nil;
  BboxData := nil;
  MsData := nil;
  PosData := nil;
  RotData := nil;
  ScaleData := nil;
  MorphData := nil;
  HideData := nil;

  if MeshChunk^.Tag <> OBJECT_NODE_TAG then
    ShowError(strERROR3DS_WRONG_OBJECT);

  ObjTag := CopyChunk(MeshChunk);

  // get information from chunks
  // search children of MeshLight chunk
  NodeHdrChunk := FindChunk(ObjTag, NODE_HDR);
  InstChunk := FindChunk(ObjTag, INSTANCE_NAME);
  PivotChunk := FindChunk(ObjTag, PIVOT);
  BboxChunk := FindChunk(ObjTag, BOUNDBOX);
  MsChunk := FindChunk(ObjTag, MORPH_SMOOTH);
  PosChunk := FindChunk(ObjTag, POS_TRACK_TAG);
  RotChunk := FindChunk(ObjTag, ROT_TRACK_TAG);
  ScaleChunk := FindChunk(ObjTag, SCL_TRACK_TAG);
  MorphChunk := FindChunk(ObjTag, MORPH_TRACK_TAG);
  HideChunk := FindChunk(ObjTag, HIDE_TRACK_TAG);

  Source.ReadChunkData(NodeHdrChunk);

  if Assigned(InstChunk) then
  begin
    Source.ReadChunkData(InstChunk);
    InstData := InstChunk^.Data.Dummy;
    InstChunk^.Data.Dummy := nil;
  end;

  if Assigned(PivotChunk) then
  begin
    Source.ReadChunkData(PivotChunk);
    PivotData := PivotChunk^.Data.Dummy;
    PivotChunk^.Data.Dummy := nil;
  end;

  if Assigned(BboxChunk) then
  begin
    Source.ReadChunkData(BboxChunk);
    BboxData := BboxChunk^.Data.Dummy;
    BboxChunk^.Data.Dummy := nil;
  end;

  if Assigned(MsChunk) then
  begin
    Source.ReadChunkData(MsChunk);
    MsData := MsChunk^.Data.Dummy;
    MsChunk^.Data.Dummy := nil;
  end;

  if Assigned(PosChunk) then
  begin
    Source.ReadChunkData(PosChunk);
    PosData := PosChunk^.Data.Dummy;
    PosKeys := PosData^.TrackHdr.KeyCount;
    PosChunk^.Data.Dummy := nil;
  end;

  if Assigned(RotChunk) then
  begin
    Source.ReadChunkData(RotChunk);
    RotData := RotChunk^.Data.Dummy;
    RotKeys := RotData^.TrackHdr.KeyCount;
    RotChunk^.Data.Dummy := nil;
  end;

  if Assigned(ScaleChunk) then
  begin
    Source.ReadChunkData(ScaleChunk);
    ScaleData := ScaleChunk^.Data.Dummy;
    ScaleKeys := ScaleData^.TrackHdr.KeyCount;
    ScaleChunk^.Data.Dummy := nil;
  end;

  if Assigned(MorphChunk) then
  begin
    Source.ReadChunkData(MorphChunk);
    MorphData := MorphChunk^.Data.Dummy;
    MorphKeys := MorphData^.TrackHdr.KeyCount;
    MorphChunk^.Data.Dummy := nil;
  end;

  if Assigned(HideChunk) then
  begin
    Source.ReadChunkData(HideChunk);
    HideData := HideChunk^.Data.Dummy;
    HideKeys := HideData^.TrackHdr.KeyCount;
    HideChunk^.Data.Dummy := nil;
  end;

  // set-up and fill-in the TKFMesh3DS structure
  with Result do
  begin
    //--- header Information
    NameStr := ansistring(NodeHdrChunk^.Data.NodeHdr^.ObjNameStr);
    Flags1 := NodeHdrChunk^.Data.NodeHdr^.Flags1;
    Flags2 := NodeHdrChunk^.Data.NodeHdr^.Flags2;

    //--- get parent name if there is one
    ParentStr := ansistring(GetParentName(Source, NodeHdrChunk));

    //--- Instance
    if Assigned(InstData) then
    begin
      InstanceStr := StrPas(InstData);
      NameStr := NameStr + '.' + InstanceStr;
      FreeMem(InstData);
    end
    else
      InstanceStr := '';

    //--- Pivot
    if Assigned(PivotData) then
    begin
      Pivot := PivotData^;
      FreeMem(PivotData);
    end
    else
      Pivot := DefPoint3DS;

    //--- Bound
    if Assigned(BboxData) then
    begin
      BoundMin := BboxData^.Min;
      BoundMax := BboxData^.Max;
      FreeMem(BboxData);
    end
    else
    begin
      BoundMin := DefPoint3DS;
      BoundMax := DefPoint3DS;
    end;

    //--- MorphSmooth Angle
    if Assigned(MsData) then
    begin
      MSAngle := MsData^;
      FreeMem(MsData);
    end
    else
      MSAngle := 0;

    //--- Position
    NPKeys := PosKeys;
    if PosKeys <> 0 then
    begin
      PKeys := PosData^.KeyHdrList;
      Pos := PosData^.PositionList;
      NPFlag := PosData^.TrackHdr.Flags;
      FreeMem(PosData);
    end
    else
    begin
      PKeys := nil;
      Pos := nil;
      NPFlag := 0;
    end;

    //--- Rotation
    NRKeys := RotKeys;
    if RotKeys <> 0 then
    begin
      RKeys := RotData^.KeyHdrList;
      Rot := RotData^.RotationList;
      NRFlag := RotData^.TrackHdr.Flags;
      FreeMem(RotData);
    end
    else
    begin
      RKeys := nil;
      Rot := nil;
      NRFlag := 0;
    end;

    //--- Scale
    NSKeys := ScaleKeys;
    if ScaleKeys <> 0 then
    begin
      SKeys := ScaleData^.KeyHdrList;
      Scale := ScaleData^.ScaleList;
      NSFlag := ScaleData^.TrackHdr.Flags;
      FreeMem(ScaleData);
    end
    else
    begin
      SKeys := nil;
      Scale := nil;
      NSFlag := 0;
    end;

    //--- Morph
    NMKeys := MorphKeys;
    if MorphKeys <> 0 then
    begin
      MKeys := MorphData^.KeyHdrList;
      Morph := MorphData^.MorphList;
      NMFlag := MorphData^.TrackHdr.Flags;
      FreeMem(MorphData);
    end
    else
    begin
      MKeys := nil;
      Morph := nil;
      NMFlag := 0;
    end;

    NHKeys := HideKeys;
    if HideKeys <> 0 then
    begin
      HKeys := HideData^.KeyHdrList;
      NHFlag := HideData^.TrackHdr.Flags;
      FreeMem(HideData);
    end
    else
    begin
      HKeys := nil;
      NHFlag := 0;
    end;
  end;

  //-- ADDITIONAL Morph INFO HERE

  //--- free chunk data: only free those that arent being copied
  ReleaseChunk(ObjTag);
end;

//---------------------------------------------------------------------------------------------------------------------

function GetObjectMotionByName(const Source: TFile3DS; var DB: TDatabase3DS;
  const Name: string): TKFMesh3DS;

var
  ObjectChunk: PChunk3DS;

begin
  FillChar(Result, SizeOf(Result), 0);

  ObjectChunk := FindNodeTagByNameAndType(Source, DB, Name, OBJECT_NODE_TAG);
  if Assigned(ObjectChunk) then
    Result := GetObjectMotion(Source, ObjectChunk);
end;

//---------------------------------------------------------------------------------------------------------------------

function GetObjectMotionByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: cardinal): TKFMesh3DS;

var
  Chunk: PChunk3DS;

begin
  FillChar(Result, SizeOf(Result), 0);

  Chunk := FindNodeTagByIndexAndType(Source, DB, Index, OBJECT_NODE_TAG);
  if Assigned(Chunk) then
    Result := GetObjectMotion(Source, Chunk);
end;

//----------------- Omni Light animation ------------------------------------------------------------------------------

procedure InitOmnilightMotion(var Light: TKFOmni3DS; NewNPKeys, NewNCKeys: cardinal);

var
  I: integer;

begin
  with Light do
  begin
    if Assigned(PKeys) then
      FreeMem(PKeys);
    if Assigned(Pos) then
      FreeMem(Pos);
    if Assigned(CKeys) then
      FreeMem(CKeys);
    if Assigned(Color) then
      FreeMem(Color);

    FillChar(Light, SizeOf(Light), 0);
    NPKeys := NewNPKeys;
    NCKeys := NewNCKeys;

    if NPKeys <> 0 then
    begin
      NPFlag := TrackSingle3DS;

      PKeys := AllocMem(NPKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NPKeys - 1 do
        PKeys^[I] := DefKeyHeader3DS;

      Pos := AllocMem(NPKeys * SizeOf(TPoint3DS));
      for I := 0 to NPKeys - 1 do
        Pos^[I] := DefPoint3DS;
    end;

    if NCKeys <> 0 then
    begin
      NCFlag := TrackSingle3DS;

      CKeys := AllocMem(NCKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NCKeys - 1 do
        CKeys^[I] := DefKeyHeader3DS;

      Color := AllocMem(NCKeys * SizeOf(TFColor3DS));
      for I := 0 to NCKeys - 1 do
      begin
        Color^[I].R := 1;
        Color^[I].G := 1;
        Color^[I].B := 1;
      end;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ReleaseOmnilightMotion(Light: PKFOmni3DS);

begin
  if Assigned(Light) then
  begin
    with Light^ do
    begin
      if Assigned(PKeys) then
        FreeMem(PKeys);
      if Assigned(Pos) then
        FreeMem(Pos);
      if Assigned(CKeys) then
        FreeMem(CKeys);
      if Assigned(Color) then
        FreeMem(Color);
    end;
    Dispose(Light);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetOmnilightNodeCount(const Source: TFile3DS; var DB: TDatabase3DS): cardinal;

begin
  Result := GetGenericNodeCount(Source, DB, LIGHT_NODE_TAG);
end;

//---------------------------------------------------------------------------------------------------------------------

procedure GetOmnilightNodeNameList(const Source: TFile3DS; var DB: TDatabase3DS;
  List: TStringList);

begin
  GetGenericNodeNameList(Source, DB, LIGHT_NODE_TAG, List);
end;

//---------------------------------------------------------------------------------------------------------------------

function GetOmnilightMotion(const Source: TFile3DS; OmniChunk: PChunk3DS): TKFOmni3DS;

  // Gets Omnilight keyframe information from chunk

  //  L_TARGET
  //  NODE_ID
  //  NODE_HDR
  //  APP_DATA
  //  POS_TRACK
  //  COL_TRACK
  //  HOT_TRACK
  //  FALL_TRACK
  //  ROLL_TRACK

var
  NodeHdrChunk, PosChunk, ColChunk: PChunk3DS;
  PosKeys, ColKeys: cardinal;

begin
  PosKeys := 0;
  ColKeys := 0;

  // get information from chunks
  // search children of OmniLight chunk
  NodeHdrChunk := FindChunk(OmniChunk, NODE_HDR);
  PosChunk := FindChunk(OmniChunk, POS_TRACK_TAG);
  ColChunk := FindChunk(OmniChunk, COL_TRACK_TAG);

  Source.ReadChunkData(NodeHdrChunk);

  if Assigned(PosChunk) then
  begin
    Source.ReadChunkData(PosChunk);
    PosKeys := PosChunk^.Data.PosTrackTag^.TrackHdr.KeyCount;
  end;

  if Assigned(ColChunk) then
  begin
    Source.ReadChunkData(ColChunk);
    ColKeys := ColChunk^.Data.ColTrackTag^.TrackHdr.KeyCount;
  end;

  // set-up and fill-in the TKFOmni3DS structure
  FillChar(Result, SizeOf(Result), $00);
  InitOmnilightMotion(Result, PosKeys, ColKeys);
  with Result do
  begin
    //--- Header Information
    Name := ansistring(NodeHdrChunk^.Data.NodeHdr^.ObjNameStr);
    Flags1 := NodeHdrChunk^.Data.NodeHdr^.Flags1;
    Flags2 := NodeHdrChunk^.Data.NodeHdr^.Flags2;
    Parent := ansistring(GetParentName(Source, NodeHdrChunk));

    //--- Position Information
    if PosKeys <> 0 then
    begin
      NPFlag := PosChunk^.Data.PosTrackTag^.TrackHdr.Flags;
      Move(PosChunk^.Data.PosTrackTag^.KeyHdrList^, PKeys^, PosKeys *
        SizeOf(TKeyHeader3DS));
      Move(PosChunk^.Data.PosTrackTag^.PositionList^, Pos^, PosKeys * SizeOf(TPoint3DS));
    end;

    //--- Color Information
    if ColKeys <> 0 then
    begin
      NCFlag := PosChunk^.Data.ColTrackTag^.TrackHdr.Flags;
      Move(ColChunk^.Data.ColTrackTag^.KeyHdrList^, CKeys^, ColKeys *
        SizeOf(TKeyHeader3DS));
      Move(ColChunk^.Data.ColTrackTag^.ColorList^, Color^, ColKeys * SizeOf(TFColor3DS));
    end;

    //--- Free Chunk Data
    if Assigned(NodeHdrChunk) then
      FreeChunkData(NodeHdrChunk);
    if Assigned(PosChunk) then
      FreeChunkData(PosChunk);
    if Assigned(ColChunk) then
      FreeChunkData(ColChunk);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetOmnilightMotionByName(const Source: TFile3DS; var DB: TDatabase3DS;
  const Name: string): TKFOmni3DS;

var
  Chunk: PChunk3DS;

begin
  FillChar(Result, SizeOf(Result), 0);

  Chunk := FindNamedAndTaggedChunk(Source, DB, Name, LIGHT_NODE_TAG);
  if Assigned(Chunk) then
    Result := GetOmnilightMotion(Source, Chunk);
end;

//---------------------------------------------------------------------------------------------------------------------

function GetOmnilightMotionByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: cardinal): TKFOmni3DS;

var
  Chunk: PChunk3DS;
  List: TStringList;

begin
  FillChar(Result, SizeOf(Result), 0);

  List := TStringList.Create;
  try
    GetOmnilightNodeNameList(Source, DB, List);

    if Index < cardinal(List.Count) then
    begin
      Chunk := FindNamedAndTaggedChunk(Source, DB, List[Index], LIGHT_NODE_TAG);
      if Assigned(Chunk) then
        Result := GetOmnilightMotion(Source, Chunk);
    end;
  finally
    List.Free;
  end;
end;

//----------------- Spot Light animation ------------------------------------------------------------------------------

procedure InitSpotlightMotion(var Spot: TKFSpot3DS;
  NewNPKeys, //  Number of position keys
  NewNCKeys, //  Number of Color keys
  NewNHKeys, //  Number of hot spot angle keys
  NewNFKeys, //  Number of falloff angle keys
  NewNRKeys, //  Number of roll keys
  NewNTKeys: cardinal); //  Number of target position keys

var
  I: cardinal;

begin
  with Spot do
  begin
    if Assigned(PKeys) then
      FreeMem(PKeys);
    if Assigned(Pos) then
      FreeMem(Pos);
    if Assigned(CKeys) then
      FreeMem(CKeys);
    if Assigned(Color) then
      FreeMem(Color);
    if Assigned(HKeys) then
      FreeMem(HKeys);
    if Assigned(Hot) then
      FreeMem(Hot);
    if Assigned(FKeys) then
      FreeMem(FKeys);
    if Assigned(Fall) then
      FreeMem(Fall);
    if Assigned(RKeys) then
      FreeMem(RKeys);
    if Assigned(Roll) then
      FreeMem(Roll);
    if Assigned(TKeys) then
      FreeMem(TKeys);
    if Assigned(TPos) then
      FreeMem(TPos);

    FillChar(Spot, SizeOf(Spot), 0);
    NPKeys := NewNPKeys;
    NCKeys := NewNCKeys;
    NFKeys := NewNFKeys;
    NTKeys := NewNTKeys;
    NHKeys := NewNHKeys;
    NRKeys := NewNRKeys;

    //--- POSITION KEYS -----------------------------------------------------
    if NPKeys <> 0 then
    begin
      NPFlag := TrackSingle3DS;

      PKeys := AllocMem(NPKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NPKeys - 1 do
        PKeys^[I] := DefKeyHeader3DS;

      Pos := AllocMem(NPKeys * SizeOf(TPoint3DS));
      for I := 0 to NPKeys - 1 do
        Pos^[I] := DefPoint3DS;
    end;

    //--- Color KEYS ----------------------------------------------------------
    if NCKeys <> 0 then
    begin
      NCFlag := TrackSingle3DS;
      CKeys := AllocMem(NCKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NCKeys - 1 do
        CKeys^[I] := DefKeyHeader3DS;

      Color := AllocMem(NCKeys * SizeOf(TFColor3DS));
      // Initialization is unclear, even the original developers didn't know what's up.
      // They put this part in an '#ifdef LATER #endif' block. ml
      // for I := 0 to NCKeys - 1 do Color[I] := localDColor.bDefFColor3DS;
    end;

    //---Hot-Spot ANGLE KEYS---------------------------------------------------
    if NHKeys <> 0 then
    begin
      NHFlag := TrackSingle3DS;

      HKeys := AllocMem(NHKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NHKeys - 1 do
        HKeys^[I] := DefKeyHeader3DS;

      Hot := AllocMem(NHKeys * SizeOf(single));
      // default Hot Spot ange 90.0 for now, get real value later (1..174.5)
      for I := 0 to NHKeys - 1 do
        Hot^[I] := 90;
    end;

    //---FALLOFF ANGLE KEYS----------------------------------------------------
    if NFKeys <> 0 then
    begin
      NFFlag := TrackSingle3DS;

      FKeys := AllocMem(NFKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NFKeys - 1 do
        FKeys^[I] := DefKeyHeader3DS;

      Fall := AllocMem(NFKeys * SizeOf(single));

      // default falloff ange 90.0 for now, get real value later (1..175)
      for I := 0 to NFKeys - 1 do
        Fall^[I] := 90;
    end;

    //--- Roll KEYS ----------------------------------------------------------
    if NRKeys <> 0 then
    begin
      NRFlag := TrackSingle3DS;

      RKeys := AllocMem(NRKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NRKeys - 1 do
        RKeys^[I] := DefKeyHeader3DS;

      Roll := AllocMem(NRKeys * SizeOf(single));
      for I := 0 to NRKeys - 1 do
        Roll^[I] := 0;
    end;

    //---L_TARGET Pos KEYS ------------------------------------------------
    if NTKeys <> 0 then
    begin
      NTFlag := TrackSingle3DS;

      TKeys := AllocMem(NTKeys * SizeOf(TKeyHeader3DS));
      for I := 0 to NTKeys - 1 do
        TKeys^[I] := DefKeyHeader3DS;

      TPos := AllocMem(NTKeys * SizeOf(TPoint3DS));
      // default target position, 0, 0, 0  sjw fix later if necessary
      for I := 0 to NTKeys - 1 do
        TPos^[I] := DefPoint3DS;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

procedure ReleaseSpotlightMotion(Spot: PKFSpot3DS);

begin
  if Assigned(Spot) then
  begin
    with Spot^ do
    begin
      if Assigned(PKeys) then
        FreeMem(PKeys);
      if Assigned(Pos) then
        FreeMem(Pos);
      if Assigned(CKeys) then
        FreeMem(CKeys);
      if Assigned(Color) then
        FreeMem(Color);
      if Assigned(HKeys) then
        FreeMem(HKeys);
      if Assigned(Hot) then
        FreeMem(Hot);
      if Assigned(FKeys) then
        FreeMem(FKeys);
      if Assigned(Fall) then
        FreeMem(Fall);
      if Assigned(RKeys) then
        FreeMem(RKeys);
      if Assigned(Roll) then
        FreeMem(Roll);
      if Assigned(TKeys) then
        FreeMem(TKeys);
      if Assigned(TPos) then
        FreeMem(TPos);
    end;
    Dispose(Spot);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetSpotlightNodeCount(const Source: TFile3DS; var DB: TDatabase3DS): cardinal;

begin
  Result := GetGenericNodeCount(Source, DB, SPOTLIGHT_NODE_TAG);
end;

//---------------------------------------------------------------------------------------------------------------------

procedure GetSpotlightNodeNameList(const Source: TFile3DS; var DB: TDatabase3DS;
  List: TStringList);

begin
  GetGenericNodeNameList(Source, DB, SPOTLIGHT_NODE_TAG, List);
end;

//---------------------------------------------------------------------------------------------------------------------

function GetSpotlightMotion(const Source: TFile3DS;
  SpotChunk, TargetChunk: PChunk3DS): TKFSpot3DS;

  // gets Spotlight keyframe information from chunk

  // L_TARGET
  //  ...
  // NODE_HDR
  // APP_DATA
  // POS_TRACK
  // COL_TRACK
  // HOT_TRACK
  // FALL_TRACK
  // ROLL_TRACK

var
  NodeHdrChunk, PosChunk, ColChunk, TargetPosChunk, HotChunk,
  FallChunk, RollChunk, TargetHdrChunk: PChunk3DS;
  PosKeys, ColKeys, HotKeys, FallKeys, RollKeys, TargetKeys: cardinal;

begin
  TargetPosChunk := nil;
  TargetHdrChunk := nil;
  PosKeys := 0;
  ColKeys := 0;
  HotKeys := 0;
  FallKeys := 0;
  RollKeys := 0;
  TargetKeys := 0;

  // get information from chunks
  // search children of Spotlight chunk
  NodeHdrChunk := FindChunk(SpotChunk, NODE_HDR);
  PosChunk := FindChunk(SpotChunk, POS_TRACK_TAG);
  ColChunk := FindChunk(SpotChunk, COL_TRACK_TAG);
  HotChunk := FindChunk(SpotChunk, HOT_TRACK_TAG);
  FallChunk := FindChunk(SpotChunk, FALL_TRACK_TAG);
  RollChunk := FindChunk(SpotChunk, ROLL_TRACK_TAG);

  Source.ReadChunkData(NodeHdrChunk);

  if Assigned(PosChunk) then
  begin
    Source.ReadChunkData(PosChunk);
    PosKeys := PosChunk^.Data.PosTrackTag^.TrackHdr.KeyCount;
  end;

  if Assigned(ColChunk) then
  begin
    Source.ReadChunkData(ColChunk);
    ColKeys := ColChunk^.Data.ColTrackTag^.TrackHdr.KeyCount;
  end;

  if Assigned(HotChunk) then
  begin
    Source.ReadChunkData(HotChunk);
    HotKeys := HotChunk^.Data.HotTrackTag^.TrackHdr.KeyCount;
  end;

  if Assigned(FallChunk) then
  begin
    Source.ReadChunkData(FallChunk);
    FallKeys := FallChunk^.Data.FallTrackTag^.TrackHdr.KeyCount;
  end;

  if Assigned(RollChunk) then
  begin
    Source.ReadChunkData(RollChunk);
    RollKeys := RollChunk^.Data.RollTrackTag^.TrackHdr.KeyCount;
  end;

  if Assigned(TargetChunk) then
  begin
    TargetHdrChunk := FindChunk(TargetChunk, NODE_HDR);
    if Assigned(TargetHdrChunk) then
      Source.ReadChunkData(TargetHdrChunk);

    TargetPosChunk := FindChunk(TargetChunk, POS_TRACK_TAG);
    if Assigned(TargetPosChunk) then
    begin
      Source.ReadChunkData(TargetPosChunk);
      TargetKeys := TargetPosChunk^.Data.PosTrackTag^.TrackHdr.KeyCount;
    end;
  end;

  // set-up and fill-in the TKFSpot3DS structure
  InitSpotlightMotion(Result, PosKeys, ColKeys, HotKeys, FallKeys, RollKeys, TargetKeys);

  with Result do
  begin
    // header Information
    Name := ansistring(NodeHdrChunk^.Data.NodeHdr^.ObjNameStr);
    Flags1 := NodeHdrChunk^.Data.NodeHdr^.Flags1;
    Flags2 := NodeHdrChunk^.Data.NodeHdr^.Flags2;

    // get parent name if there is one
    Parent := ansistring(GetParentName(Source, NodeHdrChunk));
    TParent := ansistring(GetParentName(Source, TargetHdrChunk));

    if Assigned(TargetHdrChunk) then
    begin
      TFlags1 := TargetHdrChunk^.Data.NodeHdr^.Flags1;
      TFlags2 := TargetHdrChunk^.Data.NodeHdr^.Flags2;
    end
    else
    begin
      TFlags1 := 0;
      TFlags2 := 0;
    end;

    // target information
    if TargetKeys <> 0 then
    begin
      NTFlag := TargetPosChunk^.Data.PosTrackTag^.TrackHdr.Flags;
      Move(TargetPosChunk^.Data.PosTrackTag^.KeyHdrList^, TKeys^,
        TargetKeys * SizeOf(TKeyHeader3DS));
      Move(TargetPosChunk^.Data.PosTrackTag^.PositionList^, TPos^,
        TargetKeys * SizeOf(TPoint3DS));
    end;

    // position information
    if PosKeys <> 0 then
    begin
      NPFlag := PosChunk^.Data.PosTrackTag^.TrackHdr.Flags;
      Move(PosChunk^.Data.PosTrackTag^.KeyHdrList^, PKeys^, PosKeys *
        SizeOf(TKeyHeader3DS));
      Move(PosChunk^.Data.PosTrackTag^.PositionList^, Pos^, PosKeys * SizeOf(TPoint3DS));
    end;

    // color information
    if ColKeys <> 0 then
    begin
      NCFlag := ColChunk^.Data.ColTrackTag^.TrackHdr.Flags;
      Move(ColChunk^.Data.ColTrackTag^.KeyHdrList^, CKeys^, ColKeys *
        SizeOf(TKeyHeader3DS));
      Move(ColChunk^.Data.ColTrackTag^.ColorList^, Color^, ColKeys * SizeOf(TFColor3DS));
    end;

    // hot spot information
    if HotKeys <> 0 then
    begin
      NHFlag := HotChunk^.Data.HotTrackTag^.TrackHdr.Flags;
      Move(HotChunk^.Data.HotTrackTag^.KeyHdrList^, HKeys^, HotKeys *
        SizeOf(TKeyHeader3DS));
      Move(HotChunk^.Data.HotTrackTag^.HotSpotAngleList^, Hot^, HotKeys *
        SizeOf(single));
    end;

    // falloff information
    if FallKeys <> 0 then
    begin
      NFFlag := FallChunk^.Data.FallTrackTag^.TrackHdr.Flags;
      Move(FallChunk^.Data.FallTrackTag^.KeyHdrList^, FKeys^, FallKeys *
        SizeOf(TKeyHeader3DS));
      Move(FallChunk^.Data.FallTrackTag^.FallOffAngleList^, Fall^,
        FallKeys * SizeOf(single));
    end;

    // roll track Information
    if RollKeys <> 0 then
    begin
      NRFlag := RollChunk^.Data.RollTrackTag^.TrackHdr.Flags;
      Move(RollChunk^.Data.RollTrackTag^.KeyHdrList^, RKeys^, RollKeys *
        SizeOf(TKeyHeader3DS));
      Move(RollChunk^.Data.RollTrackTag^.RollAngleList^, Roll^,
        RollKeys * SizeOf(single));
    end;
  end;

  //--- Free Chunk Data
  if Assigned(NodeHdrChunk) then
    FreeChunkData(NodeHdrChunk);
  if Assigned(PosChunk) then
    FreeChunkData(PosChunk);
  if Assigned(ColChunk) then
    FreeChunkData(ColChunk);
  if Assigned(HotChunk) then
    FreeChunkData(HotChunk);
  if Assigned(FallChunk) then
    FreeChunkData(FallChunk);
  if Assigned(RollChunk) then
    FreeChunkData(RollChunk);
  if Assigned(TargetPosChunk) then
    FreeChunkData(TargetPosChunk);
end;

//---------------------------------------------------------------------------------------------------------------------

function GetSpotlightMotionByName(const Source: TFile3DS; var DB: TDatabase3DS;
  const Name: string): TKFSpot3DS;

var
  SpotlightChunk, TargetChunk: PChunk3DS;

begin
  FillChar(Result, SizeOf(Result), 0);

  SpotlightChunk := FindNamedAndTaggedChunk(Source, DB, Name, SPOTLIGHT_NODE_TAG);
  if Assigned(SpotlightChunk) then
  begin
    TargetChunk := FindNamedAndTaggedChunk(Source, DB, Name, L_TARGET_NODE_TAG);
    Result := GetSpotlightMotion(Source, SpotlightChunk, TargetChunk);
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetSpotlightMotionByIndex(const Source: TFile3DS; var DB: TDatabase3DS;
  Index: cardinal): TKFSpot3DS;

var
  SpotChunk, TargetChunk: PChunk3DS;
  List: TStringList;

begin
  FillChar(Result, SizeOf(Result), 0);

  List := TStringList.Create;
  try
    GetSpotlightNodeNameList(Source, DB, List);
    if Index < cardinal(List.Count) then
    begin
      SpotChunk := FindNamedAndTaggedChunk(Source, DB, List[Index], SPOTLIGHT_NODE_TAG);
      if Assigned(SpotChunk) then
      begin
        TargetChunk := FindNamedAndTaggedChunk(Source, DB, List[Index],
          L_TARGET_NODE_TAG);
        if Assigned(TargetChunk) then
          Result := GetSpotlightMotion(Source, SpotChunk, TargetChunk);
      end;
    end;
  finally
    List.Free;
  end;
end;

//----------------- Versioninformation --------------------------------------------------------------------------------

function GetM3dMagicRelease(const Source: TFile3DS; var DB: TDatabase3DS): TReleaseLevel;

  // Scans the database for M3D_VERSION chunk and returnes its release

var
  Chunk: PChunk3DS;

begin
  Result := rlReleaseNotKnown;
  // If the database is a 3DS file
  if DB.TopChunk^.Tag = M3DMAGIC then
  begin
    Chunk := FindChunk(DB.TopChunk, M3D_VERSION);
    if Assigned(Chunk) then
    begin
      Source.ReadChunkData(Chunk);
      case Chunk^.Data.M3dVersion^ of
        1:
          Result := rlRelease1;
        2:
          Result := rlRelease2;
        3:
          Result := rlRelease3;
        else
          Result := rlReleaseNotKnown;
      end;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetMeshRelease(const Source: TFile3DS; var DB: TDatabase3DS): TReleaseLevel;

  // Scans the database for MESH_VERSION chunk and returnes its release

var
  Chunk: PChunk3DS;

begin
  Result := rlReleaseNotKnown;
  // If the database is a 3DS file
  if (DB.TopChunk^.Tag = M3DMAGIC) or (DB.TopChunk^.Tag = CMAGIC) then
  begin
    Chunk := FindChunk(DB.TopChunk, MESH_VERSION);
    if Assigned(Chunk) then
    begin
      Source.ReadChunkData(Chunk);
      case Chunk^.Data.MeshVersion^ of
        1:
          Result := rlRelease1;
        2:
          Result := rlRelease2;
        3:
          Result := rlRelease3;
        else
          Result := rlReleaseNotKnown;
      end;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetKfRelease(const Source: TFile3DS; var DB: TDatabase3DS): TReleaseLevel;

  // Scans the database for KFHDR chunk and returnes its release level

var
  KFChunk, Chunk: PChunk3DS;

begin
  Result := rlReleaseNotKnown;
  // If the database is a 3DS file
  if (DB.TopChunk^.Tag = M3DMAGIC) or (DB.TopChunk^.Tag = CMAGIC) then
  begin
    KFChunk := FindChunk(DB.TopChunk, KFDATA);
    if Assigned(KFChunk) then
      Chunk := FindChunk(DB.TopChunk, KFHDR)
    else
      Chunk := nil;
    if Assigned(Chunk) then
    begin
      Source.ReadChunkData(Chunk);
      case Chunk^.Data.KFHdr^.Revision of
        1:
          Result := rlRelease1;
        2:
          Result := rlRelease2;
        3:
          Result := rlRelease3;
        else
          Result := rlReleaseNotKnown;
      end;
    end;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

function GetDatabaseRelease(const Source: TFile3DS; var DB: TDatabase3DS): TReleaseLevel;

begin
  case DB.TopChunk^.Tag of
    M3DMAGIC:
      Result := GetM3dMagicRelease(Source, DB);
    CMAGIC:
      Result := GetMeshRelease(Source, DB);
    MLIBMAGIC:
      Result := rlRelease3;
    else
      Result := rlReleaseNotKnown;
  end;
end;

//---------------------------------------------------------------------------------------------------------------------

end.

