Example #1
0
class ObjParser(Parser):
    """This parser parses lines from .obj files."""
    material_parser_cls = MaterialParser
    cache_loader_cls = CacheLoader
    cache_writer_cls = CacheWriter

    def __init__(self, wavefront, file_name, strict=False, encoding="utf-8",
                 create_materials=False, collect_faces=False, parse=True, cache=False):
        """
        Create a new obj parser
        :param wavefront: The wavefront object
        :param file_name: file name and path of obj file to read
        :param strict: Enable strict mode
        :param encoding: Encoding to read the text files
        :param create_materials: Create materials if they don't exist
        :param cache: Cache the loaded obj files in binary format
        :param parse: Should parse be called immediately or manually called later?
        """
        super(ObjParser, self).__init__(file_name, strict=strict, encoding=encoding)
        self.wavefront = wavefront

        self.mesh = None
        self.material = None
        self.create_materials = create_materials
        self.collect_faces = collect_faces
        self.cache = cache
        self.cache_loaded = None

        # Stores normals and texcoords for the entire file
        self.normals = []
        self.tex_coords = []

        if parse:
            self.parse()

    def parse(self):
        """Trigger cache load or call superclass parse()"""
        start = time.time()

        if self.cache:
            self.load_cache()

        if not self.cache_loaded:
            super(ObjParser, self).parse()

        logger.info("%s: Load time: %s", self.file_name, time.time() - start)

    def load_cache(self):
        """Loads the file using cached data"""
        self.cache_loaded = self.cache_loader_cls(
            self.file_name,
            self.wavefront,
            strict=self.strict,
            create_materials=self.create_materials,
            encoding=self.encoding,
            parse=self.parse,
        ).parse()

    def post_parse(self):
        """Called after parsing is done"""
        if self.cache and not self.cache_loaded:
            self.cache_writer_cls(self.file_name, self.wavefront).write()

    # methods for parsing types of wavefront lines
    def parse_v(self):
        self.wavefront.vertices += list(self.consume_vertices())

    def consume_vertices(self):
        """
        Consumes all consecutive vertices.
        NOTE: There is no guarantee this will consume all vertices since other
        statements can also occur in the vertex list
        """
        while True:
            # Vertex color
            if len(self.values) == 7:
                yield (
                    float(self.values[1]),
                    float(self.values[2]),
                    float(self.values[3]),
                    float(self.values[4]),
                    float(self.values[5]),
                    float(self.values[6]),
                )
            # Positions only
            else:
                yield (
                    float(self.values[1]),
                    float(self.values[2]),
                    float(self.values[3]),
                )

            try:
                self.next_line()
            except StopIteration:
                break

            if not self.values:
                break

            if self.values[0] != "v":
                break

    def parse_vn(self):
        self.normals += list(self.consume_normals())

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

    def consume_normals(self):
        """Consumes all consecutive texture coordinate lines"""
        # The first iteration processes the current/first vn statement.
        # The loop continues until there are no more vn-statements or StopIteration is raised by generator
        while True:
            yield (
                float(self.values[1]),
                float(self.values[2]),
                float(self.values[3]),
            )

            try:
                self.next_line()
            except StopIteration:
                break

            if not self.values:
                break

            if self.values[0] != "vn":
                break

    def parse_vt(self):
        self.tex_coords += list(self.consume_texture_coordinates())

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

    def consume_texture_coordinates(self):
        """Consume all consecutive texture coordinates"""
        # The first iteration processes the current/first vt statement.
        # The loop continues until there are no more vt-statements or StopIteration is raised by generator
        while True:
            yield (
                float(self.values[1]),
                float(self.values[2]),
            )

            try:
                self.next_line()
            except StopIteration:
                break

            if not self.values:
                break

            if self.values[0] != "vt":
                break

    @auto_consume
    def parse_mtllib(self):
        mtllib = " ".join(self.values[1:])
        try:
            materials = self.material_parser_cls(
                os.path.join(self.dir, mtllib),
                encoding=self.encoding,
                strict=self.strict,
                collect_faces=self.collect_faces
            ).materials
            self.wavefront.mtllibs.append(mtllib)
        except IOError:
            if self.create_materials:
                return
            raise

        for name, material in materials.items():
            self.wavefront.materials[name] = material

    @auto_consume
    def parse_usemtl(self):
        name = " ".join(self.values[1:])
        self.material = self.wavefront.materials.get(name, None)

        if self.material is None:
            if not self.create_materials:
                raise PywavefrontException('Unknown material: %s' % name)

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

        if self.mesh is not None:
            self.mesh.add_material(self.material)

    def parse_usemat(self):
        self.parse_usemtl()

    @auto_consume
    def parse_o(self):
        self.mesh = Mesh(self.values[1], has_faces=self.collect_faces)
        self.wavefront.add_mesh(self.mesh)

    def parse_f(self):
        # Add default material if not created
        if self.material is None:
            self.material = Material(
                "default{}".format(len(self.wavefront.materials)),
                is_default=True,
                has_faces=self.collect_faces
            )
            self.wavefront.materials[self.material.name] = self.material

        # Support objects without `o` statement
        if self.mesh is None:
            self.mesh = Mesh(has_faces=self.collect_faces)
            self.wavefront.add_mesh(self.mesh)
            self.mesh.add_material(self.material)

        self.mesh.add_material(self.material)

        collected_faces = []
        consumed_vertices = self.consume_faces(collected_faces if self.collect_faces else None)
        self.material.vertices += list(consumed_vertices)

        if self.collect_faces:
            self.mesh.faces += list(collected_faces)

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

    def consume_faces(self, collected_faces = None):
        """
        Consume all consecutive faces

        If more than three vertices are specified, we triangulate by the following procedure:

            Let the face have n vertices in the order v_1 v_2 v_3 ... v_n, n >= 3.
            We emit the first face as usual: (v_1, v_2, v_3). For each remaining vertex v_j,
            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).

        In a perfect world we could consume all vertices straight forward and draw using
        GL_TRIANGLE_FAN (which exactly matches the procedure above).
        This is however rarely the case.

        * If the face is co-planar but concave, then you need to triangulate the face.
        * If the face is not-coplanar, you are screwed, because OBJ doesn't preserve enough information
          to know what tessellation was intended.

        We always triangulate to make it simple.

            :param collected_faces: A list into which all (possibly triangulated) faces will be written in the form
                                    of triples of the corresponding absolute vertex IDs. These IDs index the list
                                    self.wavefront.vertices.
                                    Specify None to prevent consuming faces (and thus saving memory usage).
        """

        # Helper tuple and function
        Vertex = namedtuple('Vertex', 'idx pos color uv normal')
        def emit_vertex(vertex):
            # Just yield all the values except for the index
            for v in vertex.uv:
                yield v

            for v in vertex.color:
                yield v

            for v in vertex.normal:
                yield v

            for v in vertex.pos:
                yield v


        # Figure out the format of the first vertex
        # We raise an exception if any following vertex has a different format
        # NOTE: Order is always v/vt/vn where v is mandatory and vt and vn is optional
        has_vt = False
        has_vn = False
        has_colors = False

        parts = self.values[1].split('/')
        # We assume texture coordinates are present
        if len(parts) == 2:
            has_vt = True

        # We have a vn, but not necessarily a vt
        elif len(parts) == 3:
            # Check for empty vt "1//1"
            if parts[1] != '':
                has_vt = True
            has_vn = True

        # Are we referencing vertex with color info?
        vindex = int(parts[0])
        if vindex < 0:
            vindex += len(self.wavefront.vertices)
        else:
            vindex -= 1

        vertex = self.wavefront.vertices[vindex]
        has_colors = len(vertex) == 6

        # Prepare vertex format string
        vertex_format = "_".join(e[0] for e in [
            ("T2F", has_vt),
            ("C3F", has_colors),
            ("N3F", has_vn),
            ("V3F", True)
        ] if e[1])

        # If the material already have vertex data, ensure the same format is used
        if self.material.vertex_format and self.material.vertex_format != vertex_format:
            raise ValueError((
                "Trying to merge vertex data with different format: {}. "
                "Material {} has vertex format {}"
            ).format(vertex_format, self.material.name, self.material.vertex_format))

        self.material.vertex_format = vertex_format

        # The first iteration processes the current/first f statement.
        # The loop continues until there are no more f-statements or StopIteration is raised by generator
        while True:
            # The very first vertex, the last encountered and the current one
            v1, vlast, vcurrent = None, None, None

            for i, v in enumerate(self.values[1:]):
                parts = v.split('/')
                v_index = (int(parts[0]) - 1)
                t_index = (int(parts[1]) - 1) if has_vt else None
                n_index = (int(parts[2]) - 1) if has_vn else None

                # Resolve negative index lookups
                if v_index < 0:
                    v_index += len(self.wavefront.vertices) + 1

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

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

                vlast = vcurrent
                vcurrent = Vertex(
                    idx = v_index,
                    pos = self.wavefront.vertices[v_index][0:3] if has_colors else self.wavefront.vertices[v_index],
                    color = self.wavefront.vertices[v_index][3:] if has_colors else (),
                    uv = self.tex_coords[t_index] if has_vt and t_index < len(self.tex_coords) else (),
                    normal = self.normals[n_index] if has_vn and n_index < len(self.normals) else ()
                )

                yield from emit_vertex(vcurrent)

                # Triangulation when more than 3 elements are present
                if i >= 3:
                    # The current vertex has already been emitted.
                    # Now just emit the first and the third vertices from the face
                    yield from emit_vertex(v1)
                    yield from emit_vertex(vlast)

                if i == 0:
                    # Store the first vertex
                    v1 = vcurrent

                if (collected_faces is not None) and (i >= 2):
                    if i == 2:
                        # Append the first triangle face in usual order (i.e. as specified in the Wavefront file)
                        collected_faces.append([v1.idx, vlast.idx, vcurrent.idx])
                    if i >= 3:
                        # Triangulate the remaining part of the face by putting the current, the first
                        # and the last parsed vertex in that order as a new face.
                        # This order coincides deliberately with the order from vertex yielding above.
                        collected_faces.append([vcurrent.idx, v1.idx, vlast.idx])

            # Break out of the loop when there are no more f statements

            try:
                self.next_line()
            except StopIteration:
                break

            if not self.values:
                break

            if self.values[0] != "f":
                break
Example #2
0
class ObjParser(Parser):
    """This parser parses lines from .obj files."""
    def __init__(self,
                 wavefront,
                 file_name,
                 strict=False,
                 encoding="utf-8",
                 parse=True):
        """
        Create a new obj parser
        :param wavefront: The wavefront object
        :param file_name: file name and path of obj file to read
        :param strict: Enable strict mode
        :param encoding: Encoding to read the text files
        :param parse: Should parse be called immediately or manually called later?
        """
        super(ObjParser, self).__init__(file_name,
                                        strict=strict,
                                        encoding=encoding)
        self.wavefront = wavefront

        self.mesh = None
        self.material = None
        self.vertices = [[0., 0., 0.]]
        self.normals = [[0., 0., 0.]]
        self.tex_coords = [[0., 0.]]

        if parse:
            self.parse()

    # methods for parsing types of wavefront lines
    def parse_v(self):
        self.vertices += list(self.consume_vertices())

    def consume_vertices(self):
        """
        Consumes all consecutive vertices.
        NOTE: There is no guarantee this will consume all vertices since other
        statements can also occur in the vertex list
        """
        while True:
            # TODO: Check for vertex color
            yield (
                float(self.values[1]),
                float(self.values[2]),
                float(self.values[3]),
            )

            self.next_line()
            if self.values[0] != "v":
                break

    def parse_vn(self):
        self.normals += list(self.consume_normals())

        # Since list() also consumes StopIteration we need to sanity check the line
        # to make sure the parser advances
        if self.values[0] == "vn":
            self.next_line()

    def consume_normals(self):
        """Consumes all consecutive texture coordinate lines"""
        # The first iteration processes the current/first vn statement.
        # The loop continues until there are no more vn-statements or StopIteration is raised by generator
        while True:
            yield (
                float(self.values[1]),
                float(self.values[2]),
                float(self.values[3]),
            )

            self.next_line()
            if self.values[0] != "vn":
                break

    def parse_vt(self):
        self.tex_coords += list(self.consume_texture_coordinates())

        # Since list() also consumes StopIteration we need to sanity check the line
        # to make sure the parser advances
        if self.values[0] == "vt":
            self.next_line()

    def consume_texture_coordinates(self):
        """Consume all consecutive texture coordinates"""
        # The first iteration processes the current/first vt statement.
        # The loop continues until there are no more vt-statements or StopIteration is raised by generator
        while True:
            yield (
                float(self.values[1]),
                float(self.values[2]),
            )

            self.next_line()
            if self.values[0] != "vt":
                break

    @auto_consume
    def parse_mtllib(self):
        mtllib = os.path.join(self.dir, " ".join(self.values[1:]))
        materials = MaterialParser(mtllib,
                                   encoding=self.encoding,
                                   strict=self.strict).materials

        for material_name, material_object in materials.items():
            self.wavefront.materials[material_name] = material_object

    @auto_consume
    def parse_usemtl(self):
        self.material = self.wavefront.materials.get(self.values[1], None)

        if self.material is None:
            raise PywavefrontException('Unknown material: %s' % self.values[1])

        if self.mesh is not None:
            self.mesh.add_material(self.material)

    def parse_usemat(self):
        self.parse_usemtl()

    @auto_consume
    def parse_o(self):
        self.mesh = Mesh(self.values[1])
        self.wavefront.add_mesh(self.mesh)

    def parse_f(self):
        # Support objects without `o` statement
        if self.mesh is None:
            self.mesh = Mesh()
            self.wavefront.add_mesh(self.mesh)

        # Add default material if not created
        if self.material is None:
            self.material = Material(is_default=True)
            self.wavefront.materials[self.material.name] = self.material

        self.mesh.add_material(self.material)

        self.material.vertices += list(self.consume_faces())

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

    def consume_faces(self):
        """
        Consume all consecutive faces

        If a 4th vertex is specified, we triangulate.
        In a perfect world we could consume this straight forward and draw using GL_TRIANGLE_FAN.
        This is however rarely the case..

        * If the face is co-planar but concave, then you need to triangulate the face
        * If the face is not-coplanar, you are screwed, because OBJ doesn't preserve enough information
          to know what tessellation was intended

        We always triangulate to make it simple
        """
        # The first iteration processes the current/first f statement.
        # The loop continues until there are no more f-statements or StopIteration is raised by generator
        while True:
            v1 = None
            vlast = None

            for i, v in enumerate(self.values[1:]):
                v_index, t_index, n_index = (
                    list(map(int, [j or 0
                                   for j in v.split('/')])) + [0, 0])[:3]

                # Resolve negative index lookups
                if v_index < 0:
                    v_index += len(self.vertices) - 1

                if t_index < 0:
                    t_index += len(self.tex_coords) - 1

                if n_index < 0:
                    n_index += len(self.normals) - 1

                vertex = (
                    self.tex_coords[t_index][0],
                    self.tex_coords[t_index][1],
                    self.normals[n_index][0],
                    self.normals[n_index][1],
                    self.normals[n_index][2],
                    self.vertices[v_index][0],
                    self.vertices[v_index][1],
                    self.vertices[v_index][2],
                )

                for v in vertex:
                    yield v

                if i >= 3:
                    # Emit vertex 1 and 3 triangulating when a 4th vertex is specified
                    for v in v1:
                        yield v

                    for v in vlast:
                        yield v

                if i == 0:
                    v1 = vertex

                vlast = vertex

            # Break out of the loop when there are no more f statements
            self.next_line()
            if self.values[0] != "f":
                break
Example #3
0
class ObjParser(Parser):
    """This parser parses lines from .obj files."""
    material_parser_cls = MaterialParser
    cache_loader_cls = CacheLoader
    cache_writer_cls = CacheWriter

    def __init__(self, wavefront, file_name, strict=False, encoding="utf-8",
                 create_materials=False, collect_faces=False, parse=True, cache=False,
                 warn_material=True):
        """
        Create a new obj parser
        :param wavefront: The wavefront object
        :param file_name: file name and path of obj file to read
        :param strict: Enable strict mode
        :param encoding: Encoding to read the text files
        :param create_materials: Create materials if they don't exist
        :param cache: Cache the loaded obj files in binary format
        :param parse: Should parse be called immediately or manually called later?
        """
        super(ObjParser, self).__init__(file_name, strict=strict, encoding=encoding)
        self.wavefront = wavefront

        self.mesh = None
        self.material = None
        self.create_materials = create_materials
        self.collect_faces = collect_faces
        self.cache = cache
        self.cache_loaded = None
        self.warn_material = warn_material

        # Stores normals and texcoords for the entire file
        self.normals = []
        self.tex_coords = []

        if parse:
            self.parse()

    def parse(self):
        """Trigger cache load or call superclass parse()"""
        start = time.time()

        if self.cache:
            self.load_cache()

        if not self.cache_loaded:
            super(ObjParser, self).parse()

        logger.info("%s: Load time: %s", self.file_name, time.time() - start)

    def load_cache(self):
        """Loads the file using cached data"""
        self.cache_loaded = self.cache_loader_cls(
            self.file_name,
            self.wavefront,
            strict=self.strict,
            create_materials=self.create_materials,
            encoding=self.encoding,
            parse=self.parse,
        ).parse()

    def post_parse(self):
        """Called after parsing is done"""
        if self.cache and not self.cache_loaded:
            self.cache_writer_cls(self.file_name, self.wavefront).write()

    # methods for parsing types of wavefront lines
    def parse_v(self):
        self.wavefront.vertices += list(self.consume_vertices())

    def consume_vertices(self):
        """
        Consumes all consecutive vertices.
        NOTE: There is no guarantee this will consume all vertices since other
        statements can also occur in the vertex list
        """
        while True:
            # Vertex color
            if len(self.values) == 7:
                yield (
                    float(self.values[1]),
                    float(self.values[2]),
                    float(self.values[3]),
                    float(self.values[4]),
                    float(self.values[5]),
                    float(self.values[6]),
                )
            # Positions only
            else:
                yield (
                    float(self.values[1]),
                    float(self.values[2]),
                    float(self.values[3]),
                )

            try:
                self.next_line()
            except StopIteration:
                break

            if not self.values:
                break

            if self.values[0] != "v":
                break

    def parse_vn(self):
        self.normals += list(self.consume_normals())

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

    def consume_normals(self):
        """Consumes all consecutive texture coordinate lines"""
        # The first iteration processes the current/first vn statement.
        # The loop continues until there are no more vn-statements or StopIteration is raised by generator
        while True:
            yield (
                float(self.values[1]),
                float(self.values[2]),
                float(self.values[3]),
            )

            try:
                self.next_line()
            except StopIteration:
                break

            if not self.values:
                break

            if self.values[0] != "vn":
                break

    def parse_vt(self):
        self.tex_coords += list(self.consume_texture_coordinates())

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

    def consume_texture_coordinates(self):
        """Consume all consecutive texture coordinates"""
        # The first iteration processes the current/first vt statement.
        # The loop continues until there are no more vt-statements or StopIteration is raised by generator
        while True:
            yield (
                float(self.values[1]),
                float(self.values[2]),
            )

            try:
                self.next_line()
            except StopIteration:
                break

            if not self.values:
                break

            if self.values[0] != "vt":
                break

    @auto_consume
    def parse_mtllib(self):
        mtllib = " ".join(self.values[1:])
        try:
            materials = self.material_parser_cls(
                self.dir / mtllib,
                encoding=self.encoding,
                strict=self.strict,
                collect_faces=self.collect_faces
            ).materials
            self.wavefront.mtllibs.append(mtllib)
        except IOError:
            if self.create_materials:
                return
            raise

        for name, material in materials.items():
            self.wavefront.materials[name] = material

    @auto_consume
    def parse_usemtl(self):
        name = " ".join(self.values[1:])
        self.material = self.wavefront.materials.get(name, None)

        if self.material is None:
            if not self.create_materials:
                raise PywavefrontException('Unknown material: %s' % name)

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

        if self.mesh is not None:
            self.mesh.add_material(self.material)

    def parse_usemat(self):
        self.parse_usemtl()

    @auto_consume
    def parse_o(self):
        self.mesh = Mesh(self.values[1], has_faces=self.collect_faces)
        self.wavefront.add_mesh(self.mesh)

    def parse_f(self):
        # Add default material if not created
        if self.material is None:
            self.material = Material(
                "default{}".format(len(self.wavefront.materials)),
                is_default=True,
                has_faces=self.collect_faces
            )
            self.wavefront.materials[self.material.name] = self.material

        # Support objects without `o` statement
        if self.mesh is None:
            self.mesh = Mesh(has_faces=self.collect_faces)
            self.wavefront.add_mesh(self.mesh)
            self.mesh.add_material(self.material)

        self.mesh.add_material(self.material)

        collected_faces = []
        consumed_vertices = self.consume_faces(collected_faces if self.collect_faces else None)
        self.material.vertices += list(consumed_vertices)

        if self.collect_faces:
            self.mesh.faces += list(collected_faces)

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

    def consume_faces(self, collected_faces = None):
        """
        Consume all consecutive faces

        If more than three vertices are specified, we triangulate by the following procedure:

            Let the face have n vertices in the order v_1 v_2 v_3 ... v_n, n >= 3.
            We emit the first face as usual: (v_1, v_2, v_3). For each remaining vertex v_j,
            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).

        In a perfect world we could consume all vertices straight forward and draw using
        GL_TRIANGLE_FAN (which exactly matches the procedure above).
        This is however rarely the case.

        * If the face is co-planar but concave, then you need to triangulate the face.
        * If the face is not-coplanar, you are screwed, because OBJ doesn't preserve enough information
          to know what tessellation was intended.

        We always triangulate to make it simple.

            :param collected_faces: A list into which all (possibly triangulated) faces will be written in the form
                                    of triples of the corresponding absolute vertex IDs. These IDs index the list
                                    self.wavefront.vertices.
                                    Specify None to prevent consuming faces (and thus saving memory usage).
        """

        # Helper tuple and function
        Vertex = namedtuple('Vertex', 'idx pos color uv normal')
        def emit_vertex(vertex):
            # Just yield all the values except for the index
            for v in vertex.uv:
                yield v

            for v in vertex.color:
                yield v

            for v in vertex.normal:
                yield v

            for v in vertex.pos:
                yield v


        # Figure out the format of the first vertex
        # We raise an exception if any following vertex has a different format
        # NOTE: Order is always v/vt/vn where v is mandatory and vt and vn is optional
        has_vt = False
        has_vn = False
        has_colors = False

        parts = self.values[1].split('/')
        # We assume texture coordinates are present
        if len(parts) == 2:
            has_vt = True

        # We have a vn, but not necessarily a vt
        elif len(parts) == 3:
            # Check for empty vt "1//1"
            if parts[1] != '':
                has_vt = True
            has_vn = True

        # Are we referencing vertex with color info?
        vindex = int(parts[0])
        if vindex < 0:
            vindex += len(self.wavefront.vertices)
        else:
            vindex -= 1

        vertex = self.wavefront.vertices[vindex]
        has_colors = len(vertex) == 6

        # Prepare vertex format string
        vertex_format = "_".join(e[0] for e in [
            ("T2F", has_vt),
            ("C3F", has_colors),
            ("N3F", has_vn),
            ("V3F", True)
        ] if e[1])

        # If the material already have vertex data, ensure the same format is used
        if self.material.vertex_format and self.material.vertex_format != vertex_format:
            # raise ValueError((
            if self.warn_material:
                logger.warn(
                    "Trying to merge vertex data with different format: {}. "
                    "Material {} has vertex format {}"
                    .format(vertex_format, self.material.name, self.material.vertex_format)
                )

        self.material.vertex_format = vertex_format

        # The first iteration processes the current/first f statement.
        # The loop continues until there are no more f-statements or StopIteration is raised by generator
        while True:
            # The very first vertex, the last encountered and the current one
            v1, vlast, vcurrent = None, None, None

            for i, v in enumerate(self.values[1:]):
                parts = v.split('/')
                v_index = (int(parts[0]) - 1)
                # uv field might be blank
                try:
                    t_index = (int(parts[1]) - 1) if has_vt else None
                except ValueError:
                    t_index = 0
                try:
                    n_index = (int(parts[2]) - 1) if has_vn else None
                except ValueError:
                    n_index = 0

                # Resolve negative index lookups
                if v_index < 0:
                    v_index += len(self.wavefront.vertices) + 1

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

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

                vlast = vcurrent
                vcurrent = Vertex(
                    idx = v_index,
                    pos = self.wavefront.vertices[v_index][0:3] if has_colors else self.wavefront.vertices[v_index],
                    color = self.wavefront.vertices[v_index][3:] if has_colors else (),
                    uv = self.tex_coords[t_index] if has_vt and t_index < len(self.tex_coords) else (),
                    normal = self.normals[n_index] if has_vn and n_index < len(self.normals) else ()
                )

                yield from emit_vertex(vcurrent)

                # Triangulation when more than 3 elements are present
                if i >= 3:
                    # The current vertex has already been emitted.
                    # Now just emit the first and the third vertices from the face
                    yield from emit_vertex(v1)
                    yield from emit_vertex(vlast)

                if i == 0:
                    # Store the first vertex
                    v1 = vcurrent

                if (collected_faces is not None) and (i >= 2):
                    if i == 2:
                        # Append the first triangle face in usual order (i.e. as specified in the Wavefront file)
                        collected_faces.append([v1.idx, vlast.idx, vcurrent.idx])
                    if i >= 3:
                        # Triangulate the remaining part of the face by putting the current, the first
                        # and the last parsed vertex in that order as a new face.
                        # This order coincides deliberately with the order from vertex yielding above.
                        collected_faces.append([vcurrent.idx, v1.idx, vlast.idx])

            # Break out of the loop when there are no more f statements

            try:
                self.next_line()
            except StopIteration:
                break

            if not self.values:
                break

            if self.values[0] != "f":
                break
Example #4
0
class ObjParser(Parser):
    """This parser parses lines from .obj files."""
    def __init__(self,
                 wavefront,
                 file_name,
                 strict=False,
                 encoding="utf-8",
                 create_materials=False,
                 parse=True):
        """
        Create a new obj parser
        :param wavefront: The wavefront object
        :param file_name: file name and path of obj file to read
        :param strict: Enable strict mode
        :param encoding: Encoding to read the text files
        :param create_materials: Create materials if they don't exist
        :param parse: Should parse be called immediately or manually called later?
        """
        super(ObjParser, self).__init__(file_name,
                                        strict=strict,
                                        encoding=encoding)
        self.wavefront = wavefront

        self.mesh = None
        self.material = None
        self.create_materials = create_materials

        # Stores ALL vertices, normals and texcoords for the entire file
        self.vertices = []
        self.normals = []
        self.tex_coords = []

        if parse:
            self.parse()

    # methods for parsing types of wavefront lines
    def parse_v(self):
        self.vertices += list(self.consume_vertices())

    def consume_vertices(self):
        """
        Consumes all consecutive vertices.
        NOTE: There is no guarantee this will consume all vertices since other
        statements can also occur in the vertex list
        """
        while True:
            # Vertex color
            if len(self.values) == 7:
                yield (
                    float(self.values[1]),
                    float(self.values[2]),
                    float(self.values[3]),
                    float(self.values[4]),
                    float(self.values[5]),
                    float(self.values[6]),
                )
            # Positions only
            else:
                yield (
                    float(self.values[1]),
                    float(self.values[2]),
                    float(self.values[3]),
                )

            self.next_line()
            if not self.values:
                break

            if self.values[0] != "v":
                break

    def parse_vn(self):
        self.normals += list(self.consume_normals())

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

    def consume_normals(self):
        """Consumes all consecutive texture coordinate lines"""
        # The first iteration processes the current/first vn statement.
        # The loop continues until there are no more vn-statements or StopIteration is raised by generator
        while True:
            yield (
                float(self.values[1]),
                float(self.values[2]),
                float(self.values[3]),
            )

            self.next_line()
            if not self.values:
                break

            if self.values[0] != "vn":
                break

    def parse_vt(self):
        self.tex_coords += list(self.consume_texture_coordinates())

        # Since list() also consumes StopIteration we need to sanity check the line
        # to make sure the parser advances
        if self.values[0] == "vt":
            self.next_line()

    def consume_texture_coordinates(self):
        """Consume all consecutive texture coordinates"""
        # The first iteration processes the current/first vt statement.
        # The loop continues until there are no more vt-statements or StopIteration is raised by generator
        while True:
            yield (
                float(self.values[1]),
                float(self.values[2]),
            )

            self.next_line()
            if not self.values:
                break

            if self.values[0] != "vt":
                break

    @auto_consume
    def parse_mtllib(self):
        mtllib = os.path.join(self.dir, " ".join(self.values[1:]))
        try:
            materials = MaterialParser(mtllib,
                                       encoding=self.encoding,
                                       strict=self.strict).materials
        except IOError:
            if self.create_materials:
                return
            raise

        for name, material in materials.items():
            self.wavefront.materials[name] = material

    @auto_consume
    def parse_usemtl(self):
        name = " ".join(self.values[1:])
        self.material = self.wavefront.materials.get(name, None)

        if self.material is None:
            if not self.create_materials:
                raise PywavefrontException('Unknown material: %s' % name)

            # Create a new default material if configured to resolve missing ones
            self.material = Material(name=name, is_default=True)
            self.wavefront.materials[name] = self.material

        if self.mesh is not None:
            self.mesh.add_material(self.material)

    def parse_usemat(self):
        self.parse_usemtl()

    @auto_consume
    def parse_o(self):
        self.mesh = Mesh(self.values[1])
        self.wavefront.add_mesh(self.mesh)

    def parse_f(self):
        # Add default material if not created
        if self.material is None:
            self.material = Material(is_default=True)
            self.wavefront.materials[self.material.name] = self.material

        # Support objects without `o` statement
        if self.mesh is None:
            self.mesh = Mesh()
            self.wavefront.add_mesh(self.mesh)
            self.mesh.add_material(self.material)

        self.mesh.add_material(self.material)

        self.material.vertices += list(self.consume_faces())

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

    def consume_faces(self):
        """
        Consume all consecutive faces

        If a 4th vertex is specified, we triangulate.
        In a perfect world we could consume this straight forward and draw using GL_TRIANGLE_FAN.
        This is however rarely the case..

        * If the face is co-planar but concave, then you need to triangulate the face
        * If the face is not-coplanar, you are screwed, because OBJ doesn't preserve enough information
          to know what tessellation was intended

        We always triangulate to make it simple
        """
        # Figure out the format of the first vertex
        # We raise an exception if any following vertex has a different format
        # NOTE: Order is always v/vt/vn where v is mandatory and vt and vn is optional
        has_vt = False
        has_vn = False
        has_colors = False

        parts = self.values[1].split('/')
        # We assume texture coordinates are present
        if len(parts) == 2:
            has_vt = True

        # We have a vn, but not necessarily a vt
        elif len(parts) == 3:
            # Check for empty vt "1//1"
            if parts[1] != '':
                has_vt = True
            has_vn = True

        # Are we referencing vertex with color info?
        vertex = self.vertices[int(parts[0]) - 1]
        has_colors = len(vertex) == 6

        # Prepare vertex format string
        vertex_format = "_".join(
            e[0]
            for e in [("T2F", has_vt), ("C3F",
                                        has_colors), ("N3F",
                                                      has_vn), ("V3F", True)]
            if e[1])

        # If the material already have vertex data, ensure the same format is used
        if self.material.vertex_format and self.material.vertex_format != vertex_format:
            raise ValueError(
                ("Trying to merge vertex data with different format: {}. "
                 "Material {} has vertex format {}").format(
                     vertex_format, self.material.name,
                     self.material.vertex_format))

        self.material.vertex_format = vertex_format

        # The first iteration processes the current/first f statement.
        # The loop continues until there are no more f-statements or StopIteration is raised by generator
        while True:
            v1, vlast = None, None

            # Do we need to triangulate? Each line may contain a varying amount of elements
            triangulate = (len(self.values) - 1) > 3

            for i, v in enumerate(self.values[1:]):
                parts = v.split('/')
                v_index = (int(parts[0]) - 1)
                t_index = (int(parts[1]) - 1) if has_vt else None
                n_index = (int(parts[2]) - 1) if has_vn else None

                # Resolve negative index lookups
                if v_index < 0:
                    v_index += len(self.vertices) - 1

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

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

                pos = self.vertices[v_index][
                    0:3] if has_colors else self.vertices[v_index]
                color = self.vertices[v_index][3:] if has_colors else ()
                uv = self.tex_coords[t_index] if has_vt else ()
                normal = self.normals[n_index] if has_vn else ()

                # Just yield all the values
                for v in uv:
                    yield v

                for v in color:
                    yield v

                for v in normal:
                    yield v

                for v in pos:
                    yield v

                # Triangulation when more than 3 elements is present
                if triangulate:
                    if i >= 3:
                        # Emit vertex 1 and 3 triangulating when a 4th vertex is specified
                        for v in v1:
                            yield v

                        for v in vlast:
                            yield v

                    if i == 0:
                        # Store the first vertex
                        v1 = uv + color + normal + pos

                    # Store the last vertex
                    vlast = uv + color + normal + pos

            # Break out of the loop when there are no more f statements
            self.next_line()
            if not self.values:
                break

            if self.values[0] != "f":
                break