Formatx.Q3MD3.pas 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. //
  2. // The graphics engine GXScene https://github.com/glscene
  3. //
  4. unit Formatx.Q3MD3;
  5. (* Helper classes and methods for Quake3 MD3 actors *)
  6. interface
  7. {$I Stage.Defines.inc}
  8. uses
  9. System.Classes,
  10. System.SysUtils,
  11. Stage.VectorTypes,
  12. Stage.VectorGeometry,
  13. GXS.VectorLists,
  14. GXS.ApplicationFileIO,
  15. GXS.VectorFileObjects,
  16. GXS.Material,
  17. Formatx.MD3;
  18. type
  19. (* This class is used to extract the tag transform information
  20. stored in the MD3 files. The data is used to offset each
  21. part of the model based on the parent parts animation state. *)
  22. TMD3TagList = class
  23. private
  24. FTags: array of TMD3Tag;
  25. FNumTags, FNumFrames: Integer;
  26. function GetTag(index: Integer): TMD3Tag;
  27. public
  28. procedure LoadFromFile(FileName: String);
  29. procedure LoadFromStream(AStream: TStream);
  30. function GetTransform(TagName: string; Frame: Integer): TMatrix4f;
  31. property TagCount: Integer read FNumTags;
  32. property FrameCount: Integer read FNumFrames;
  33. property Tags[index: Integer]: TMD3Tag read GetTag;
  34. end;
  35. (* These procedures are helpers to load the Quake3 animation file data
  36. into an animation list. The NamePrefix parameter is used to determine
  37. which class of animation is extracted. eg NamePrefix='TORSO' will load
  38. all animations starting with 'TORSO_' like 'TORSO_STAND' *)
  39. procedure LoadQ3Anims(Animations: TgxActorAnimations; FileName: string;
  40. NamePrefix: string); overload;
  41. procedure LoadQ3Anims(Animations: TgxActorAnimations; Strings: TStrings;
  42. NamePrefix: string); overload;
  43. (* Quake3 Skin loading procedure. Use this procedure to apply textures
  44. to a GLActor. This doens't use the actors original preloaded materials so
  45. it may be a good idea to clear the actors material library before
  46. running this to keep everything nice and clean. *)
  47. procedure LoadQ3Skin(FileName: string; Actor: TgxActor);
  48. implementation // -------------------------------------------------------------
  49. procedure LoadQ3Anims(Animations: TgxActorAnimations; FileName: string;
  50. NamePrefix: string);
  51. var
  52. AnimStrings: TStrings;
  53. begin
  54. AnimStrings := TStringList.Create;
  55. AnimStrings.LoadFromFile(FileName);
  56. LoadQ3Anims(Animations, AnimStrings, NamePrefix);
  57. AnimStrings.Free;
  58. end;
  59. procedure LoadQ3Anims(Animations: TgxActorAnimations; Strings: TStrings;
  60. NamePrefix: string);
  61. var
  62. anim: TStringList;
  63. val: array [0 .. 3] of Integer;
  64. strindex, valindex, i: Integer;
  65. GotValues: Boolean;
  66. commatext, str1: string;
  67. TorsoStartFrame, LegsStartFrame: Integer;
  68. // Used to Fix LEGS Frame values red from CFG file
  69. function StrIsNumber(str: string): Boolean;
  70. var
  71. i: Integer;
  72. begin
  73. result := false;
  74. for i := 1 to Length(str) do
  75. if (Ord(str[i]) >= Ord('0')) and (Ord(str[i]) <= Ord('9')) then
  76. result := true
  77. else
  78. begin
  79. result := false;
  80. break;
  81. end;
  82. end;
  83. begin
  84. anim := TStringList.Create;
  85. TorsoStartFrame := 0;
  86. LegsStartFrame := 0;
  87. FillChar(val[0], SizeOf(val), $00);
  88. for strindex := 0 to Strings.Count - 1 do
  89. begin
  90. commatext := Strings.Strings[strindex];
  91. while Pos(' ', commatext) > 0 do
  92. commatext := StringReplace(commatext, ' ', ' ', [rfReplaceAll]);
  93. commatext := StringReplace(commatext, ' ', ',', [rfReplaceAll]);
  94. anim.commatext := commatext;
  95. GotValues := false;
  96. valindex := 0;
  97. str1 := '';
  98. if anim.Count >= 5 then
  99. begin
  100. for i := 0 to anim.Count - 1 do
  101. begin
  102. if GotValues then
  103. begin
  104. // Store start values to Fix LEGS
  105. if (TorsoStartFrame = 0) and
  106. (Pos('TORSO_', Uppercase(anim.Strings[i])) > 0) then
  107. TorsoStartFrame := val[0];
  108. if (LegsStartFrame = 0) and
  109. (Pos('LEGS_', Uppercase(anim.Strings[i])) > 0) then
  110. LegsStartFrame := val[0];
  111. if (anim.Strings[i] <> '//') and
  112. (Pos(NamePrefix + '_', anim.Strings[i]) > 0) then
  113. begin
  114. str1 := StringReplace(anim.Strings[i], '//', '', [rfReplaceAll]);
  115. break;
  116. end;
  117. end
  118. else
  119. begin
  120. if StrIsNumber(anim.Strings[i]) then
  121. begin
  122. val[valindex] := StrToInt(anim.Strings[i]);
  123. Inc(valindex);
  124. if valindex = 4 then
  125. GotValues := true;
  126. end
  127. else
  128. break;
  129. end;
  130. end;
  131. end;
  132. if GotValues and (str1 <> '') then
  133. begin
  134. // Values ready for new animation.
  135. with Animations.Add do
  136. begin
  137. // Fix frame value for Legs
  138. if Uppercase(NamePrefix) = 'LEGS' then
  139. val[0] := val[0] - LegsStartFrame + TorsoStartFrame;
  140. Name := str1;
  141. StartFrame := val[0];
  142. EndFrame := val[0] + val[1] - 1;
  143. Reference := aarMorph;
  144. // Need a way in TgxActorAnimation to tell whether it is
  145. // a looping type animation or a play once type and
  146. // the framerate (interval) it uses. Both of these can
  147. // be determined here and loaded.
  148. end;
  149. end;
  150. end;
  151. anim.Free;
  152. end;
  153. procedure LoadQ3Skin(FileName: string; Actor: TgxActor);
  154. const
  155. // This list can be expanded if necessary
  156. ExtList: array [0 .. 3] of string = ('.jpg', '.jpeg', '.tga', '.bmp');
  157. var
  158. SkinStrings, temp: TStrings;
  159. i, j: Integer;
  160. libmat: TgxLibMaterial;
  161. mesh: TgxMeshObject;
  162. texture, textureNoDir: string;
  163. textureFound, meshFound: Boolean;
  164. function GetMeshObjectByName(MeshObjects: TgxMeshObjectList; Name: string;
  165. var mesh: TgxMeshObject): Boolean;
  166. var
  167. i: Integer;
  168. begin
  169. result := false;
  170. if (trim(Name) = '') or not Assigned(MeshObjects) then
  171. exit;
  172. for i := 0 to MeshObjects.Count - 1 do
  173. begin
  174. if MeshObjects[i].Name = Name then
  175. begin
  176. mesh := MeshObjects[i];
  177. result := true;
  178. break;
  179. end;
  180. end;
  181. end;
  182. begin
  183. if (not FileExists(FileName)) or (not Assigned(Actor)) then
  184. exit;
  185. if (not Assigned(Actor.MaterialLibrary)) then
  186. exit;
  187. SkinStrings := TStringList.Create;
  188. temp := TStringList.Create;
  189. temp.LoadFromFile(FileName);
  190. for i := 0 to temp.Count - 1 do
  191. begin
  192. SkinStrings.commatext := temp.Strings[i];
  193. if SkinStrings.Count > 1 then
  194. begin
  195. libmat := Actor.MaterialLibrary.Materials.GetLibMaterialByName
  196. (SkinStrings.Strings[1]);
  197. meshFound := GetMeshObjectByName(Actor.MeshObjects,
  198. SkinStrings.Strings[0], mesh);
  199. if meshFound then
  200. begin
  201. if not Assigned(libmat) then
  202. begin
  203. libmat := Actor.MaterialLibrary.Materials.Add;
  204. libmat.Name := SkinStrings.Strings[1];
  205. // Search for the texture file
  206. textureFound := false;
  207. for j := 0 to Length(ExtList) - 1 do
  208. begin
  209. texture := StringReplace(SkinStrings.Strings[1], '/', '\',
  210. [rfReplaceAll]);
  211. texture := ChangeFileExt(texture, ExtList[j]);
  212. if FileExists(texture) then
  213. begin
  214. libmat.Material.texture.Image.LoadFromFile(texture);
  215. libmat.Material.texture.Disabled := false;
  216. textureFound := true;
  217. end
  218. else
  219. begin
  220. textureNoDir := ExtractFileName(texture);
  221. if FileExists(textureNoDir) then
  222. begin
  223. libmat.Material.texture.Image.LoadFromFile(textureNoDir);
  224. libmat.Material.texture.Disabled := false;
  225. textureFound := true;
  226. end;
  227. end;
  228. if textureFound then
  229. break;
  230. end;
  231. end;
  232. for j := 0 to mesh.FaceGroups.Count - 1 do
  233. mesh.FaceGroups[j].MaterialName := libmat.Name;
  234. end;
  235. end;
  236. end;
  237. temp.Free;
  238. SkinStrings.Free;
  239. end;
  240. // ------------------
  241. // ------------------ TMD3TagList ------------------
  242. // ------------------
  243. procedure TMD3TagList.LoadFromFile(FileName: String);
  244. var
  245. fs: TStream;
  246. begin
  247. if FileName <> '' then
  248. begin
  249. fs := TFileStream.Create(FileName, fmOpenRead + fmShareDenyWrite);
  250. try
  251. LoadFromStream(fs);
  252. finally
  253. fs.Free;
  254. end;
  255. end;
  256. end;
  257. procedure TMD3TagList.LoadFromStream(AStream: TStream);
  258. var
  259. MD3Header: TMD3Header;
  260. begin
  261. // Load the MD3 header
  262. AStream.Read(MD3Header, SizeOf(MD3Header));
  263. // Test for correct file ID and version
  264. Assert(MD3Header.fileID = 'IDP3', 'Incorrect MD3 file ID');
  265. Assert(MD3Header.version = 15, 'Incorrect MD3 version number');
  266. // Get the tags from the file
  267. FNumTags := MD3Header.numTags;
  268. FNumFrames := MD3Header.numFrames;
  269. SetLength(FTags, FNumTags * FNumFrames);
  270. AStream.Position := MD3Header.tagStart;
  271. AStream.Read(FTags[0], FNumTags * FNumFrames * SizeOf(TMD3Tag));
  272. end;
  273. function TMD3TagList.GetTag(index: Integer): TMD3Tag;
  274. begin
  275. result := FTags[index];
  276. end;
  277. function TMD3TagList.GetTransform(TagName: string; Frame: Integer): TMatrix4f;
  278. var
  279. TagIdx, i, j: Integer;
  280. Tag: TMD3Tag;
  281. begin
  282. result := IdentityHMGMatrix;
  283. TagIdx := -1;
  284. for i := 0 to FNumTags do
  285. if lowercase(trim(TagName)) = lowercase(trim(string(FTags[i].strName))) then
  286. begin
  287. TagIdx := i;
  288. break;
  289. end;
  290. if TagIdx = -1 then
  291. exit;
  292. Tag := FTags[TagIdx + Frame * FNumTags];
  293. for j := 0 to 2 do
  294. for i := 0 to 2 do
  295. result.V[i].V[j] := Tag.rotation.V[i].V[j];
  296. for i := 0 to 2 do
  297. result.W.V[i] := Tag.vPosition.V[i];
  298. end;
  299. //---------------------------------------------------------------------------
  300. end.