LCOV - code coverage report
Current view: top level - src/obj - objbuffer.jl (source / functions) Hit Total Coverage
Test: on branch nothing Lines: 122 148 82.4 %
Date: 2025-07-10 13:29:30 Functions: 0 0 -

          Line data    Source code
       1             : """
       2             : Store contents of Wavefront OBJ file, use for reading files.
       3             : 
       4             : - `T` is the scalar type, `I` is the index type,
       5             :    `ColorT` is used for RGBA colors
       6             : - `nv`: numberof vertices
       7             : - `positiondim` number of coordinates (2,3,3)
       8             : - `texcoorddim` number of texture coordinates (1,2,3)
       9             : - `position` coordinates of vertex positions
      10             : - `texcoord` texture coordinates
      11             : - `normal` coordinates of normals (always 3 coordinates per normal)
      12             : - `fdegree` vector of face degrees
      13             : - `vidx` indices of vertices (positions) spanning faces
      14             : - `tidx` indices of texture coordinates, may be discontinuous (i.e.,
      15             :   possibly >1 per vertex)
      16             : - `nidx` indices of normal vectors, may be discontinuous (i.e.,
      17             :   possibly >1 per vertex)
      18             : - `vcolor` per vertex color as `ColorT`
      19             : 
      20             : !!! note
      21             :     In contrast to [MeshIO](https://github.com/JuliaIO/MeshIO.jl),
      22             :     polygons are **not triangulated**, and there is support for
      23             :     "discontinuous" normals and texture coordinates that are defined
      24             :     per face vertex.
      25             : 
      26             : See also [Wavefront OBJ](https://paulbourke.net/dataformats/obj/) and
      27             : [vertex color](https://paulbourke.net/dataformats/obj/colour.html)
      28             : 
      29             : See also [`writeobjmesh`](@ref)
      30             : """
      31             : struct OBJBuffer{T, I, ColorT}
      32             :     nv::Int
      33             :     positiondim::Int
      34             :     texcoorddim::Int
      35             :     position::Vector{T}
      36             :     texcoord::Vector{T}
      37             :     normal::Vector{T}
      38             :     fdegree::Vector{I}
      39             :     vidx::Vector{I}
      40             :     tidx::Vector{I}
      41             :     nidx::Vector{I}
      42             :     vcolor::Vector{ColorT}
      43             : 
      44           3 :     function OBJBuffer{T, I, C}() where {T, I, C}
      45           3 :         new(0, 0, 0,
      46             :             T[], T[], T[],
      47             :             I[],
      48             :             I[], I[], I[],
      49             :             C[])
      50             :     end
      51             : 
      52           3 :     function OBJBuffer{T, I, C}(obj::OBJBuffer{T, I, C},
      53             :                              nv::Int,
      54             :                                 positiondim::Int,
      55             :                                 texcoorddim::Int) where {T, I, C}
      56             : 
      57           3 :         @assert obj.nv == 0 && obj.positiondim == 0 && obj.texcoorddim == 0
      58             : 
      59           3 :         new(nv, positiondim, texcoorddim,
      60             :             obj.position, obj.texcoord, obj.normal,
      61             :             obj.fdegree,
      62             :             obj.vidx, obj.tidx, obj.nidx, obj.vcolor)
      63             :     end
      64             : end
      65             : 
      66             : """
      67             : Construct [`OBJBuffer`](@ref)
      68             : """
      69           6 : OBJBuffer(;
      70             :           real_type::Type{T}=Float64,
      71             :           index_type::Type{I}=Int32,
      72             :           color_type::Type{C}=RGBA) where {T<:Real, I<:Integer, C} =
      73             :         OBJBuffer{real_type, index_type, color_type}()
      74             : 
      75             : """
      76             : Update number of vertices and dimensions of [`OBJBuffer`](@ref) after reading.
      77             : 
      78             : !!! note
      79             :     Constructs a new `OBJBuffer` and "steals" data from `obj`!
      80             : """
      81           3 : update!(obj::OBJBuffer{T, I, C},
      82             :         nv::Int, positiondim::Int, texcoorddim::Int) where {T, I, C} =
      83             :             OBJBuffer{T, I, C}(obj, nv, positiondim, texcoorddim)
      84             : 
      85             : """
      86             : Get number of vertices in [`OBJBuffer`](@ref)
      87             : """
      88           6 : nv(obj::OBJBuffer) = obj.nv
      89             : 
      90             : """
      91             : Get number of faces in [`OBJBuffer`](@ref)
      92             : """
      93           3 : nf(obj::OBJBuffer) = length(obj.fdegree)
      94             : 
      95             : """
      96             : Get number of texture coordinates in [`OBJBuffer`](@ref)
      97             : """
      98           8 : nt(obj::OBJBuffer) = length(obj.texcoord) ÷ obj.texcoorddim
      99             : 
     100             : """
     101             : Get number of normal vectors in [`OBJBuffer`](@ref)
     102             : """
     103          11 : nn(obj::OBJBuffer) = length(obj.normal) ÷ 3
     104             : 
     105             : """
     106             : Does [`OBJBuffer`](@ref) represent a triangle mesh?
     107             : """
     108           0 : istrianglemesh(obj::OBJBuffer) = all(==(3), obj.fdegree)
     109             : 
     110             : """
     111             : Does [`OBJBuffer`](@ref) store texture coordinates?
     112             : """
     113           3 : hastexcoord(obj::OBJBuffer) = !isempty(obj.texcoord)
     114             : 
     115             : """
     116             : Does [`OBJBuffer`](@ref) store vormal vectors coordinates?
     117             : """
     118           3 : hasnormal(obj::OBJBuffer) = !isempty(obj.normal)
     119             : 
     120             : """
     121             : Does [`OBJBuffer`](@ref) store vertex colors?
     122             : """
     123           6 : hasvertexcolor(obj::OBJBuffer) = !isempty(obj.vcolor)
     124             : 
     125             : """
     126             : Get dimension of positions in [`OBJBuffer`](@ref) (`2, 3, 4`)
     127             : """
     128           3 : positiondim(obj::OBJBuffer) = obj.positiondim
     129             : 
     130             : """
     131             : Get dimension of texture coordinates in [`OBJBuffer`](@ref) (`2, 3`)
     132             : """
     133           3 : texcoorddim(obj::OBJBuffer) = obj.texcoorddim
     134             : 
     135             : """
     136             : Are texture coordinates given per vertex? Alternative is per face.
     137             : """
     138           0 : istexcoordpervertex(obj::OBJBuffer) = hastexcoord(obj) && isempty(obj.tidx)
     139             : 
     140             : """
     141             : Are normals given per vertex? Alternative is per face.
     142             : """
     143           0 : isnormalpervertex(obj::OBJBuffer) = hasnormal(obj) && isempty(obj.nidx)
     144             : 
     145           3 : position(obj::OBJBuffer) = reshape(obj.position, obj.positiondim, nv(obj))
     146             : 
     147           3 : position(obj::OBJBuffer, m::Val{M}) where {M} = scmatrix(m, position(obj))[:]
     148             : 
     149             : """
     150             :     X = position(obj)
     151             :     xs = position(obj, Val(N))
     152             : 
     153             : Get vertex positions stored in [`OBJBuffer`](@ref) as `X::Matrix{T}`
     154             : with positions in columns or `xs::Vector{SVector{T, N}}` (where `N`
     155             : must match [`positiondim`](@ref).
     156             : 
     157             : !!! note
     158             :     The returned matrix/vectors **shares** data with `obj`!
     159             : """ position
     160             : 
     161           1 : texcoord(obj::OBJBuffer) = reshape(obj.texcoord, obj.texcoorddim, nt(obj))
     162             : 
     163           1 : texcoord(obj::OBJBuffer, m::Val{M}) where {M} = scmatrix(m, texcoord(obj))[:]
     164             : 
     165             : """
     166             :     U = texcoord(obj)
     167             :     us = texcoord(obj, Val(N))
     168             : 
     169             : Get texcoords stored in [`OBJBuffer`](@ref) as `U::Matrix{T}` with
     170             : texture coordinates in columns or `us::Vector{SVector{T, N}}` (where
     171             : `N` must match [`texcoorddim`](@ref).
     172             : 
     173             : !!! note
     174             :     The returned matrix/vectors **shares** data with `obj`!
     175             : """ texcoord
     176             : 
     177           0 : normal(obj::OBJBuffer) = reshape(obj.normal, 3, nn(obj))
     178             : 
     179           0 : normal(obj::OBJBuffer, ::Val{3}) = scmatrix(Val(3), normal(obj))[:]
     180             : 
     181             : """
     182             : Does [`OBJBuffer`](@ref) provide per-vertex normals?
     183             : """
     184           4 : hasvt(obj::OBJBuffer) = (nt(obj) > 0) && isempty(obj.tidx)
     185             : 
     186             : """
     187             : Does [`OBJBuffer`](@ref) provide vertex normals per face?
     188             : 
     189             : !!! note
     190             :     Per-face normals will be stored as half-edge attribute (`hattr`)
     191             :     in `PMesh`
     192             : """
     193           3 : hasht(obj::OBJBuffer) = (nt(obj) > 0) && !isempty(obj.tidx)
     194             : 
     195             : """
     196             : Does [`OBJBuffer`](@ref) provide texture coordinates?
     197             : """
     198           5 : hasvn(obj::OBJBuffer) = (nn(obj) > 0) && isempty(obj.nidx)
     199             : 
     200             : """
     201             : Does [`OBJBuffer`](@ref) provide texture coordinates per face?
     202             : 
     203             : !!! note
     204             :     Per-face texture coordinates will be stored as half-edge attribute
     205             :     (`hattr`) in `PMesh`
     206             : """
     207           3 : hashn(obj::OBJBuffer) = (nn(obj) > 0) && !isempty(obj.nidx)
     208             : 
     209             : """
     210             :     N = normal(obj)
     211             :     ns = normal(obj, Val(3))
     212             : 
     213             : Get normals stored in [`OBJBuffer`](@ref) as `N::Matrix{T}` with
     214             : normals in columns or `ns::Vector{SVector{T, 3}}`.
     215             : 
     216             : !!! note
     217             :     The returned matrix/vectors **shares** data with `obj`!
     218             : """ normal
     219             : 
     220           0 : function Base.show(io::IO, obj::OBJBuffer)
     221           0 :     info = ["nv=$(nv(obj))",
     222             :             "nf=$(nf(obj))",
     223             :             "dim=$(positiondim(obj))",
     224             :             "triangles=$(istrianglemesh(obj))"]
     225             : 
     226           0 :     hasnormal(obj) && push!(info, hasvn(obj) ? "vnormal" : "vnormal/face")
     227           0 :     hasvertexcolor(obj) && push!(info, "vcolor")
     228           0 :     hastexcoord(obj) && push!(info, hasvt(obj) ? "texcoord" : "texcoord/face")
     229           0 :     hastexcoord(obj) && push!(info, "tdim=$(texcoorddim(obj))")
     230             : 
     231           0 :     print(io, "OBJBuffer [$(join(info, ", "))]")
     232             : end
     233             : 
     234             : """
     235             :    obj = readobj(io[; real_type=Float64,
     236             :                     index_type=Int32, color_type=PMeshIO.RBGA)
     237             : 
     238             : Reads OBJ file from `io` and returns [`OBJBuffer`](@ref) `obj`.
     239             : 
     240             : See also [`OBJBuffer`](@ref), [`writeobjmesh`](@ref)
     241             : """
     242           6 : function readobj(io::IO;
     243             :                  real_type::Type{T}=Float64,
     244             :                  index_type::Type{I}=Int32,
     245             :                  color_type::Type{C}=RGBA) where {T, I, C}
     246             : 
     247             :     # There is no mandatory header.
     248             : 
     249           3 :     obj = OBJBuffer(; real_type, index_type, color_type)
     250             : 
     251           3 :     lineno = 0 # current line number for diagnostics
     252           3 :     positiondim = 0
     253           3 :     texcoorddim = 0
     254             : 
     255       11354 :     function position!(line)
     256       11351 :         tokens = split(line)
     257       11351 :         values = parse.(T, tokens)
     258             : 
     259       11351 :         if positiondim == 0
     260           3 :             positiondim = length(values)
     261             : 
     262           4 :             if !(2 <= positiondim <= 4 || positiondim == 6)
     263           0 :                 error("L$(lineno): invalid number of coordinates $(positiondim)")
     264           3 :             elseif positiondim < length(values)
     265           0 :                 error("L$(lineno): inconsistent number of coordinates $(length(values))")
     266             :             else
     267           6 :                 for i in length(values)+1:positiondim
     268           0 :                     push!(values, i == 4 ? T(0) : T(1))
     269           0 :                 end
     270             :             end
     271             :         end
     272             : 
     273       11351 :         @assert length(values) == positiondim
     274             : 
     275       11351 :         if positiondim == 6
     276       18444 :             append!(obj.position, values[1:3])
     277             : 
     278        9222 :             color =
     279             :                 if contains(tokens[end], '.') # NOTE: inefficient, heuristic (Int|Float)
     280           0 :                     all(x -> (0 <= x <= 1), values[4:6]) ||
     281             :                         error("L$(lineno): invalid vertex color '$(join(tokens[4:6], ' '))")
     282           0 :                     color_type(values[4:6]..., T(1))
     283             :                 else
     284       36888 :                     all(x -> (0 <= x <= 255), values[4:6]) ||
     285             :                         error("L$(lineno): invalid vertex color '$(join(tokens[4:6], ' '))")
     286       18444 :                     color_type((values[4:6] / T(255))..., T(1))
     287             :                 end
     288             : 
     289        9222 :             push!(obj.vcolor, color)
     290             :         else
     291        2129 :             append!(obj.position, values)
     292             :         end
     293             :     end
     294             : 
     295        7004 :     function texcoord!(line)
     296        7001 :         values = parse.(T, split(line))
     297             : 
     298        7001 :         if texcoorddim == 0
     299           1 :             texcoorddim = length(values)
     300           1 :             if !(1 <= texcoorddim <= 3)
     301           0 :                 error("L$(lineno): invalid number of texcoords $(texcoorddim)")
     302           1 :             elseif texcoorddim < length(values)
     303           0 :                 error("L$(lineno): inconsistent number of texcoords $(length(values))")
     304             :             else
     305           2 :                 for _ in length(values)+1:texcoorddim
     306           0 :                     push!(values, T(0))
     307           0 :                 end
     308             :             end
     309             :         end
     310             : 
     311        7001 :         @assert length(values) == texcoorddim
     312             : 
     313        7001 :         append!(obj.texcoord, values)
     314             :     end
     315             : 
     316             : 
     317        2177 :     function normal!(line)
     318        2174 :         values = parse.(T, split(line))
     319        2174 :         (length(values) != 3) && error("L$(lineno): invalid normal 'vn $(line)'")
     320             : 
     321        2174 :         nrm = SVector{3, T}(values[1], values[2], values[3])
     322        2174 :         len = norm(nrm)
     323             : 
     324        2174 :         if len == 0
     325           0 :             @warn("zero normal")
     326             :         else
     327        2174 :             nrm /= len
     328             :         end
     329             : 
     330        2174 :         for coord in nrm
     331        6522 :             push!(obj.normal, coord)
     332        6522 :         end
     333             :     end
     334             : 
     335       65916 :     function parse_face_vertex(str)
     336       65913 :         vtn = split(str, '/')
     337       65913 :         v = parse(Int, first(vtn))
     338       65913 :         t = (length(vtn) > 1 && !isempty(vtn[2])) ? parse(Int, vtn[2]) : 0
     339       65913 :         n = (length(vtn) > 2 && !isempty(vtn[3])) ? parse(Int, vtn[3]) : 0
     340             : 
     341       65913 :         (v, t, n)
     342             :     end
     343             : 
     344           3 :     first_face = true
     345             : 
     346       21818 :     function face!(line)
     347       21815 :         parts = split(line)
     348             : 
     349       21815 :         if length(parts) < 3
     350           0 :             error("L$(lineno): invalid face 'f $(line)'")
     351             :         end
     352             : 
     353       21815 :         for part in parts
     354       65913 :             vi, ti, ni = parse_face_vertex(part)
     355             : 
     356       65913 :             push!(obj.vidx, vi)
     357             : 
     358       65913 :             if ti > 0
     359        9577 :                 !isempty(obj.tidx) || first_face ||
     360             :                     error("L$(lineno): inconsistent texture index '$(part)' for 'f $(line)'")
     361             : 
     362        9576 :                 push!(obj.tidx, ti)
     363             :             else
     364       56337 :                 isempty(obj.tidx) ||
     365             :                     error("L$(lineno): inconsistent texture index '$(part)' for 'f $(line)'")
     366             :             end
     367       65913 :             if ni > 0
     368       11546 :                 !isempty(obj.nidx) || first_face ||
     369             :                     error("L$(lineno): inconsistent normal index '$(part)' for 'f $(line)'")
     370             : 
     371       11544 :                 push!(obj.nidx, ni)
     372             :             else
     373       54369 :                 isempty(obj.nidx) ||
     374             :                     error("L$(lineno): inconsistent normal index '$(part)' for 'f $(line)'")
     375             :             end
     376             : 
     377       65913 :             first_face = false
     378       65913 :         end
     379             : 
     380       21815 :         push!(obj.fdegree, length(parts))
     381             :     end
     382             : 
     383           6 :     for line in eachline(io)
     384       42357 :         lineno += 1
     385             : 
     386      127062 :         (isempty(line) || line[1] == '#' || isspace(line[1])) && continue
     387             : 
     388       42348 :         if (startswith(line, "v "))
     389       11351 :             position!(line[3:end])
     390       30997 :         elseif startswith(line, "vt ")
     391        7001 :             texcoord!(line[4:end])
     392       23996 :         elseif startswith(line, "vn ")
     393        2174 :             normal!(line[4:end])
     394       21822 :         elseif startswith(line, "f ")
     395       21815 :             face!(line[3:end])
     396           7 :         elseif match(r"^(mtllib|usemtl|o|s|g|vp|l)\s", line) === nothing
     397           0 :             @warn "L$(lineno): ignore '$(line)'"
     398             : 
     399             :             # NOTE: https://paulbourke.net/dataformats/obj/
     400             :             # NOTE: https://en.wikipedia.org/wiki/Wavefront_.obj_file
     401             : 
     402             :             # TODO: track object names and groups
     403             :             # TODO: track texture/material/... (for range of indices each)
     404             :             # TODO: store line elements (won't store in mesh)
     405             : 
     406             :         end
     407       84711 :     end
     408             : 
     409           3 :     @assert (positiondim != 0) || isempty(obj.position)
     410           5 :     @assert (texcoorddim != 0) || isempty(obj.texcoord)
     411             : 
     412           3 :     if positiondim == 6
     413           1 :         positiondim = 3
     414           1 :         @assert length(obj.vcolor) == length(obj.position) ÷ positiondim
     415             :     end
     416             : 
     417           3 :     if positiondim == 0
     418           0 :         positiondim = 3
     419             :     end
     420             : 
     421           3 :     if texcoorddim == 0
     422           2 :         texcoorddim = 2
     423             :     end
     424             : 
     425           3 :     @assert length(obj.position) % positiondim == 0
     426           3 :     @assert length(obj.texcoord) % texcoorddim == 0
     427           3 :     @assert length(obj.normal) % 3 == 0
     428             : 
     429           3 :     nv = length(obj.position) ÷ positiondim
     430           3 :     nt = length(obj.texcoord) ÷ texcoorddim
     431           3 :     nn = length(obj.normal) ÷ 3
     432             : 
     433             :     # TODO: output offending index
     434       65916 :     all(i -> (1<= i <= nv), obj.vidx) || error("invalid vertex index")
     435        9579 :     all(i -> (1<= i <= nt), obj.tidx) || error("invalid vertex index for texture coordinate")
     436       11547 :     all(i -> (1<= i <= nn), obj.nidx) || error("invalid vertex index for normal")
     437             : 
     438           3 :     n = length(obj.vidx)
     439           4 :     isempty(obj.tidx) || (length(obj.tidx) == n) || error("inconsistent texture coordinates")
     440           5 :     isempty(obj.nidx) || (length(obj.nidx) == n) || error("inconsistent normals")
     441             : 
     442           3 :     update!(obj, nv, positiondim, texcoorddim)
     443             : end

Generated by: LCOV version 1.16