Exemple #1
0
def loadOBJ(data, aux_file_loader=None, validate_output=False):
    """Loads an OBJ file
    
    :param data: A binary data string containing the OBJ file
    :param aux_file_loader: Should be a callable function that takes one parameter.
                            The parameter will be a string containing an auxiliary
                            file that needs to be found, in this case usually a .mtl
                            file or a texture file.
    
    :returns: An instance of :class:`collada.Collada` or None if could not be loaded
    """

    mesh = collada.Collada(validate_output=validate_output)
    namer = NameUniqifier()
    material_map = {}
    cimages = []
    materialNamer = NameUniqifier()

    vertices = []
    normals = []
    texcoords = []

    groups = []
    group = ObjGroup(namer.name("default"))
    geometry_name = namer.name("convertedobjgeometry")

    file_like = StringIO(to_unicode(data))
    for line in file_like:
        line = line.strip()

        # ignore blank lines and comments
        if len(line) == 0 or line.startswith('#'):
            continue

        # split off the first non-whitespace token and ignore the line if there isn't > 1 token
        splitup = line.split(None, 1)
        if len(splitup) != 2:
            continue
        command, line = splitup

        if command == 'v':
            line_tokens = line.split()
            vertices.extend(line_tokens[:3])

        elif command == 'vn':
            line_tokens = line.split()
            normals.extend(line_tokens[:3])

        elif command == 'vt':
            line_tokens = line.split()
            texcoords.extend(line_tokens[:2])

        # TODO: other vertex data statements
        # vp
        # cstype
        # deg
        # bmat
        # step

        elif command == 'f':
            faces = line.split()

            if group.face_mode == FACEMODE.UNKNOWN:
                group.face_mode = detectFaceStyle(faces[0])
                if group.face_mode is None:
                    sys.stderr.write(
                        "Error: could not detect face type for line '%s'" %
                        line)
                    return

            group.face_lengths.append(len(faces))

            # Don't decode the faces here because the / separators have to be parsed out
            # and this is very slow to do one at a time. Instead, just append to a list
            # which is much faster than appending to a string, and it will get joined and
            # parsed later
            group.face_indices.append(line)

        elif command == 'l':
            faces = line.split()

            if group.face_mode == FACEMODE.UNKNOWN:
                group.face_mode = detectFaceStyle(faces[0])
                if group.face_mode is None:
                    sys.stderr.write(
                        "Error: could not detect face type for line '%s'" %
                        line)
                    return

            # COLLADA defines lines as a pair of points, so the index values "1 2 3 4" would
            # refer to *two* lines, one between 1 and 2 and one between 3 and 4. OBJ defines
            # lines as continous, so it would be three lines: 1-2, 2-3, 3-4. This duplicates
            # the points to get pairs for COLLADA. This is not very efficient, but not sure
            # of a faster way to do this and I've never seen any files with a huge number of
            # lines in it anyway.
            line = faces[0] + " " + faces[1]
            prev = faces[1]
            for cur in faces[2:]:
                line += " " + prev + " " + cur
                prev = cur
            group.line_indices.append(line)

        elif command == 'p':
            faces = line.split()

            if group.face_mode == FACEMODE.UNKNOWN:
                group.face_mode = detectFaceStyle(faces[0])
                if group.face_mode is None:
                    sys.stderr.write(
                        "Error: could not detect face type for line '%s'" %
                        line)
                    return

            # COLLADA does not have points, so this converts a point to a line with two
            # identical endpoints
            line = " ".join(f + " " + f for f in faces)
            group.line_indices.append(line)

        # TODO: other elements
        # curv
        # curv2
        # surf

        elif command == 'g':
            if group.empty():
                # first group without any previous data, so just set name
                group.name = namer.name(line)
                continue

            # end of previous group and start of new group
            groups.append(group)
            group = ObjGroup(namer.name(line))

        elif command == 's':
            # there is no way to map shading groups into collada
            continue

        elif command == 'o':
            geometry_name = namer.name(line)

        # TODO: grouping info
        # mg

        # TODO: Free-form curve/surface body statements
        # parm
        # trim
        # hole
        # scrv
        # sp
        # end
        # con

        elif command == 'mtllib':
            mtl_file = None
            if aux_file_loader is not None:
                mtl_file = aux_file_loader(line)
            if mtl_file is not None:
                material_data = loadMaterialLib(
                    mtl_file,
                    namer=materialNamer,
                    aux_file_loader=aux_file_loader)
                material_map.update(material_data['material_map'])
                cimages.extend(material_data['images'])

        elif command == 'usemtl':
            group.material = slugify(line)

        # TODO: display and render attributes
        # bevel
        # c_interp
        # d_interp
        # lod
        # shadow_obj
        # trace_obj
        # ctech
        # stech

        else:
            print('  MISSING LINE: %s %s' % (command, line))

    # done, append last group
    if not group.empty():
        groups.append(group)

    for material in list(material_map.values()):
        mesh.effects.append(material.effect)
        mesh.materials.append(material)
    for cimg in cimages:
        mesh.images.append(cimg)

    vertices = numpy.array(vertices, dtype=numpy.float32).reshape(-1, 3)
    normals = numpy.array(normals, dtype=numpy.float32).reshape(-1, 3)
    texcoords = numpy.array(texcoords, dtype=numpy.float32).reshape(-1, 2)

    sources = []
    # all modes have vertex source
    sources.append(
        collada.source.FloatSource("obj-vertex-source", vertices,
                                   ('X', 'Y', 'Z')))
    if len(normals) > 0:
        sources.append(
            collada.source.FloatSource("obj-normal-source", normals,
                                       ('X', 'Y', 'Z')))
    if len(texcoords) > 0:
        sources.append(
            collada.source.FloatSource("obj-uv-source", texcoords, ('S', 'T')))

    geom = collada.geometry.Geometry(mesh, geometry_name, geometry_name,
                                     sources)

    materials_mapped = set()
    for group in groups:
        input_list = collada.source.InputList()
        input_list.addInput(0, 'VERTEX', "#obj-vertex-source")
        if group.face_mode == FACEMODE.VN:
            input_list.addInput(1, 'NORMAL', '#obj-normal-source')
        elif group.face_mode == FACEMODE.VT:
            input_list.addInput(1, 'TEXCOORD', '#obj-uv-source')
        elif group.face_mode == FACEMODE.VTN:
            input_list.addInput(1, 'TEXCOORD', '#obj-uv-source')
            input_list.addInput(2, 'NORMAL', '#obj-normal-source')

        if len(group.face_lengths) > 0:
            face_lengths = numpy.array(group.face_lengths, dtype=numpy.int32)

            # First, join the individual face lines together, separated by spaces. Then,
            # just replace 1/2/3 and 1//3 with "1 2 3" and "1  3", as numpy.fromstring can
            # handle any whitespace it's given, similar to python's split(). Concatenating
            # together this way is much faster than parsing the numbers in python - let
            # numpy do it. Note that sep=" " is actually misleading - it handles tabs and
            # other whitespace also
            group.face_indices = (" ".join(group.face_indices)).replace(
                "/", " ")
            face_indices = numpy.fromstring(group.face_indices,
                                            dtype=numpy.int32,
                                            sep=" ")

            # obj indices start at 1, while collada start at 0
            face_indices -= 1

            polylist = geom.createPolylist(
                face_indices, face_lengths, input_list, group.material
                or namer.name("nullmaterial"))
            geom.primitives.append(polylist)

        if len(group.line_indices) > 0:
            group.line_indices = (" ".join(group.line_indices)).replace(
                "/", " ")
            line_indices = numpy.fromstring(group.line_indices,
                                            dtype=numpy.int32,
                                            sep=" ")
            line_indices -= 1
            lineset = geom.createLineSet(
                line_indices, input_list, group.material
                or namer.name("nullmaterial"))
            geom.primitives.append(lineset)

        if group.material in material_map:
            materials_mapped.add(group.material)

    mesh.geometries.append(geom)

    matnodes = []
    for matref in materials_mapped:
        matnode = collada.scene.MaterialNode(matref,
                                             material_map[matref],
                                             inputs=[('TEX0', 'TEXCOORD', '0')
                                                     ])
        matnodes.append(matnode)
    geomnode = collada.scene.GeometryNode(geom, matnodes)
    node = collada.scene.Node(namer.name("node"), children=[geomnode])
    myscene = collada.scene.Scene(namer.name("scene"), [node])
    mesh.scenes.append(myscene)
    mesh.scene = myscene

    return mesh
def loadOBJ(data, aux_file_loader=None, validate_output=False):
    """Loads an OBJ file
    
    :param data: A binary data string containing the OBJ file
    :param aux_file_loader: Should be a callable function that takes one parameter.
                            The parameter will be a string containing an auxiliary
                            file that needs to be found, in this case usually a .mtl
                            file or a texture file.
    
    :returns: An instance of :class:`collada.Collada` or None if could not be loaded
    """
    
    mesh = collada.Collada(validate_output=validate_output)
    namer = NameUniqifier()
    material_map = {}
    cimages = []
    materialNamer = NameUniqifier()
    
    vertices = []
    normals = []
    texcoords = []
    
    groups = []
    group = ObjGroup(namer.name("default"))
    geometry_name = namer.name("convertedobjgeometry")
    
    file_like = StringIO(to_unicode(data))
    for line in file_like:
        line = line.strip()
        
        # ignore blank lines and comments
        if len(line) == 0 or line.startswith('#'):
            continue
        
        # split off the first non-whitespace token and ignore the line if there isn't > 1 token
        splitup = line.split(None, 1)
        if len(splitup) != 2:
            continue
        command, line = splitup
        
        if command == 'v':
            line_tokens = line.split()
            vertices.extend(line_tokens[:3])
            
        elif command == 'vn':
            line_tokens = line.split()
            normals.extend(line_tokens[:3])
           
        elif command == 'vt':
            line_tokens = line.split()
            texcoords.extend(line_tokens[:2])
            
        # TODO: other vertex data statements
        # vp
        # cstype
        # deg
        # bmat
        # step
            
        elif command == 'f':
            faces = line.split()
            
            if group.face_mode == FACEMODE.UNKNOWN:
                group.face_mode = detectFaceStyle(faces[0])
                if group.face_mode is None:
                    sys.stderr.write("Error: could not detect face type for line '%s'" % line)
                    return
            
            group.face_lengths.append(len(faces))
            
            # Don't decode the faces here because the / separators have to be parsed out
            # and this is very slow to do one at a time. Instead, just append to a list
            # which is much faster than appending to a string, and it will get joined and
            # parsed later
            group.face_indices.append(line)
        
        elif command == 'l':
            faces = line.split()
            
            if group.face_mode == FACEMODE.UNKNOWN:
                group.face_mode = detectFaceStyle(faces[0])
                if group.face_mode is None:
                    sys.stderr.write("Error: could not detect face type for line '%s'" % line)
                    return
            
            # COLLADA defines lines as a pair of points, so the index values "1 2 3 4" would
            # refer to *two* lines, one between 1 and 2 and one between 3 and 4. OBJ defines
            # lines as continous, so it would be three lines: 1-2, 2-3, 3-4. This duplicates
            # the points to get pairs for COLLADA. This is not very efficient, but not sure
            # of a faster way to do this and I've never seen any files with a huge number of
            # lines in it anyway.
            line = faces[0] + " " + faces[1]
            prev = faces[1]
            for cur in faces[2:]:
                line += " " + prev + " " + cur
                prev = cur
            group.line_indices.append(line)
        
        elif command == 'p':
            faces = line.split()
            
            if group.face_mode == FACEMODE.UNKNOWN:
                group.face_mode = detectFaceStyle(faces[0])
                if group.face_mode is None:
                    sys.stderr.write("Error: could not detect face type for line '%s'" % line)
                    return
                
            # COLLADA does not have points, so this converts a point to a line with two
            # identical endpoints
            line = " ".join(f + " " + f for f in faces)
            group.line_indices.append(line)
        
        # TODO: other elements
        # curv
        # curv2
        # surf
        
        elif command == 'g':
            if group.empty():
                # first group without any previous data, so just set name
                group.name = namer.name(line)
                continue
            
            # end of previous group and start of new group
            groups.append(group)
            group = ObjGroup(namer.name(line))
        
        elif command == 's':
            # there is no way to map shading groups into collada
            continue
        
        elif command == 'o':
            geometry_name = namer.name(line)
        
        # TODO: grouping info
        # mg
        
        # TODO: Free-form curve/surface body statements
        # parm
        # trim
        # hole
        # scrv
        # sp
        # end
        # con
        
        elif command == 'mtllib':
            mtl_file = None
            if aux_file_loader is not None:
                mtl_file = aux_file_loader(line)
            if mtl_file is not None:
                material_data = loadMaterialLib(mtl_file, namer=materialNamer, aux_file_loader=aux_file_loader)
                material_map.update(material_data['material_map'])
                cimages.extend(material_data['images'])
            
        elif command == 'usemtl':
            group.material = slugify(line)
        
        # TODO: display and render attributes
        # bevel
        # c_interp
        # d_interp
        # lod
        # shadow_obj
        # trace_obj
        # ctech
        # stech
        
        else:
            print('  MISSING LINE: %s %s' % (command, line))
    
    # done, append last group
    if not group.empty():
        groups.append(group)
    
    for material in list(material_map.values()):
        mesh.effects.append(material.effect)
        mesh.materials.append(material)
    for cimg in cimages:
        mesh.images.append(cimg)
    
    vertices = numpy.array(vertices, dtype=numpy.float32).reshape(-1, 3)
    normals = numpy.array(normals, dtype=numpy.float32).reshape(-1, 3)
    texcoords = numpy.array(texcoords, dtype=numpy.float32).reshape(-1, 2)
    
    sources = []
    # all modes have vertex source
    sources.append(collada.source.FloatSource("obj-vertex-source", vertices, ('X', 'Y', 'Z')))
    if len(normals) > 0:
        sources.append(collada.source.FloatSource("obj-normal-source", normals, ('X', 'Y', 'Z')))
    if len(texcoords) > 0:
        sources.append(collada.source.FloatSource("obj-uv-source", texcoords, ('S', 'T')))
    
    geom = collada.geometry.Geometry(mesh, geometry_name, geometry_name, sources)
    
    materials_mapped = set()
    for group in groups:
        input_list = collada.source.InputList()
        input_list.addInput(0, 'VERTEX', "#obj-vertex-source")
        if group.face_mode == FACEMODE.VN:
            input_list.addInput(1, 'NORMAL', '#obj-normal-source')
        elif group.face_mode == FACEMODE.VT:
            input_list.addInput(1, 'TEXCOORD', '#obj-uv-source')
        elif group.face_mode == FACEMODE.VTN:
            input_list.addInput(1, 'TEXCOORD', '#obj-uv-source')
            input_list.addInput(2, 'NORMAL', '#obj-normal-source')
        
        if len(group.face_lengths) > 0:
            face_lengths = numpy.array(group.face_lengths, dtype=numpy.int32)
    
            # First, join the individual face lines together, separated by spaces. Then,        
            # just replace 1/2/3 and 1//3 with "1 2 3" and "1  3", as numpy.fromstring can
            # handle any whitespace it's given, similar to python's split(). Concatenating
            # together this way is much faster than parsing the numbers in python - let
            # numpy do it. Note that sep=" " is actually misleading - it handles tabs and
            # other whitespace also
            group.face_indices = (" ".join(group.face_indices)).replace("/", " ")
            face_indices = numpy.fromstring(group.face_indices, dtype=numpy.int32, sep=" ")
            
            # obj indices start at 1, while collada start at 0
            face_indices -= 1
            
            polylist = geom.createPolylist(face_indices, face_lengths, input_list, group.material or namer.name("nullmaterial"))
            geom.primitives.append(polylist)
            
        if len(group.line_indices) > 0:
            group.line_indices = (" ".join(group.line_indices)).replace("/", " ")
            line_indices = numpy.fromstring(group.line_indices, dtype=numpy.int32, sep=" ")
            line_indices -= 1
            lineset = geom.createLineSet(line_indices, input_list, group.material or namer.name("nullmaterial"))
            geom.primitives.append(lineset)
        
        if group.material in material_map:
            materials_mapped.add(group.material)
    
    mesh.geometries.append(geom)
    
    matnodes = []
    for matref in materials_mapped:
        matnode = collada.scene.MaterialNode(matref, material_map[matref], inputs=[('TEX0', 'TEXCOORD', '0')])
        matnodes.append(matnode)
    geomnode = collada.scene.GeometryNode(geom, matnodes)
    node = collada.scene.Node(namer.name("node"), children=[geomnode])
    myscene = collada.scene.Scene(namer.name("scene"), [node])
    mesh.scenes.append(myscene)
    mesh.scene = myscene
    
    return mesh
Exemple #3
0
def loadMaterialLib(data, namer, aux_file_loader=None):
    """Load an MTL file
    
    :param data: A binary string containing the mtl file
    :param namer: Should be an instance of :class:`ObjGroup`, used to generate unique
                  names for materials in the file, in case of duplicates or invalid
                  names containing funny characters (spaces, etc)
    :param aux_file_loader: Should be a callable function that takes one parameter.
                            The parameter will be a string containing an auxiliary
                            file that needs to be found, in this case usually a .mtl
                            file or a texture file.
    
    :returns: a `dict` containing 'material_map', 'images', and 'effects'
    """

    # maps MTL illumination types to collada shading types
    # note that 0,1,2,3 are mostly correct, but 4-10 have no
    # direct mapping to collada, so blinn is just a standin
    illumination_map = collections.defaultdict(lambda: 'blinn', **{
        0: 'constant',
        1: 'lambert'
    })

    cimages = []
    effects = []
    current_effect = collada.material.Effect(' empty ', [], 'blinn')

    file_like = StringIO(to_unicode(data))
    for line in file_like:
        line = line.strip()

        # ignore blank lines and comments
        if len(line) == 0 or line.startswith('#'):
            continue

        # split off the first non-whitespace token and ignore the line if there isn't > 1 token
        splitup = line.split(None, 1)
        if len(splitup) != 2:
            continue
        command, line = splitup

        if command == 'newmtl':
            if current_effect.id == ' empty ':
                current_effect.id = namer.name(line)
                continue

            effects.append(current_effect)
            current_effect = collada.material.Effect(namer.name(line), [],
                                                     'blinn')

        elif command == 'illum':
            illum_num = None
            try:
                illum_num = int(line)
            except ValueError:
                pass

            current_effect.shadingtype = illumination_map[illum_num]

        elif command == 'Kd':
            color = decode_mtl_color(line)
            if color is not None:
                current_effect.diffuse = color
        elif command == 'Ka':
            color = decode_mtl_color(line)
            if color is not None:
                current_effect.ambient = color
        elif command == 'Ke':
            color = decode_mtl_color(line)
            if color is not None:
                current_effect.emission = color
        elif command == 'Ks':
            color = decode_mtl_color(line)
            if color is not None:
                current_effect.specular = color

        elif command == 'd' or command == 'Tr':
            color = decode_mtl_single(line)
            if color is not None:
                current_effect.transparency = color
        elif command == 'Ns':
            color = decode_mtl_single(line)
            if color is not None:
                current_effect.shininess = color

        elif command == 'map_Kd':
            cimg, texmap = decode_mtl_texture(line, current_effect,
                                              aux_file_loader)
            if texmap is not None:
                current_effect.diffuse = texmap
                cimages.append(cimg)
        elif command == 'map_Ka':
            cimg, texmap = decode_mtl_texture(line, current_effect,
                                              aux_file_loader)
            if texmap is not None:
                current_effect.ambient = texmap
                cimages.append(cimg)
        elif command == 'map_Ks':
            cimg, texmap = decode_mtl_texture(line, current_effect,
                                              aux_file_loader)
            if texmap is not None:
                current_effect.specular = texmap
                cimages.append(cimg)
        elif command == 'map_bump' or command == 'bump':
            cimg, texmap = decode_mtl_texture(line, current_effect,
                                              aux_file_loader)
            if texmap is not None:
                current_effect.bumpmap = texmap
                cimages.append(cimg)

        else:
            print('MISSING MTL LINE', command, line)

    if current_effect.id != ' empty ':
        effects.append(current_effect)

    material_map = {}
    for effect in effects:
        material_id = namer.name(effect.id + '-material')
        material = collada.material.Material(material_id, material_id, effect)
        material_map[effect.id] = material

    return {
        'material_map': material_map,
        'images': cimages,
        'effects': effects
    }
def loadMaterialLib(data, namer, aux_file_loader=None):
    """Load an MTL file
    
    :param data: A binary string containing the mtl file
    :param namer: Should be an instance of :class:`ObjGroup`, used to generate unique
                  names for materials in the file, in case of duplicates or invalid
                  names containing funny characters (spaces, etc)
    :param aux_file_loader: Should be a callable function that takes one parameter.
                            The parameter will be a string containing an auxiliary
                            file that needs to be found, in this case usually a .mtl
                            file or a texture file.
    
    :returns: a `dict` containing 'material_map', 'images', and 'effects'
    """
    
    # maps MTL illumination types to collada shading types
    # note that 0,1,2,3 are mostly correct, but 4-10 have no
    # direct mapping to collada, so blinn is just a standin
    illumination_map = collections.defaultdict(lambda: 'blinn',
                                               **{0: 'constant',
                                                  1:'lambert'})
    
    cimages = []
    effects = []
    current_effect = collada.material.Effect(' empty ', [], 'blinn')
    
    file_like = StringIO(to_unicode(data))
    for line in file_like:
        line = line.strip()
        
        # ignore blank lines and comments
        if len(line) == 0 or line.startswith('#'):
            continue

        # split off the first non-whitespace token and ignore the line if there isn't > 1 token
        splitup = line.split(None, 1)
        if len(splitup) != 2:
            continue
        command, line = splitup
        
        if command == 'newmtl':
            if current_effect.id == ' empty ':
                current_effect.id = namer.name(line)
                continue
            
            effects.append(current_effect)
            current_effect = collada.material.Effect(namer.name(line),
                                                     [],
                                                     'blinn')
        
        elif command == 'illum':
            illum_num = None
            try:
                illum_num = int(line)
            except ValueError:
                pass
            
            current_effect.shadingtype = illumination_map[illum_num]
        
        elif command == 'Kd':
            color = decode_mtl_color(line)
            if color is not None:
                current_effect.diffuse = color
        elif command == 'Ka':
            color = decode_mtl_color(line)
            if color is not None:
                current_effect.ambient = color
        elif command == 'Ke':
            color = decode_mtl_color(line)
            if color is not None:
                current_effect.emission = color
        elif command == 'Ks':
            color = decode_mtl_color(line)
            if color is not None:
                current_effect.specular = color
        
        elif command == 'd' or command == 'Tr':
            color = decode_mtl_single(line)
            if color is not None:
                current_effect.transparency = color
        elif command == 'Ns':
            color = decode_mtl_single(line)
            if color is not None:
                current_effect.shininess = color
        
        elif command == 'map_Kd':
            cimg, texmap = decode_mtl_texture(line, current_effect, aux_file_loader)
            if texmap is not None:
                current_effect.diffuse = texmap
                cimages.append(cimg)
        elif command == 'map_Ka':
            cimg, texmap = decode_mtl_texture(line, current_effect, aux_file_loader)
            if texmap is not None:
                current_effect.ambient = texmap
                cimages.append(cimg)
        elif command == 'map_Ks':
            cimg, texmap = decode_mtl_texture(line, current_effect, aux_file_loader)
            if texmap is not None:
                current_effect.specular = texmap
                cimages.append(cimg)
        elif command == 'map_bump' or command == 'bump':
            cimg, texmap = decode_mtl_texture(line, current_effect, aux_file_loader)
            if texmap is not None:
                current_effect.bumpmap = texmap
                cimages.append(cimg)
        
        else:
            print('MISSING MTL LINE', command, line)
        
    if current_effect.id != ' empty ':
        effects.append(current_effect)
    
    material_map = {}
    for effect in effects:
        material_id = namer.name(effect.id + '-material')
        material = collada.material.Material(material_id, material_id, effect)
        material_map[effect.id] = material
    
    return {'material_map': material_map,
            'images': cimages,
            'effects': effects}