unit ObjFile; interface uses Windows, SysUtils, Messages, Types, Classes, rgl; type ERglObjFileError = class(Exception); function ParseObjData(const AData: TArray<string>): TArray<GLfloat6>; function PrintRawBuffer(ABuffer: Pointer; ALength: Integer; const ASep: string = ''): string; overload; function PrintRawBuffer(ABuffer: TArray<GLfloat6>; const ASep: string = ''): string; overload; implementation uses Math, Generics.Defaults, Generics.Collections, Character; var InvFS: TFormatSettings; function Tokenize(const S: string; ADefs: TDictionary<string, string>): TArray<string>; function TDA(const AToken: string): string; begin if (ADefs = nil) or (ADefs.Count = 0) or not ADefs.TryGetValue(AToken, Result) then Result := AToken; end; begin var ActualLength := 0; SetLength(Result, 32); var InToken := False; var p := 0; for var i := 1 to S.Length do begin if InToken then begin if S[i].IsWhiteSpace then begin InToken := False; Result[ActualLength] := TDA(Copy(S, p, i - p)); Inc(ActualLength); if ActualLength = Length(Result) then Break; end; end else begin if not S[i].IsWhiteSpace then begin InToken := True; p := i; end; end; end; if InToken and (ActualLength < Length(Result)) then begin Result[ActualLength] := TDA(Copy(S, p)); Inc(ActualLength); end; SetLength(Result, ActualLength); end; procedure ParseVec(AList: TList<rglv>; const AData: TArray<string>; ANormalize: Boolean); begin if Length(AData) < 4 then raise ERglObjFileError.Create('Vector is of too small dimension.'); var v := vec( StrToFloat(AData[1], InvFS), StrToFloat(AData[2], InvFS), StrToFloat(AData[3], InvFS) ); if ANormalize and not IsZero(v.Norm) then v := v.Normalized; if Assigned(AList) then AList.Add(v); end; type TVertexData = record VertexIndex: Integer; TextureIndex: Integer; NormalIndex: Integer; FaceData: GLr3n3v; end; function ParseVertex(const AData: string): TVertexData; begin Result := Default(TVertexData); var P := AData.Split(['/']); if Length(P) >= 1 then Result.VertexIndex := StrToInt(P[0]); if Length(P) >= 2 then begin if (Length(P) = 2) or not P[1].IsEmpty then Result.TextureIndex := StrToInt(P[1]); end; if Length(P) >= 3 then Result.NormalIndex := StrToInt(P[2]); end; procedure TranslateVertex(var Vertex: TVertexData; AVertices: TList<rglv>); begin case Sign(Vertex.VertexIndex) of NegativeValue: Vertex.FaceData.r := AVertices[AVertices.Count + Vertex.VertexIndex]; PositiveValue: Vertex.FaceData.r := AVertices[Vertex.VertexIndex - 1]; else raise ERglObjFileError.Create('Invalid vertex index.'); end; end; procedure TranslateNormal(var Vertex: TVertexData; ANormals: TList<rglv>; const ADefNormal: rglv); begin case Sign(Vertex.NormalIndex) of NegativeValue: Vertex.FaceData.n := ANormals[ANormals.Count + Vertex.NormalIndex]; PositiveValue: Vertex.FaceData.n := ANormals[Vertex.NormalIndex - 1]; else Vertex.FaceData.n := ADefNormal; end; end; procedure ParseFace(AVertices, ANormals: TList<rglv>; AFaces: TList<GLfloat6>; AData: TArray<string>); begin if Length(AData) < 4 then raise ERglObjFileError.Create('Vector is of too small dimension.'); if Length(AData) >= 5 then begin ParseFace(AVertices, ANormals, AFaces, Copy(AData, 0, 4)); for var i := 3 to High(AData) - 1 do ParseFace(AVertices, ANormals, AFaces, ['f', AData[1], AData[i], AData[Succ(i)]]); Exit; end; var A := ParseVertex(AData[1]); var B := ParseVertex(AData[2]); var C := ParseVertex(AData[3]); TranslateVertex(A, AVertices); TranslateVertex(B, AVertices); TranslateVertex(C, AVertices); var LComputedNormal := rglv.Zero; if (A.NormalIndex = 0) or (B.NormalIndex = 0) or (C.NormalIndex = 0) then begin var u := B.FaceData.r - A.FaceData.r; var v := C.FaceData.r - B.FaceData.r; LComputedNormal := u xor v; if not IsZero(LComputedNormal.Norm) then LComputedNormal := LComputedNormal.Normalized; end; TranslateNormal(A, ANormals, LComputedNormal); TranslateNormal(B, ANormals, LComputedNormal); TranslateNormal(C, ANormals, LComputedNormal); if Assigned(AFaces) then begin AFaces.Add(GLfloat6(A.FaceData)); AFaces.Add(GLfloat6(B.FaceData)); AFaces.Add(GLfloat6(C.FaceData)); end; end; function ParseObjData(const AData: TArray<string>): TArray<GLfloat6>; begin var Defines := TDictionary<string, string>.Create; try var Vertices := TList<rglv>.Create; try var Normals := TList<rglv>.Create; try var Faces := TList<GLfloat6>.Create; try var i := 0; try while i <= High(AData) do begin var L := AData[i].Trim; Inc(i); if L.IsEmpty or L.StartsWith('#') then Continue; var P := Tokenize(L, Defines); if Length(P) = 0 then Continue; if P[0] = 'v' then ParseVec(Vertices, P, False) else if P[0] = 'vn' then ParseVec(Normals, P, True) else if P[0] = 'f' then ParseFace(Vertices, Normals, Faces, P) else if (P[0] = 'def') and (Length(P) >= 2) then Defines.Add(P[1], string.Join(#32, Copy(P, 2))); end; except on E: Exception do raise ERglObjFileError.CreateFmt('Couldn''t parse OBJ file at line %d.'#32 + E.Message, [i]); end; Result := Faces.ToArray; finally Faces.Free; end; finally Normals.Free; end; finally Vertices.Free; end; finally Defines.Free; end; end; {$POINTERMATH ON} type PFloat = ^Single; {$POINTERMATH OFF} function FormatFloat(p: Single): string; begin Result := SysUtils.FormatFloat('0.########', p, InvFS); end; function FormatFloat6(p: PFloat; const ASep: string): string; begin var LSep := ASep; if LSep = '' then LSep := #32; Result := FormatFloat(p^); Inc(p); Result := Result + LSep + FormatFloat(p^); Inc(p); Result := Result + LSep + FormatFloat(p^); Inc(p); Result := Result + LSep + FormatFloat(p^); Inc(p); Result := Result + LSep + FormatFloat(p^); Inc(p); Result := Result + LSep + FormatFloat(p^) + LSep.TrimRight; end; function PrintRawBuffer(ABuffer: Pointer; ALength: Integer; const ASep: string): string; begin const FloatCount = ALength div SizeOf(Single); const VertexCount = FloatCount div 6; var L: TArray<string> := nil; SetLength(L, VertexCount); var p: PFloat := PFloat(ABuffer); for var i := 0 to High(L) do begin L[i] := FormatFloat6(p, ASep); Inc(p, 6); end; Result := string.Join(#13#10, L); end; function PrintRawBuffer(ABuffer: TArray<GLfloat6>; const ASep: string): string; begin Result := PrintRawBuffer(Pointer(ABuffer), Length(ABuffer) * SizeOf(GLfloat6), ASep); end; initialization InvFS := TFormatSettings.Invariant; end.