// // The graphics engine GLXEngine. The unit of GXScene for Delphi // unit GXS.FileMD5; (* Doom3 MD5 mesh and animation vector file format implementation *) interface uses System.Classes, System.SysUtils, GXS.VectorFileObjects, Stage.Utils, GXS.ApplicationFileIO, Stage.VectorTypes, Stage.VectorGeometry, GXS.VectorLists; type TgxMD5VectorFile = class(TgxVectorFile) private FMD5String, FTempString, FBoneNames: TStringList; FCurrentPos: Integer; FBasePose: TgxSkeletonFrame; FFramePositions: TgxAffineVectorList; FFrameQuaternions: TgxQuaternionList; FJointFlags: TgxIntegerList; FNumFrames, FFirstFrame, FFrameRate, FNumJoints: Integer; function ReadLine: String; public class function Capabilities: TDataFileCapabilities; override; procedure LoadFromStream(aStream: TStream); override; end; var vMD5TextureExtensions: TStringList; // ------------------------------------------------------------------ implementation // ------------------------------------------------------------------ // ----------- // ----------- TgxMD5VectorFile ----------- // ----------- function TgxMD5VectorFile.ReadLine: String; begin Result := ''; if not Assigned(FMD5String) then exit; if FCurrentPos >= FMD5String.Count then exit; repeat Result := FMD5String[FCurrentPos]; Result := StringReplace(Result, '(', '', [rfReplaceAll]); Result := StringReplace(Result, ')', '', [rfReplaceAll]); Result := Trim(Result); Inc(FCurrentPos); until (Result <> '') or (FCurrentPos >= FMD5String.Count); end; class function TgxMD5VectorFile.Capabilities: TDataFileCapabilities; begin Result := [dfcRead]; end; procedure TgxMD5VectorFile.LoadFromStream(aStream: TStream); procedure AllocateMaterial(var shader: String); { const cTexType : array[0..2] of String = ('_local', '_d', '_s'); var shader_nopath, temp : String; libmat : TgxLibMaterial; i, j : Integer;// } begin { if Assigned(Owner.MaterialLibrary) then begin shader:=StringReplace(shader,'/','\',[rfReplaceAll]); if not DirectoryExists(ExtractFilePath(shader)) then shader:=ExtractFileName(shader); if not Assigned(Owner.MaterialLibrary.Materials.GetLibMaterialByName(shader)) then begin libmat:=Owner.MaterialLibrary.Materials.Add; libmat.Name:=shader; for i:=0 to High(cTexType) do begin temp:=ChangeFileExt(shader, '')+cTexType[i]; for j:=0 to vMD5TextureExtensions.Count-1 do begin if FileExists(temp+vMD5TextureExtensions[j]) then begin with libmat.Material.TextureEx.Add do begin Texture.Image.LoadFromFile(temp+vMD5TextureExtensions[j]); Texture.Enabled:=True; end; Break; end; end; end; end; end else// } shader := ''; end; function QuaternionMakeFromImag(ix, iy, iz: Single): TQuaternion; var rr: Single; begin with Result do begin ImagPart.X := ix; ImagPart.Y := iy; ImagPart.Z := iz; rr := 1 - (ix * ix) - (iy * iy) - (iz * iz); if rr < 0 then RealPart := 0 else RealPart := sqrt(rr); end; end; procedure ReadBone(BoneNum: Integer; BoneString: String); var bonename: String; pos: TAffineVector; quat: TQuaternion; mat, rmat: TMatrix4f; ParentBoneID: Integer; bone, parentbone: TgxSkeletonBone; begin FTempString.CommaText := BoneString; bonename := FTempString[0]; ParentBoneID := StrToInt(FTempString[1]); pos.X := StrToFloatDef(FTempString[2]); pos.Y := StrToFloatDef(FTempString[4]); pos.Z := StrToFloatDef(FTempString[3]); quat := QuaternionMakeFromImag(StrToFloatDef(FTempString[5]), StrToFloatDef(FTempString[7]), StrToFloatDef(FTempString[6])); FFramePositions.Add(pos); FFrameQuaternions.Add(quat); if bonename <> '' then begin FBoneNames.Add(bonename); if ParentBoneID = -1 then bone := TgxSkeletonBone.CreateOwned(Owner.Skeleton.RootBones) else begin parentbone := Owner.Skeleton.RootBones.BoneByID(ParentBoneID); bone := TgxSkeletonBone.CreateOwned(parentbone); mat := QuaternionToMatrix(quat); mat.W := PointMake(pos); rmat := QuaternionToMatrix(FFrameQuaternions[ParentBoneID]); rmat.W := PointMake(FFramePositions[ParentBoneID]); InvertMatrix(rmat); mat := MatrixMultiply(mat, rmat); pos := AffineVectorMake(mat.W); quat := QuaternionFromMatrix(mat); end; with bone do begin BoneID := BoneNum; Name := bonename; end; end; FBasePose.Position[BoneNum] := pos; FBasePose.Quaternion[BoneNum] := quat; end; procedure ReadJoints; var temp: String; i: Integer; begin i := 0; repeat temp := ReadLine; if temp <> '}' then begin ReadBone(i, temp); Inc(i); end; until temp = '}'; Owner.Skeleton.CurrentFrame.Assign(FBasePose); Owner.Skeleton.CurrentFrame.FlushLocalMatrixList; Owner.Skeleton.RootBones.PrepareGlobalMatrices; end; procedure ReadMesh; var temp, shader: String; mesh: TgxSkeletonMeshObject; fg: TgxFGVertexIndexList; vnum, wnum, numverts, numweights: Integer; VertexWeightID, VertexWeightCount, VertexBoneRef: TgxIntegerList; VertexWeight: TgxSingleList; VertexWeighted: TgxAffineVectorList; blendedVert, transformedVert: TAffineVector; i, j, k: Integer; mat: TMatrix4f; begin VertexWeightID := TgxIntegerList.Create; VertexWeightCount := TgxIntegerList.Create; VertexBoneRef := TgxIntegerList.Create; VertexWeight := TgxSingleList.Create; VertexWeighted := TgxAffineVectorList.Create; numverts := 0; mesh := TgxSkeletonMeshObject.CreateOwned(Owner.MeshObjects); fg := TgxFGVertexIndexList.CreateOwned(mesh.FaceGroups); mesh.Mode := momFaceGroups; fg.Mode := fgmmTriangles; repeat temp := ReadLine; FTempString.CommaText := temp; if FTempString.Count > 1 then begin temp := LowerCase(FTempString[0]); if temp = 'shader' then begin shader := FTempString[1]; AllocateMaterial(shader); fg.MaterialName := shader; end else if temp = 'numverts' then begin numverts := StrToInt(FTempString[1]); mesh.TexCoords.Count := numverts; VertexWeightID.Count := numverts; VertexWeightCount.Count := numverts; end else if temp = 'vert' then begin if FTempString.Count >= 6 then begin vnum := StrToInt(FTempString[1]); mesh.TexCoords[vnum] := AffineVectorMake(StrToFloatDef(FTempString[2]), 1 - StrToFloatDef(FTempString[3]), 0); VertexWeightID[vnum] := StrToInt(FTempString[4]); VertexWeightCount[vnum] := StrToInt(FTempString[5]); if VertexWeightCount[vnum] > mesh.BonesPerVertex then mesh.BonesPerVertex := VertexWeightCount[vnum]; end; end else if temp = 'numtris' then begin fg.VertexIndices.Capacity := StrToInt(FTempString[1]) * 3; end else if temp = 'tri' then begin if FTempString.Count >= 5 then begin fg.VertexIndices.Add(StrToInt(FTempString[2])); fg.VertexIndices.Add(StrToInt(FTempString[3])); fg.VertexIndices.Add(StrToInt(FTempString[4])); end; end else if temp = 'numweights' then begin numweights := StrToInt(FTempString[1]); VertexBoneRef.Count := numweights; VertexWeight.Count := numweights; VertexWeighted.Count := numweights; end else if temp = 'weight' then begin if FTempString.Count >= 7 then begin wnum := StrToInt(FTempString[1]); VertexBoneRef[wnum] := StrToInt(FTempString[2]); VertexWeight[wnum] := StrToFloatDef(FTempString[3]); VertexWeighted[wnum] := AffineVectorMake(StrToFloatDef(FTempString[4]), StrToFloatDef(FTempString[6]), StrToFloatDef(FTempString[5])); end; end; end; until temp = '}'; mesh.Vertices.Count := numverts; mesh.VerticeBoneWeightCount := numverts; for i := 0 to numverts - 1 do begin blendedVert := NullVector; for j := 0 to mesh.BonesPerVertex - 1 do begin if j < VertexWeightCount[i] then begin k := VertexWeightID[i] + j; mesh.VerticesBonesWeights^[i]^[j].BoneID := VertexBoneRef[k]; mesh.VerticesBonesWeights^[i]^[j].Weight := VertexWeight[k]; mat := Owner.Skeleton.RootBones.BoneByID(VertexBoneRef[k]) .GlobalMatrix; transformedVert := VectorTransform(VertexWeighted[k], mat); AddVector(blendedVert, VectorScale(transformedVert, VertexWeight[k])); end else begin mesh.VerticesBonesWeights^[i]^[j].BoneID := 0; mesh.VerticesBonesWeights^[i]^[j].Weight := 0; end; end; mesh.Vertices[i] := blendedVert; end; mesh.BuildNormals(fg.VertexIndices, momTriangles); VertexWeightID.Free; VertexWeightCount.Free; VertexBoneRef.Free; VertexWeight.Free; VertexWeighted.Free; end; procedure ReadHierarchy; var temp: String; bone: TgxSkeletonBone; begin if not Assigned(FJointFlags) then begin FJointFlags := TgxIntegerList.Create; Assert(Owner.Skeleton.Frames.Count > 0, 'The md5mesh file must be loaded before md5anim files!'); FJointFlags.Count := Owner.Skeleton.Frames[0].Position.Count; end; repeat temp := ReadLine; FTempString.CommaText := temp; if FTempString.Count >= 3 then begin bone := Owner.Skeleton.BoneByName(FTempString[0]); if Assigned(bone) then FJointFlags[bone.BoneID] := StrToInt(FTempString[2]); end; until temp = '}'; end; procedure ReadBaseFrame; var temp: String; pos: TAffineVector; quat: TQuaternion; begin FFramePositions.Clear; FFrameQuaternions.Clear; repeat temp := ReadLine; FTempString.CommaText := temp; if FTempString.Count >= 6 then begin pos := AffineVectorMake(StrToFloatDef(FTempString[0]), StrToFloatDef(FTempString[1]), StrToFloatDef(FTempString[2])); quat := QuaternionMakeFromImag(StrToFloatDef(FTempString[3]), StrToFloatDef(FTempString[4]), StrToFloatDef(FTempString[5])); FFramePositions.Add(pos); FFrameQuaternions.Add(quat); end; until temp = '}'; end; procedure ReadFrame(framenum: Integer); var temp: String; i, j: Integer; frame: TgxSkeletonFrame; pos: TAffineVector; quat: TQuaternion; begin frame := Owner.Skeleton.Frames[FFirstFrame + framenum]; frame.TransformMode := sftQuaternion; frame.Position.Count := FNumJoints; frame.Quaternion.Count := FNumJoints; for i := 0 to FJointFlags.Count - 1 do begin pos := FFramePositions[i]; quat := FFrameQuaternions[i]; if FJointFlags[i] > 0 then begin temp := ReadLine; FTempString.CommaText := temp; j := 0; if FJointFlags[i] and 1 > 0 then begin pos.X := StrToFloatDef(FTempString[j]); Inc(j); end; if FJointFlags[i] and 2 > 0 then begin pos.Y := StrToFloatDef(FTempString[j]); Inc(j); end; if FJointFlags[i] and 4 > 0 then begin pos.Z := StrToFloatDef(FTempString[j]); Inc(j); end; if FJointFlags[i] and 8 > 0 then begin quat.ImagPart.X := StrToFloatDef(FTempString[j]); Inc(j); end; if FJointFlags[i] and 16 > 0 then begin quat.ImagPart.Y := StrToFloatDef(FTempString[j]); Inc(j); end; if FJointFlags[i] and 32 > 0 then quat.ImagPart.Z := StrToFloatDef(FTempString[j]); end; pos := AffineVectorMake(pos.X, pos.Z, pos.Y); quat := QuaternionMakeFromImag(quat.ImagPart.X, quat.ImagPart.Z, quat.ImagPart.Y); frame.Position[i] := pos; frame.Quaternion[i] := quat; end; end; procedure InitializeMeshes; var i: Integer; begin for i := 0 to Owner.MeshObjects.Count - 1 do TgxSkeletonMeshObject(Owner.MeshObjects[i]) .PrepareBoneMatrixInvertedMeshes; end; var str, temp: String; nummeshes, md5Version, meshid, i: Integer; begin FCurrentPos := 0; FMD5String := TStringList.Create; FTempString := TStringList.Create; FBoneNames := TStringList.Create; meshid := 0; nummeshes := 0; md5Version := 0; try FMD5String.LoadFromStream(aStream); // Version checking str := ReadLine; FTempString.CommaText := str; if FTempString.Count >= 2 then if LowerCase(FTempString[0]) = 'md5version' then md5Version := StrToInt(FTempString[1]); Assert(md5Version = 10, 'Invalid or missing md5Version number.'); repeat str := ReadLine; FTempString.CommaText := str; if FTempString.Count > 1 then begin temp := LowerCase(FTempString[0]); if (temp = 'numjoints') then begin FNumJoints := StrToInt(FTempString[1]); FFramePositions := TgxAffineVectorList.Create; FFrameQuaternions := TgxQuaternionList.Create; if Owner.Skeleton.Frames.Count = 0 then begin FBasePose := TgxSkeletonFrame.CreateOwned(Owner.Skeleton.Frames); FBasePose.Position.Count := FNumJoints; FBasePose.TransformMode := sftQuaternion; FBasePose.Quaternion.Count := FNumJoints; end else FBasePose := Owner.Skeleton.Frames[0]; end else if (temp = 'joints') then begin ReadJoints; if Owner is TgxActor then TgxActor(Owner).Reference := aarSkeleton; end else if (temp = 'nummeshes') then begin nummeshes := StrToInt(FTempString[1]); end else if (temp = 'mesh') then begin if meshid < nummeshes then begin ReadMesh; if meshid = nummeshes - 1 then InitializeMeshes; Inc(meshid); end else begin repeat str := ReadLine; until str = '}'; end; end else if (temp = 'hierarchy') then begin ReadHierarchy; end else if (temp = 'numframes') then begin FNumFrames := StrToInt(FTempString[1]); if FNumFrames > 0 then begin FFirstFrame := Owner.Skeleton.Frames.Count; for i := 1 to FNumFrames do TgxSkeletonFrame.CreateOwned(Owner.Skeleton.Frames); if Owner is TgxActor then begin with TgxActor(Owner).Animations.Add do begin Name := ChangeFileExt(ExtractFileName(ResourceName), ''); Reference := aarSkeleton; StartFrame := FFirstFrame; EndFrame := FFirstFrame + FNumFrames - 1; end; end; end; end else if (temp = 'framerate') then begin FFrameRate := StrToInt(FTempString[1]); end else if (temp = 'baseframe') then begin ReadBaseFrame; end else if (temp = 'frame') then begin ReadFrame(StrToInt(FTempString[1])); end; end; until str = ''; finally if Assigned(FFramePositions) then FreeAndNil(FFramePositions); if Assigned(FFrameQuaternions) then FreeAndNil(FFrameQuaternions); if Assigned(FJointFlags) then FreeAndNil(FJointFlags); FBoneNames.Free; FTempString.Free; FMD5String.Free; end; end; // ------------------------------------------------------------------ // ------------------------------------------------------------------ // ------------------------------------------------------------------ initialization // ------------------------------------------------------------------ // ------------------------------------------------------------------ // ------------------------------------------------------------------ RegisterVectorFileFormat('md5mesh', 'Doom3 mesh files', TgxMD5VectorFile); RegisterVectorFileFormat('md5anim', 'Doom3 animation files', TgxMD5VectorFile); vMD5TextureExtensions := TStringList.Create; with vMD5TextureExtensions do begin Add('.bmp'); Add('.dds'); Add('.jpg'); Add('.tga'); end; // ------------------------------------------------------------------ // ------------------------------------------------------------------ // ------------------------------------------------------------------ finalization // ------------------------------------------------------------------ // ------------------------------------------------------------------ // ------------------------------------------------------------------ vMD5TextureExtensions.Free; end.