Skip to content

Commit 70e601b

Browse files
author
Kurt Yoder
authored
Merge pull request #74 from ComFreek/master
Add option to retain face data
2 parents 3d7cbd6 + 191dcda commit 70e601b

File tree

7 files changed

+174
-59
lines changed

7 files changed

+174
-59
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ A more complex example
3636
* `strict` (Default: `False`) will raise an exception if unsupported features are found in the obj or mtl file
3737
* `encoding` (Default: `utf-8`) of the obj and mtl file(s)
3838
* `create_materials` (Default: `False`) will create materials if mtl file is missing or obj file references non-existing materials
39+
* `collect_faces` (Default: `False`) will collect triangle face data for every mesh. In case faces with more than three vertices are specified they will be triangulated. See the documentation of `ObjParser#consume_faces()` in [`obj.py`](https://github.com/greenmoss/PyWavefront/blob/master/pywavefront/obj.py).
3940
* `parse` (Default: `True`) decides if parsing should start immediately.
4041
* `cache` (Default: `False`) writes the parsed geometry to a binary file for faster loading in the future
4142

pywavefront/material.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141

4242

4343
class Material(object):
44-
def __init__(self, name, is_default=False):
44+
def __init__(self, name, is_default=False, has_faces=False):
4545
"""
4646
Create a new material
4747
:param name: Name of the material
@@ -55,7 +55,7 @@ def __init__(self, name, is_default=False):
5555
self.transparency = 1.0
5656
self.shininess = 0.
5757
self.optical_density = 1.0
58-
# Multiple illumination models are available, per material. These are enumerated as follows:
58+
# Multiple illumination models are available, per material. These are enumerated as follows:
5959
# 0. Color on and Ambient off
6060
# 1. Color on and Ambient on
6161
# 2. Highlight on
@@ -155,7 +155,7 @@ def unset_texture(self):
155155
class MaterialParser(Parser):
156156
"""Object to parse lines of a materials definition file."""
157157

158-
def __init__(self, file_name, strict=False, encoding="utf-8", parse=True):
158+
def __init__(self, file_name, strict=False, encoding="utf-8", parse=True, collect_faces=False):
159159
"""
160160
Create a new material parser
161161
:param file_name: file name and path of obj file to read
@@ -167,13 +167,14 @@ def __init__(self, file_name, strict=False, encoding="utf-8", parse=True):
167167

168168
self.materials = {}
169169
self.this_material = None
170+
self.collect_faces = collect_faces
170171

171172
if parse:
172173
self.parse()
173174

174175
@auto_consume
175176
def parse_newmtl(self):
176-
self.this_material = Material(self.values[1])
177+
self.this_material = Material(self.values[1], has_faces=self.collect_faces)
177178
self.materials[self.this_material.name] = self.this_material
178179

179180
@auto_consume

pywavefront/mesh.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,19 @@ class Mesh(object):
3737
"""This is a basic mesh for drawing using OpenGL. Interestingly, it does
3838
not contain its own vertices. These are instead drawn via materials."""
3939

40-
def __init__(self, name=None):
40+
def __init__(self, name=None, has_faces=False):
4141
self.name = name
4242
self.materials = []
4343

44+
self.has_faces = has_faces
45+
46+
# If self.has_faces, this is a list of triangle faces, i.e. triples with vertex indices.
47+
#
48+
# The original faces have been possibly triangulated if quad or even higher dimensional
49+
# faces were specified. See :meth:`ObjParser.consume_faces` for the specific triangulation
50+
# algorithm.
51+
self.faces = []
52+
4453
def has_material(self, new_material):
4554
"""Determine whether we already have a material of this name."""
4655
for material in self.materials:

pywavefront/obj.py

Lines changed: 90 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
3232
# POSSIBILITY OF SUCH DAMAGE.
3333
# ----------------------------------------------------------------------------
34+
from collections import namedtuple
3435
import logging
3536
import os
3637
import time
@@ -51,7 +52,7 @@ class ObjParser(Parser):
5152
cache_writer_cls = CacheWriter
5253

5354
def __init__(self, wavefront, file_name, strict=False, encoding="utf-8",
54-
create_materials=False, parse=True, cache=False):
55+
create_materials=False, collect_faces=False, parse=True, cache=False):
5556
"""
5657
Create a new obj parser
5758
:param wavefront: The wavefront object
@@ -68,11 +69,11 @@ def __init__(self, wavefront, file_name, strict=False, encoding="utf-8",
6869
self.mesh = None
6970
self.material = None
7071
self.create_materials = create_materials
72+
self.collect_faces = collect_faces
7173
self.cache = cache
7274
self.cache_loaded = None
7375

74-
# Stores ALL vertices, normals and texcoords for the entire file
75-
self.vertices = []
76+
# Stores normals and texcoords for the entire file
7677
self.normals = []
7778
self.tex_coords = []
7879

@@ -109,7 +110,7 @@ def post_parse(self):
109110

110111
# methods for parsing types of wavefront lines
111112
def parse_v(self):
112-
self.vertices += list(self.consume_vertices())
113+
self.wavefront.vertices += list(self.consume_vertices())
113114

114115
def consume_vertices(self):
115116
"""
@@ -213,7 +214,9 @@ def parse_mtllib(self):
213214
materials = self.material_parser_cls(
214215
os.path.join(self.dir, mtllib),
215216
encoding=self.encoding,
216-
strict=self.strict).materials
217+
strict=self.strict,
218+
collect_faces=self.collect_faces
219+
).materials
217220
self.wavefront.mtllibs.append(mtllib)
218221
except IOError:
219222
if self.create_materials:
@@ -233,7 +236,7 @@ def parse_usemtl(self):
233236
raise PywavefrontException('Unknown material: %s' % name)
234237

235238
# Create a new default material if configured to resolve missing ones
236-
self.material = Material(name, is_default=True)
239+
self.material = Material(name, is_default=True, has_faces=self.collect_faces)
237240
self.wavefront.materials[name] = self.material
238241

239242
if self.mesh is not None:
@@ -244,44 +247,82 @@ def parse_usemat(self):
244247

245248
@auto_consume
246249
def parse_o(self):
247-
self.mesh = Mesh(self.values[1])
250+
self.mesh = Mesh(self.values[1], has_faces=self.collect_faces)
248251
self.wavefront.add_mesh(self.mesh)
249252

250253
def parse_f(self):
251254
# Add default material if not created
252255
if self.material is None:
253-
self.material = Material("default{}".format(len(self.wavefront.materials)), is_default=True)
256+
self.material = Material(
257+
"default{}".format(len(self.wavefront.materials)),
258+
is_default=True,
259+
has_faces=self.collect_faces
260+
)
254261
self.wavefront.materials[self.material.name] = self.material
255262

256263
# Support objects without `o` statement
257264
if self.mesh is None:
258-
self.mesh = Mesh()
265+
self.mesh = Mesh(has_faces=self.collect_faces)
259266
self.wavefront.add_mesh(self.mesh)
260267
self.mesh.add_material(self.material)
261268

262269
self.mesh.add_material(self.material)
263270

264-
self.material.vertices += list(self.consume_faces())
271+
collected_faces = []
272+
consumed_vertices = self.consume_faces(collected_faces if self.collect_faces else None)
273+
self.material.vertices += list(consumed_vertices)
274+
275+
if self.collect_faces:
276+
self.mesh.faces += list(collected_faces)
265277

266278
# Since list() also consumes StopIteration we need to sanity check the line
267279
# to make sure the parser advances
268280
if self.values and self.values[0] == "f":
269281
self.next_line()
270282

271-
def consume_faces(self):
283+
def consume_faces(self, collected_faces = None):
272284
"""
273285
Consume all consecutive faces
274286
275-
If a 4th vertex is specified, we triangulate.
276-
In a perfect world we could consume this straight forward and draw using GL_TRIANGLE_FAN.
277-
This is however rarely the case..
287+
If more than three vertices are specified, we triangulate by the following procedure:
288+
289+
Let the face have n vertices in the order v_1 v_2 v_3 ... v_n, n >= 3.
290+
We emit the first face as usual: (v_1, v_2, v_3). For each remaining vertex v_j,
291+
j > 3, we emit (v_j, v_1, v_{j - 1}), e.g. (v_4, v_1, v_3), (v_5, v_1, v_4).
278292
279-
* If the face is co-planar but concave, then you need to triangulate the face
293+
In a perfect world we could consume all vertices straight forward and draw using
294+
GL_TRIANGLE_FAN (which exactly matches the procedure above).
295+
This is however rarely the case.
296+
297+
* If the face is co-planar but concave, then you need to triangulate the face.
280298
* If the face is not-coplanar, you are screwed, because OBJ doesn't preserve enough information
281-
to know what tessellation was intended
299+
to know what tessellation was intended.
300+
301+
We always triangulate to make it simple.
282302
283-
We always triangulate to make it simple
303+
:param collected_faces: A list into which all (possibly triangulated) faces will be written in the form
304+
of triples of the corresponding absolute vertex IDs. These IDs index the list
305+
self.wavefront.vertices.
306+
Specify None to prevent consuming faces (and thus saving memory usage).
284307
"""
308+
309+
# Helper tuple and function
310+
Vertex = namedtuple('Vertex', 'idx pos color uv normal')
311+
def emit_vertex(vertex):
312+
# Just yield all the values except for the index
313+
for v in vertex.uv:
314+
yield v
315+
316+
for v in vertex.color:
317+
yield v
318+
319+
for v in vertex.normal:
320+
yield v
321+
322+
for v in vertex.pos:
323+
yield v
324+
325+
285326
# Figure out the format of the first vertex
286327
# We raise an exception if any following vertex has a different format
287328
# NOTE: Order is always v/vt/vn where v is mandatory and vt and vn is optional
@@ -304,11 +345,11 @@ def consume_faces(self):
304345
# Are we referencing vertex with color info?
305346
vindex = int(parts[0])
306347
if vindex < 0:
307-
vindex += len(self.vertices)
348+
vindex += len(self.wavefront.vertices)
308349
else:
309350
vindex -= 1
310351

311-
vertex = self.vertices[vindex]
352+
vertex = self.wavefront.vertices[vindex]
312353
has_colors = len(vertex) == 6
313354

314355
# Prepare vertex format string
@@ -331,10 +372,8 @@ def consume_faces(self):
331372
# The first iteration processes the current/first f statement.
332373
# The loop continues until there are no more f-statements or StopIteration is raised by generator
333374
while True:
334-
v1, vlast = None, None
335-
336-
# Do we need to triangulate? Each line may contain a varying amount of elements
337-
triangulate = (len(self.values) - 1) > 3
375+
# The very first vertex, the last encountered and the current one
376+
v1, vlast, vcurrent = None, None, None
338377

339378
for i, v in enumerate(self.values[1:]):
340379
parts = v.split('/')
@@ -344,50 +383,48 @@ def consume_faces(self):
344383

345384
# Resolve negative index lookups
346385
if v_index < 0:
347-
v_index += len(self.vertices) + 1
386+
v_index += len(self.wavefront.vertices) + 1
348387

349388
if has_vt and t_index < 0:
350389
t_index += len(self.tex_coords) + 1
351390

352391
if has_vn and n_index < 0:
353392
n_index += len(self.normals) + 1
354393

355-
pos = self.vertices[v_index][0:3] if has_colors else self.vertices[v_index]
356-
color = self.vertices[v_index][3:] if has_colors else ()
357-
uv = self.tex_coords[t_index] if has_vt else ()
358-
normal = self.normals[n_index] if has_vn else ()
359-
360-
# Just yield all the values
361-
for v in uv:
362-
yield v
394+
vlast = vcurrent
395+
vcurrent = Vertex(
396+
idx = v_index,
397+
pos = self.wavefront.vertices[v_index][0:3] if has_colors else self.wavefront.vertices[v_index],
398+
color = self.wavefront.vertices[v_index][3:] if has_colors else (),
399+
uv = self.tex_coords[t_index] if has_vt else (),
400+
normal = self.normals[n_index] if has_vn else ()
401+
)
363402

364-
for v in color:
365-
yield v
403+
yield from emit_vertex(vcurrent)
366404

367-
for v in normal:
368-
yield v
405+
# Triangulation when more than 3 elements are present
406+
if i >= 3:
407+
# The current vertex has already been emitted.
408+
# Now just emit the first and the third vertices from the face
409+
yield from emit_vertex(v1)
410+
yield from emit_vertex(vlast)
369411

370-
for v in pos:
371-
yield v
412+
if i == 0:
413+
# Store the first vertex
414+
v1 = vcurrent
372415

373-
# Triangulation when more than 3 elements is present
374-
if triangulate:
416+
if (collected_faces is not None) and (i >= 2):
417+
if i == 2:
418+
# Append the first triangle face in usual order (i.e. as specified in the Wavefront file)
419+
collected_faces.append([v1.idx, vlast.idx, vcurrent.idx])
375420
if i >= 3:
376-
# Emit vertex 1 and 3 triangulating when a 4th vertex is specified
377-
for v in v1:
378-
yield v
379-
380-
for v in vlast:
381-
yield v
382-
383-
if i == 0:
384-
# Store the first vertex
385-
v1 = uv + color + normal + pos
386-
387-
# Store the last vertex
388-
vlast = uv + color + normal + pos
421+
# Triangulate the remaining part of the face by putting the current, the first
422+
# and the last parsed vertex in that order as a new face.
423+
# This order coincides deliberately with the order from vertex yielding above.
424+
collected_faces.append([vcurrent.idx, v1.idx, vlast.idx])
389425

390426
# Break out of the loop when there are no more f statements
427+
391428
try:
392429
self.next_line()
393430
except StopIteration:

pywavefront/wavefront.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,16 @@ class Wavefront(object):
4242
parser_cls = ObjParser
4343

4444
"""Import a wavefront .obj file."""
45-
def __init__(self, file_name, strict=False, encoding="utf-8", create_materials=False, parse=True, cache=False):
45+
def __init__(
46+
self,
47+
file_name,
48+
strict=False,
49+
encoding="utf-8",
50+
create_materials=False,
51+
collect_faces=False,
52+
parse=True,
53+
cache=False
54+
):
4655
"""
4756
Create a Wavefront instance
4857
:param file_name: file name and path of obj file to read
@@ -55,6 +64,7 @@ def __init__(self, file_name, strict=False, encoding="utf-8", create_materials=F
5564
self.mtllibs = []
5665
self.materials = {}
5766
self.meshes = {} # Name mapping
67+
self.vertices = []
5868
self.mesh_list = [] # Also includes anonymous meshes
5969

6070
self.parser = self.parser_cls(
@@ -63,6 +73,7 @@ def __init__(self, file_name, strict=False, encoding="utf-8", create_materials=F
6373
strict=strict,
6474
encoding=encoding,
6575
create_materials=create_materials,
76+
collect_faces=collect_faces,
6677
parse=parse,
6778
cache=cache)
6879

test/arbitrary-faces.obj

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Model with faces with vertex counts >= 3
2+
3+
o triangleOnly
4+
v 0.01 0.02 0.03
5+
v 0.04 0.05 0.06
6+
v 0.07 0.08 0.09
7+
v 0.11 0.12 0.13
8+
f 2 1 3
9+
10+
o quadOnly
11+
v 1.0 0.0 1.0
12+
v -1.0 0.0 1.0
13+
v 1.0 0.0 -1.0
14+
v -1.0 0.0 -1.0
15+
f 5 6 7 8
16+
f 6 5 7 8
17+
f 8 7 5 6
18+
19+
o arbitrary
20+
v 1.0 0.0 1.0
21+
v -1.0 0.0 1.0
22+
v 1.0 0.0 -1.0
23+
v -1.0 0.0 -1.0
24+
v -1.0 1.0 -1.0
25+
v -1.0 1.0 -2.0
26+
f 9 10 11 12
27+
f 12 9 10 11 13
28+
f 13 10 11 9 12 14
29+
f 14 12 10

0 commit comments

Comments
 (0)