diff --git a/src/MeshIO.jl b/src/MeshIO.jl index 0f50e29..01d0c24 100644 --- a/src/MeshIO.jl +++ b/src/MeshIO.jl @@ -9,8 +9,6 @@ using FileIO: FileIO, @format_str, Stream, File, stream, skipmagic import Base.show -include("util.jl") - include("io/off.jl") include("io/ply.jl") include("io/stl.jl") diff --git a/src/io/gts.jl b/src/io/gts.jl index 9c3c33b..88c421e 100644 --- a/src/io/gts.jl +++ b/src/io/gts.jl @@ -12,15 +12,13 @@ function parseGtsLine( s::AbstractString, C, T=eltype(C) ) end end -function load( st::Stream{format"GTS"}, MeshType=GLNormalMesh ) +function load( st::Stream{format"GTS"}; facetype=GLTriangleFace, pointtype=Point) io = stream(st) head = readline( io ) - FT = facetype(MeshType) - VT = vertextype(MeshType) nVertices, nEdges, nFacets = parseGtsLine( head, Tuple{Int,Int,Int} ) iV = iE = iF = 1 - vertices = Vector{VT}(undef, nVertices) + vertices = Vector{pointtype}(undef, nVertices) edges = Vector{Vector{Int}}(undef, nEdges) facets = Vector{Vector{Int}}(undef, nFacets) for full_line::String in eachline(io) @@ -30,7 +28,7 @@ function load( st::Stream{format"GTS"}, MeshType=GLNormalMesh ) if !startswith(line, "#") && !isempty(line) && !all(iscntrl, line) #ignore comments if iV <= nVertices - vertices[iV] = parseGtsLine( line, VT ) + vertices[iV] = parseGtsLine( line, pointtype ) iV += 1 elseif iV > nVertices && iE <= nEdges edges[iE] = parseGtsLine( line, Array{Int} ) @@ -41,8 +39,8 @@ function load( st::Stream{format"GTS"}, MeshType=GLNormalMesh ) end # if end # if end # for - faces = [ FT( union( edges[facets[i][1]], edges[facets[i][2]], edges[facets[i][3]] ) ) for i in 1:length(facets) ] # orientation not guaranteed - return MeshType( vertices, faces ) + faces = [ facetype( union( edges[facets[i][1]], edges[facets[i][2]], edges[facets[i][3]] ) ) for i in 1:length(facets) ] # orientation not guaranteed + return Mesh( vertices, faces ) end function save( st::Stream{format"GTS"}, mesh::AbstractMesh ) diff --git a/src/io/ifs.jl b/src/io/ifs.jl index 2a6c67d..fedc173 100644 --- a/src/io/ifs.jl +++ b/src/io/ifs.jl @@ -1,4 +1,4 @@ -function load(fs::Stream{format"IFS"}, MeshType = GLNormalMesh) +function load(fs::Stream{format"IFS"}; facetype=GLTriangleFace, pointtype=Point3f) io = stream(fs) function str() n = read(io, UInt32) @@ -11,15 +11,15 @@ function load(fs::Stream{format"IFS"}, MeshType = GLNormalMesh) end nverts = read(io, UInt32) verts_float = read(io, Float32, nverts * 3) - verts = reinterpret(Point3f0, verts_float) + verts = reinterpret(pointtype, verts_float) tris = str() if tris != "TRIANGLES\0" error("$(filename(fs)) does not seem to be of format IFS") end nfaces = read(io, UInt32) faces_int = read(io, UInt32, nfaces * 3) - faces = reinterpret(GLTriangle, faces_int) - MeshType(vertices = verts, faces = faces) + faces = reinterpret(facetype, faces_int) + return GeometryBasics.Mesh(verts, faces) end function save(fs::Stream{format"IFS"}, msh::AbstractMesh; meshname = "mesh") @@ -29,8 +29,8 @@ function save(fs::Stream{format"IFS"}, msh::AbstractMesh; meshname = "mesh") write(io, UInt32(length(s0))) write(io, s0) end - vts = decompose(Point3f0, msh) - fcs = decompose(GLTriangle, msh) + vts = decompose(Point3f, msh) + fcs = decompose(GLTriangleFace, msh) # write the header write0str("IFS") diff --git a/src/io/obj.jl b/src/io/obj.jl index 5f48873..66bf130 100644 --- a/src/io/obj.jl +++ b/src/io/obj.jl @@ -5,12 +5,11 @@ ############################## function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, - pointtype=Point3f, normaltype=Vec3f, uvtype=Any) + pointtype=Point3f, normaltype=Vec3f, uvtype=Any) points, v_normals, uv, faces = pointtype[], normaltype[], uvtype[], facetype[] f_uv_n_faces = (faces, facetype[], facetype[]) - last_command = "" - attrib_type = nothing + for full_line in eachline(stream(io)) # read a line, remove newline and leading/trailing whitespaces line = strip(chomp(full_line)) @@ -58,40 +57,19 @@ function load(io::Stream{format"OBJ"}; facetype=GLTriangleFace, end end - point_attributes = Dict{Symbol, Any}() - non_empty_faces = filtertuple(!isempty, f_uv_n_faces) - - # Do we have faces with different indices for positions and normals - # (and texture coordinates) per vertex? - if length(non_empty_faces) > 1 - - # map vertices with distinct indices for possition and normal (and uv) - # to new indices, updating faces along the way - faces, attrib_maps = merge_vertex_attribute_indices(non_empty_faces) - - # Update order of vertex attributes - points = points[attrib_maps[1]] - counter = 2 - if !isempty(uv) - point_attributes[:uv] = uv[attrib_maps[counter]] - counter += 1 - end - if !isempty(v_normals) - point_attributes[:normals] = v_normals[attrib_maps[counter]] - end - - else # we have vertex indexing - no need to remap - - if !isempty(v_normals) - point_attributes[:normals] = v_normals - end - if !isempty(uv) - point_attributes[:uv] = uv - end - + if !isempty(f_uv_n_faces[2]) && (f_uv_n_faces[2] != faces) + uv = FaceView(uv, f_uv_n_faces[2]) + end + + if !isempty(f_uv_n_faces[3]) && (f_uv_n_faces[3] != faces) + v_normals = FaceView(v_normals, f_uv_n_faces[3]) end - return Mesh(meta(points; point_attributes...), faces) + return GeometryBasics.mesh( + points, faces, facetype = facetype; + uv = isempty(uv) ? nothing : uv, + normal = isempty(v_normals) ? nothing : v_normals + ) end # of form "faces v1 v2 v3 ...."" @@ -111,6 +89,11 @@ function _typemax(::Type{OffsetInteger{O, T}}) where {O, T} end function save(f::Stream{format"OBJ"}, mesh::AbstractMesh) + # TODO: allow saving with faceviews (i.e. build the / or // syntax) + if any(v -> v isa FaceView, values(vertex_attributes(mesh))) + mesh = GeometryBasics.clear_faceviews(mesh) + end + io = stream(f) for p in decompose(Point3f, mesh) println(io, "v ", p[1], " ", p[2], " ", p[3]) @@ -122,7 +105,7 @@ function save(f::Stream{format"OBJ"}, mesh::AbstractMesh) end end - if hasproperty(mesh, :normals) + if hasproperty(mesh, :normal) for n in decompose_normals(mesh) println(io, "vn ", n[1], " ", n[2], " ", n[3]) end diff --git a/src/io/ply.jl b/src/io/ply.jl index f4a537e..d44f116 100644 --- a/src/io/ply.jl +++ b/src/io/ply.jl @@ -121,7 +121,7 @@ function load(fs::Stream{format"PLY_ASCII"}; facetype=GLTriangleFace, pointtype= end end if has_normals - return Mesh(meta(points; normals=point_normals), faces) + return Mesh(points, faces; normal = point_normals) else return Mesh(points, faces) end @@ -194,7 +194,7 @@ function load(fs::Stream{format"PLY_BINARY"}; facetype=GLTriangleFace, pointtype end if has_normals - return Mesh(meta(points; normals=point_normals), faces) + return Mesh(points, faces; normal = point_normals) else return Mesh(points, faces) end diff --git a/src/io/stl.jl b/src/io/stl.jl index 6dc838e..d40ee98 100644 --- a/src/io/stl.jl +++ b/src/io/stl.jl @@ -84,7 +84,7 @@ function load(fs::Stream{format"STL_BINARY"}; facetype=GLTriangleFace, i += 1 end - return Mesh(meta(vertices; normals=normals), faces) + return Mesh(vertices, faces; normal = normals) end @@ -127,5 +127,5 @@ function load(fs::Stream{format"STL_ASCII"}; facetype=GLTriangleFace, push!(faces, TriangleFace{Int}(vert_idx...)) end end - return Mesh(meta(points; normals=normals), faces) + return Mesh(points, faces; normal = normals) end diff --git a/src/util.jl b/src/util.jl deleted file mode 100644 index 1c24a8f..0000000 --- a/src/util.jl +++ /dev/null @@ -1,56 +0,0 @@ -# Graphics backends like OpenGL only have one index buffer so the indices to -# positions, normals and texture coordinates cannot be different. E.g. a face -# cannot use positional indices (1, 2, 3) and normal indices (1, 1, 2). In that -# case we need to remap normals such that new_normals[1, 2, 3] = normals[[1, 1, 2]] - - -# ... -_typemin(x) = typemin(x) -_typemin(::Type{OffsetInteger{N, T}}) where {N, T} = typemin(T) - N - -merge_vertex_attribute_indices(faces...) = merge_vertex_attribute_indices(faces) - -function merge_vertex_attribute_indices(faces::Tuple) - FaceType = eltype(faces[1]) - IndexType = eltype(FaceType) - D = length(faces) - N = length(faces[1]) - - # (pos_idx, normal_idx, uv_idx, ...) -> new_idx - vertex_index_map = Dict{NTuple{D, UInt32}, IndexType}() - # faces after remapping (0 based assumed) - new_faces = sizehint!(FaceType[], N) - temp = IndexType[] # keeping track of vertex indices of a face - counter = _typemin(IndexType) - # for remaping attribs, i.e. `new_attrib = old_attrib[index2vertex[attrib_index]]` - index2vertex = ntuple(_ -> sizehint!(UInt32[], N), D) - - for i in eachindex(faces[1]) - # (pos_faces[i], normal_faces[i], uv_faces[i], ...) - attrib_faces = getindex.(faces, i) - empty!(temp) - - for j in eachindex(attrib_faces[1]) - # (pos_index, normal_idx, uv_idx, ...) - # = (pos_faces[i][j], normal_faces[i][j], uv_faces[i][j], ...) - vertex = GeometryBasics.value.(getindex.(attrib_faces, j)) # 1 based - - # if combination of indices in vertex is new, make a new index - if !haskey(vertex_index_map, vertex) - vertex_index_map[vertex] = counter - counter = IndexType(counter + 1) - push!.(index2vertex, vertex) - end - - # keep track of the (new) index for this vertex - push!(temp, vertex_index_map[vertex]) - end - - # store face with new indices - push!(new_faces, FaceType(temp...)) - end - - sizehint!(new_faces, length(new_faces)) - - return new_faces, index2vertex -end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 70ae35b..51db14c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,8 +21,10 @@ end Rect3f(Vec3f(baselen), Vec3f(baselen, dirlen, baselen)), Rect3f(Vec3f(baselen), Vec3f(baselen, baselen, dirlen)) ] - uvn_mesh = merge(map(uv_normal_mesh, mesh)) - mesh = merge(map(triangle_mesh, mesh)) + uvn_mesh = GeometryBasics.clear_faceviews(merge(map(uv_normal_mesh, mesh))) + mesh = GeometryBasics.clear_faceviews(merge(map(triangle_mesh, mesh))) + empty!(uvn_mesh.views) + empty!(mesh.views) mktempdir() do tmpdir @@ -54,6 +56,8 @@ end @test mesh_loaded == uvn_mesh end end + + @testset "Real world files" begin @testset "STL" begin @@ -64,23 +68,26 @@ end @test test_face_indices(msh) msh = load(joinpath(tf, "binary.stl")) - @test msh isa GLNormalMesh + @test msh isa Mesh{D, Float32, GLTriangleFace} where D + @test all(v -> v isa AbstractVector, values(vertex_attributes(msh))) @test length(faces(msh)) == 828 @test length(coordinates(msh)) == 2484 - @test length(msh.normals) == 2484 + @test length(normals(msh)) == 2484 @test test_face_indices(msh) mktempdir() do tmpdir save(File{format"STL_BINARY"}(joinpath(tmpdir, "test.stl")), msh) msh1 = load(joinpath(tmpdir, "test.stl")) - @test msh1 isa GLNormalMesh + @test msh1 isa Mesh{D, Float32, GLTriangleFace} where D + @test all(v -> v isa AbstractVector, values(vertex_attributes(msh1))) @test faces(msh) == faces(msh1) @test coordinates(msh) == coordinates(msh1) - @test msh.normals == msh1.normals + @test normals(msh) == normals(msh1) end msh = load(joinpath(tf, "binary_stl_from_solidworks.STL")) - @test msh isa GLNormalMesh + @test msh isa Mesh{D, Float32, GLTriangleFace} where D + @test all(v -> v isa AbstractVector, values(vertex_attributes(msh))) @test length(faces(msh)) == 12 @test length(coordinates(msh)) == 36 @test test_face_indices(msh) @@ -133,8 +140,9 @@ end @testset "OBJ" begin msh = load(joinpath(tf, "test.obj")) @test length(faces(msh)) == 3954 - @test length(coordinates(msh)) == 2519 - @test length(normals(msh)) == 2519 + @test length(coordinates(msh)) == 2248 + @test length(normals(msh)) == 2240 + @test length(texturecoordinates(msh)) == 2220 @test test_face_indices(msh) msh = load(joinpath(tf, "cube.obj")) # quads @@ -172,29 +180,9 @@ end end @testset "GTS" begin # TODO: FileIO upstream - #msh = load(joinpath(tf, "sphere5.gts")) - #@test typeof(msh) == GLNormalMesh - #test_face_indices(msh) - end - - @testset "Index remapping" begin - pos_faces = GLTriangleFace[(5, 6, 7), (5, 6, 8), (5, 7, 8)] - normal_faces = GLTriangleFace[(5, 6, 7), (3, 6, 8), (5, 7, 8)] - uv_faces = GLTriangleFace[(1, 2, 3), (4, 2, 5), (1, 3, 1)] - - # unique combinations -> new indices - # 551 662 773 534 885 881 1 2 3 4 5 6 (or 0..5 with 0 based indices) - faces, maps = MeshIO.merge_vertex_attribute_indices(pos_faces, normal_faces, uv_faces) - - @test length(faces) == 3 - @test faces == GLTriangleFace[(1, 2, 3), (4, 2, 5), (1, 3, 6)] - - # maps are structured as map[new_index] = old_index, so they grab the - # first/second/third index of the unique combinations above - # maps = (pos_map, normal_map, uv_map) - @test maps[1] == [5, 6, 7, 5, 8, 8] - @test maps[2] == [5, 6, 7, 3, 8, 8] - @test maps[3] == [1, 2, 3, 4, 5, 1] + # msh = load(joinpath(tf, "sphere5.gts")) + # @test typeof(msh) == GLNormalMesh + # test_face_indices(msh) end end end