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
|