This commit is contained in:
2021-07-23 02:36:56 +02:00
commit 4d622c5291
4878 changed files with 1849508 additions and 0 deletions

View File

@ -0,0 +1,334 @@
from math import sqrt
from ..geometry import Vector
from .mesh import Material, MeshPart
Vertex = Vector
TexCoord = Vertex
Normal = Vertex
Color = Vertex
class FaceVertex:
"""Contains the information a vertex needs in a face
In contains the index of the vertex, the index of the texture coordinate
and the index of the normal. It is None if it is not available.
:param vertex: index of the vertex
:param tex_coord: index of the texture coordinate
:param normal: index of the normal
:param color: index of the color
"""
def __init__(self, vertex = None, tex_coord = None, normal = None, color = None):
"""Initializes a FaceVertex from its indices
"""
self.vertex = vertex
self.tex_coord = tex_coord
self.normal = normal
self.color = color
def from_array(self, arr):
"""Initializes a FaceVertex from an array
:param arr: can be an array of strings, the first value will be the
vertex index, the second will be the texture coordinate index, the
third will be the normal index, and the fourth will be the color index.
"""
self.vertex = int(arr[0]) if len(arr) > 0 else None
try:
self.tex_coord = int(arr[1]) if len(arr) > 1 else None
except:
self.tex_coord = None
try:
self.normal = int(arr[2]) if len(arr) > 2 else None
except:
self.normal = None
try:
self.color = int(arr[3]) if len(arr) > 3 else None
except:
self.color = None
return self
class Face:
"""Represents a face with 3 vertices
Faces with more than 3 vertices are not supported in this class. You should
split your face first and then create the number needed of instances of
this class.
"""
def __init__(self, a = None, b = None, c = None, material = None):
"""Initializes a Face with its three FaceVertex and its Material
:param a: first FaceVertex element
:param b: second FaceVertex element
:param c: third FaceVertex element
:param material: the material to use with this face
"""
self.a = a
self.b = b
self.c = c
self.material = material
# Expects array of array
def from_array(self, arr):
"""Initializes a Face with an array
:param arr: should be an array of array of objects. Each array will
represent a FaceVertex
"""
self.a = FaceVertex().from_array(arr[0])
self.b = FaceVertex().from_array(arr[1])
self.c = FaceVertex().from_array(arr[2])
return self
class ModelParser:
"""Represents a 3D model
"""
def __init__(self, up_conversion = None):
"""Initializes the model
:param up_conversion: couple of characters, can be y z or z y
"""
self.up_conversion = up_conversion
self.vertices = []
self.colors = []
self.normals = []
self.tex_coords = []
self.parts = []
self.materials = []
self.current_part = None
self.path = None
def init_textures(self):
"""Initializes the textures of the parts of the model
Basically, calls glGenTexture on each texture
"""
for part in self.parts:
part.init_texture()
def add_vertex(self, vertex):
"""Adds a vertex to the current model
Will also update its bounding box, and convert the up vector if
up_conversion was specified.
:param vertex: vertex to add to the model
"""
# Apply up_conversion to the vertex
new_vertex = vertex
if self.up_conversion is not None:
if self.up_conversion[0] == 'y' and self.up_conversion[1] == 'z':
new_vertex = Vector(vertex.y, vertex.z, vertex.x)
elif self.up_conversion[0] == 'z' and self.up_conversion[1] == 'y':
new_vertex = Vector(vertex.z, vertex.x, vertex.y)
self.vertices.append(new_vertex)
def add_tex_coord(self, tex_coord):
"""Adds a texture coordinate element to the current model
:param tex_coord: tex_coord to add to the model
"""
self.tex_coords.append(tex_coord)
def add_normal(self, normal):
"""Adds a normal element to the current model
:param normal: normal to add to the model
"""
self.normals.append(normal)
def add_color(self, color):
"""Adds a color element to the current model
:param color: color to add to the model
"""
self.colors.append(color)
def add_face(self, face):
"""Adds a face to the current model
If the face has a different material than the current material, it will
create a new mesh part and update the current material.
:param face: face to add to the model
"""
if self.current_part is None or (face.material != self.current_part.material and face.material is not None):
self.current_part = MeshPart(self)
self.current_part.material = face.material if face.material is not None else Material.DEFAULT_MATERIAL
self.parts.append(self.current_part)
self.current_part.add_face(face)
def parse_file(self, path, chunk_size = 512):
"""Sets the path of the model and parse bytes by chunk
:param path: path to the file to parse
:param chunk_size: the file will be read chunk by chunk, each chunk
having chunk_size bytes
"""
self.path = path
byte_counter = 0
with open(path, 'rb') as f:
while True:
bytes = f.read(chunk_size)
if bytes == b'':
return
self.parse_bytes(bytes, byte_counter)
byte_counter += chunk_size
def draw(self):
"""Draws each part of the model with OpenGL
"""
import OpenGL.GL as gl
for part in self.parts:
part.draw()
def generate_vbos(self):
"""Generates the VBOs of each part of the model
"""
for part in self.parts:
part.generate_vbos()
def generate_vertex_normals(self):
"""Generate the normals for each vertex of the model
A normal will be the average normal of the adjacent faces of a vertex.
"""
self.normals = [Normal() for i in self.vertices]
for part in self.parts:
for face in part.faces:
v1 = Vertex.from_points(self.vertices[face.a.vertex], self.vertices[face.b.vertex])
v2 = Vertex.from_points(self.vertices[face.a.vertex], self.vertices[face.c.vertex])
v1.normalize()
v2.normalize()
cross = Vertex.cross_product(v1, v2)
self.normals[face.a.vertex] += cross
self.normals[face.b.vertex] += cross
self.normals[face.c.vertex] += cross
for normal in self.normals:
normal.normalize()
for part in self.parts:
for face in part.faces:
face.a.normal = face.a.vertex
face.b.normal = face.b.vertex
face.c.normal = face.c.vertex
def generate_face_normals(self):
"""Generate the normals for each face of the model
A normal will be the normal of the face
"""
# Build array of faces
faces = sum(map(lambda x: x.faces, self.parts), [])
self.normals = [Normal()] * len(faces)
for (index, face) in enumerate(faces):
v1 = Vertex.from_points(self.vertices[face.a.vertex], self.vertices[face.b.vertex])
v2 = Vertex.from_points(self.vertices[face.a.vertex], self.vertices[face.c.vertex])
cross = Vertex.cross_product(v1, v2)
cross.normalize()
self.normals[index] = cross
face.a.normal = index
face.b.normal = index
face.c.normal = index
def get_material_index(self, material):
"""Finds the index of the given material
:param material: Material you want the index of
"""
return [i for (i,m) in enumerate(self.materials) if m.name == material.name][0]
class TextModelParser(ModelParser):
def parse_file(self, path):
"""Sets the path of the model and parse each line
:param path: path to the text file to parse
"""
self.path = path
with open(path) as f:
for line in f.readlines():
line = line.rstrip()
if line != '':
self.parse_line(line)
class BoundingBox:
"""Represents a bounding box of a 3D model
"""
def __init__(self):
"""Initializes the coordinates of the bounding box
"""
self.min_x = +float('inf')
self.min_y = +float('inf')
self.min_z = +float('inf')
self.max_x = -float('inf')
self.max_y = -float('inf')
self.max_z = -float('inf')
def add(self, vector):
"""Adds a vector to a bounding box
If the vector is outside the bounding box, the bounding box will be
enlarged, otherwise, nothing will happen.
:param vector: the vector that will enlarge the bounding box
"""
self.min_x = min(self.min_x, vector.x)
self.min_y = min(self.min_y, vector.y)
self.min_z = min(self.min_z, vector.z)
self.max_x = max(self.max_x, vector.x)
self.max_y = max(self.max_y, vector.y)
self.max_z = max(self.max_z, vector.z)
def __str__(self):
"""Returns a string that represents the bounding box
"""
return "[{},{}],[{},{}],[{},{}]".format(
self.min_x,
self.min_y,
self.min_z,
self.max_x,
self.max_y,
self.max_z)
def get_center(self):
"""Returns the center of the bounding box
"""
return Vertex(
(self.min_x + self.max_x) / 2,
(self.min_y + self.max_y) / 2,
(self.min_z + self.max_z) / 2)
def get_scale(self):
"""Returns the maximum edge of the bounding box
"""
return max(
abs(self.max_x - self.min_x),
abs(self.max_y - self.min_y),
abs(self.max_z - self.min_z))
class Exporter:
"""Represents an object that can export a model into a certain format
"""
def __init__(self, model):
"""Creates a exporter for the model
:param model: model to export
"""
self.model = model

View File

@ -0,0 +1,4 @@
from os.path import dirname, basename, isfile
import glob
modules = glob.glob(dirname(__file__)+"/*.py")
__all__ = [ basename(f)[:-3] for f in modules if isfile(f)]

View File

@ -0,0 +1,208 @@
from ..basemodel import TextModelParser, Exporter, Vertex, TexCoord, Normal, FaceVertex, Face
from ..mesh import Material, MeshPart
from functools import reduce
import os.path
import sys
def is_obj(filename):
"""Checks that the file is a .obj file
Only checks the extension of the file
:param filename: path to the file
"""
return filename[-4:] == '.obj'
class OBJParser(TextModelParser):
"""Parser that parses a .obj file
"""
def __init__(self, up_conversion = None):
super().__init__(up_conversion)
self.current_material = None
self.mtl = None
self.vertex_offset = 0
def parse_line(self, string):
"""Parses a line of .obj file
:param string: the line to parse
"""
if string == '':
return
split = string.split()
first = split[0]
split = split[1:]
if first == 'usemtl' and self.mtl is not None:
self.current_material = self.mtl[split[0]]
elif first == 'mtllib':
path = os.path.join(os.path.dirname(self.path), ' '.join(split[:]))
if os.path.isfile(path):
self.mtl = MTLParser(self)
self.mtl.parse_file(path)
else:
print('Warning : ' + path + ' not found ', file=sys.stderr)
elif first == 'v':
self.add_vertex(Vertex().from_array(split))
elif first == 'vn':
self.add_normal(Normal().from_array(split))
elif first == 'vt':
self.add_tex_coord(TexCoord().from_array(split))
elif first == 'f':
splits = list(map(lambda x: x.split('/'), split))
for i in range(len(splits)):
for j in range(len(splits[i])):
if splits[i][j] != '':
splits[i][j] = int(splits[i][j])
if splits[i][j] > 0:
splits[i][j] -= 1
else:
splits[i][j] = len(self.vertices) + splits[i][j]
# if Face3
if len(split) == 3:
face = Face().from_array(splits)
face.material = self.current_material
self.add_face(face)
# Face4 are well supported with the next stuff
# elif len(split) == 4:
# face = Face().from_array(splits[:3])
# face.material = self.current_material
# self.add_face(face)
# face = Face().from_array([splits[0], splits[2], splits[3]])
# face.material = self.current_material
# self.add_face(face)
else:
# Bweeee
# First, lets compute all the FaceVertex for each vertex
face_vertices = []
for face_vertex in splits[:]:
face_vertices.append(FaceVertex(*face_vertex))
# Then, we build the faces 0 i i+1 for each 1 <= i < len - 1
for i in range(1, len(face_vertices) - 1):
# Create face with barycenter, i and i + 1
face = Face(face_vertices[0], face_vertices[i], face_vertices[i+1])
face.material = self.current_material
self.add_face(face)
class MTLParser:
"""Parser that parses a .mtl material file
"""
def __init__(self, parent):
"""Creates a MTLParser bound to the OBJParser
:param parent: the OBJParser this MTLParser refers to
"""
self.parent = parent
self.current_mtl = None
def parse_line(self, string):
"""Parses a line of .mtl file
:param string: line to parse
"""
if string == '':
return
split = string.split()
first = split[0]
split = split[1:]
if first == 'newmtl':
self.current_mtl = Material(' '.join(split[:]))
self.parent.materials.append(self.current_mtl)
elif first == 'Ka':
self.current_mtl.Ka = Vertex().from_array(split)
elif first == 'Kd':
self.current_mtl.Kd = Vertex().from_array(split)
elif first == 'Ks':
self.current_mtl.Ks = Vertex().from_array(split)
elif first == 'map_Kd':
self.current_mtl.relative_path_to_texture = ' '.join(split)
self.current_mtl.absolute_path_to_texture = os.path.join(os.path.dirname(self.parent.path), ' '.join(split))
def parse_file(self, path):
with open(path) as f:
for line in f.readlines():
line = line.rstrip()
self.parse_line(line)
def __getitem__(self, key):
for material in self.parent.materials:
if material.name == key:
return material
class OBJExporter(Exporter):
"""Exporter to .obj format
"""
def __init__(self, model):
"""Creates an exporter from the model
:param model: Model to export
"""
super().__init__(model)
def __str__(self):
"""Exports the model
"""
current_material = ''
string = ""
for vertex in self.model.vertices:
string += "v " + ' '.join([str(vertex.x), str(vertex.y), str(vertex.z)]) + "\n"
string += "\n"
if len(self.model.tex_coords) > 0:
for tex_coord in self.model.tex_coords:
string += "vt " + ' '.join([str(tex_coord.x), str(tex_coord.y)]) + "\n"
string += "\n"
if len(self.model.normals) > 0:
for normal in self.model.normals:
string += "vn " + ' '.join([str(normal.x), str(normal.y), str(normal.z)]) + "\n"
string += "\n"
faces = sum(map(lambda x: x.faces, self.model.parts), [])
for face in faces:
if face.material is not None and face.material.name != current_material:
current_material = face.material.name
string += "usemtl " + current_material + "\n"
string += "f "
arr = []
for v in [face.a, face.b, face.c]:
sub_arr = []
sub_arr.append(str(v.vertex + 1))
if v.normal is None:
if v.tex_coord is not None:
sub_arr.append('')
sub_arr.append(str(v.tex_coord + 1))
elif v.tex_coord is not None:
sub_arr.append(str(v.tex_coord + 1))
if v.normal is not None:
sub_arr.append(str(v.normal + 1))
arr.append('/'.join(sub_arr))
string += ' '.join(arr) + '\n'
return string

View File

@ -0,0 +1,65 @@
from ..basemodel import TextModelParser, Exporter, Vertex, TexCoord, Normal, FaceVertex, Face
from ..mesh import Material, MeshPart
def is_off(filename):
"""Checks that the file is a .off file
Only checks the extension of the file
:param filename: path to the file
"""
return filename[-4:] == '.off'
class OFFParser(TextModelParser):
"""Parser that parses a .off file
"""
def __init__(self, up_conversion = None):
super().__init__(up_conversion)
self.vertex_number = None
self.face_number = None
self.edge_number = None
def parse_line(self, string):
"""Parses a line of .off file
:param string: the line to parse
"""
split = string.split()
if string == '' or string == 'OFF':
pass
elif self.vertex_number is None:
# The first will be the header
self.vertex_number = int(split[0])
self.face_number = int(split[1])
self.edge_number = int(split[2])
elif len(self.vertices) < self.vertex_number:
self.add_vertex(Vertex().from_array(split))
else:
self.add_face(Face(FaceVertex(int(split[1])), FaceVertex(int(split[2])), FaceVertex(int(split[3]))))
class OFFExporter(Exporter):
"""Exporter to .off format
"""
def __init__(self, model):
"""Creates an exporter from the model
:param model: Model to export
"""
super().__init__(model)
def __str__(self):
"""Exports the model
"""
faces = sum(map(lambda x: x.faces, self.model.parts), [])
string = "OFF\n{} {} {}".format(len(self.model.vertices), len(faces), 0) + '\n'
for vertex in self.model.vertices:
string += ' '.join([str(vertex.x), str(vertex.y), str(vertex.z)]) + '\n'
for face in faces:
string += '3 ' + ' '.join([str(face.a.vertex), str(face.b.vertex), str(face.c.vertex)]) + '\n'
return string

View File

@ -0,0 +1,494 @@
import os
import sys
import struct
from ..basemodel import ModelParser, TextModelParser, Exporter, Vertex, Face, Color, FaceVertex, TexCoord, Material
class UnkownTypeError(Exception):
def __init__(self, message):
self.message = message
def is_ply(filename):
"""Checks that the file is a .ply file
Only checks the extension of the file
:param filename: path to the file
"""
return filename[-4:] == '.ply'
# List won't work with this function
def _ply_type_size(type):
"""Returns the size of a ply property
:param type: a string that is in a ply element
"""
if type == 'char' or type == 'uchar':
return 1
elif type == 'short' or type == 'ushort':
return 2
elif type == 'int' or type == 'uint':
return 4
elif type == 'float':
return 4
elif type == 'double':
return 8
else:
raise UnkownTypeError('Type ' + type + ' is unknown')
def ply_type_size(type):
"""Returns the list containing the sizes of the elements
:param type: a string that is in a ply element
"""
split = type.split()
if len(split) == 1:
return [_ply_type_size(type)]
else:
if split[0] != 'list':
print('You have multiple types but it\'s not a list...', file=sys.stderr)
sys.exit(-1)
else:
return list(map(lambda a: _ply_type_size(a), split[1:]))
def bytes_to_element(type, bytes, byteorder = 'little'):
"""Returns a python object parsed from bytes
:param type: the type of the object to parse
:param bytes: the bytes to read
:param byteorder: little or big endian
"""
if type == 'char':
return ord(struct.unpack('<b', bytes)[0])
if type == 'uchar':
return ord(struct.unpack('<c', bytes)[0])
elif type == 'short':
return struct.unpack('<h', bytes)[0]
elif type == 'ushort':
return struct.unpack('<H', bytes)[0]
elif type == 'int':
return struct.unpack('<i', bytes)[0]
elif type == 'uint':
return struct.unpack('<I', bytes)[0]
elif type == 'float':
return struct.unpack('<f', bytes)[0]
elif type == 'double':
return struct.unpack('<d', bytes)[0]
else:
raise UnkownTypeError('Type ' + type + ' is unknown')
class PLYParser(ModelParser):
"""Parser that parses a .ply file
"""
def __init__(self, up_conversion = None):
super().__init__(up_conversion)
self.counter = 0
self.elements = []
self.inner_parser = PLYHeaderParser(self)
self.beginning_of_line = ''
self.header_finished = False
def parse_bytes(self, bytes, byte_counter):
"""Parses bytes of a .ply file
"""
if self.header_finished:
self.inner_parser.parse_bytes(self.beginning_of_line + bytes, byte_counter - len(self.beginning_of_line))
self.beginning_of_line = b''
return
# Build lines for header and use PLYHeaderParser
current_line = self.beginning_of_line
for (i, c) in enumerate(bytes):
char = chr(c)
if char == '\n':
self.inner_parser.parse_line(current_line)
if current_line == 'end_header':
self.header_finished = True
self.beginning_of_line = bytes[i+1:]
return
current_line = ''
else:
current_line += chr(c)
self.beginning_of_line = current_line
class PLYHeaderParser:
"""Parser that parses the header of a .ply file
"""
def __init__(self, parent):
self.current_element = None
self.parent = parent
self.content_parser = None
def parse_line(self, string):
split = string.split()
if string == 'ply':
return
elif split[0] == 'format':
if split[2] != '1.0':
print('Only format 1.0 is supported', file=sys.stderr)
sys.exit(-1)
if split[1] == 'ascii':
self.content_parser = PLY_ASCII_ContentParser(self.parent)
elif split[1] == 'binary_little_endian':
self.content_parser = PLYLittleEndianContentParser(self.parent)
elif split[1] == 'binary_big_endian':
self.content_parser = PLYBigEndianContentParser(self.parent)
else:
print('Only ascii, binary_little_endian and binary_big_endian are supported', \
file=sys.stderr)
sys.exit(-1)
elif split[0] == 'element':
self.current_element = PLYElement(split[1], int(split[2]))
self.parent.elements.append(self.current_element)
elif split[0] == 'property':
self.current_element.add_property(split[-1], ' '.join(split[1:-1]))
elif split[0] == 'end_header':
self.parent.inner_parser = self.content_parser
elif split[0] == 'comment' and split[1] == 'TextureFile':
material = Material('mat' + str(len(self.parent.materials)))
self.parent.materials.append(material)
material.relative_path_to_texture = split[2]
material.absolute_path_to_texture = os.path.join(os.path.dirname(self.parent.path), split[2])
class PLYElement:
def __init__(self, name, number):
self.name = name
self.number = number
self.properties = []
def add_property(self, name, type):
self.properties.append((name, type))
class PLY_ASCII_ContentParser:
def __init__(self, parent):
self.parent = parent
self.element_index = 0
self.counter = 0
self.current_element = None
self.beginning_of_line = ''
def parse_bytes(self, bytes, byte_counter):
current_line = self.beginning_of_line
for (i, c) in enumerate(bytes):
char = chr(c)
if char == '\n':
self.parse_line(current_line)
current_line = ''
else:
current_line += chr(c)
self.beginning_of_line = current_line
def parse_line(self, string):
if string == '':
return
if self.current_element is None:
self.current_element = self.parent.elements[0]
split = string.split()
color = None
if self.current_element.name == 'vertex':
vertex = Vertex()
red = None
blue = None
green = None
alpha = None
offset = 0
for property in self.current_element.properties:
if property[0] == 'x':
vertex.x = float(split[offset])
elif property[0] == 'y':
vertex.y = float(split[offset])
elif property[0] == 'z':
vertex.z = float(split[offset])
elif property[0] == 'red':
red = float(split[offset]) / 255
elif property[0] == 'green':
green = float(split[offset]) / 255
elif property[0] == 'blue':
blue = float(split[offset]) / 255
elif property[0] == 'alpha':
alpha = float(split[offset]) / 255
offset += 1
self.parent.add_vertex(vertex)
if red is not None:
color = Color(red, blue, green)
self.parent.add_color(color)
elif self.current_element.name == 'face':
faceVertexArray = []
current_material = None
# Analyse element
offset = 0
for property in self.current_element.properties:
if property[0] == 'vertex_indices':
for i in range(int(split[offset])):
faceVertexArray.append(FaceVertex(int(split[i+offset+1])))
offset += int(split[0]) + 1
elif property[0] == 'texcoord':
offset += 1
for i in range(3):
# Create corresponding tex_coords
tex_coord = TexCoord().from_array(split[offset:offset+2])
offset += 2
self.parent.add_tex_coord(tex_coord)
faceVertexArray[i].tex_coord = len(self.parent.tex_coords) - 1
elif property[0] == 'texnumber':
current_material = self.parent.materials[int(split[offset])]
offset += 1
face = Face(*faceVertexArray)
face.material = current_material
self.parent.add_face(face)
self.counter += 1
if self.counter == self.current_element.number:
self.next_element()
def next_element(self):
self.element_index += 1
if self.element_index < len(self.parent.elements):
self.current_element = self.parent.elements[self.element_index]
class PLYLittleEndianContentParser:
def __init__(self, parent):
self.parent = parent
self.previous_bytes = b''
self.element_index = 0
self.counter = 0
self.current_element = None
self.started = False
# Serves for debugging purposes
# self.current_byte = 0
def parse_bytes(self, bytes, byte_counter):
if not self.started:
# self.current_byte = byte_counter
self.started = True
if self.current_element is None:
self.current_element = self.parent.elements[0]
bytes = self.previous_bytes + bytes
current_byte_index = 0
while True:
property_values = []
beginning_byte_index = current_byte_index
for property in self.current_element.properties:
size = ply_type_size(property[1])
if current_byte_index + size[0] > len(bytes):
self.previous_bytes = bytes[beginning_byte_index:]
# self.current_byte -= len(self.previous_bytes)
return
if len(size) == 1:
size = size[0]
current_property_bytes = bytes[current_byte_index:current_byte_index+size]
property_values.append(bytes_to_element(property[1], current_property_bytes))
current_byte_index += size
# self.current_byte += size
elif len(size) == 2:
types = property[1].split()[1:]
current_property_bytes = bytes[current_byte_index:current_byte_index+size[0]]
number_of_elements = bytes_to_element(types[0], current_property_bytes)
current_byte_index += size[0]
# self.current_byte += size[0]
property_values.append([])
# Parse list
for i in range(number_of_elements):
if current_byte_index + size[1] > len(bytes):
self.previous_bytes = bytes[beginning_byte_index:]
# self.current_byte -= len(self.previous_bytes)
return
current_property_bytes = bytes[current_byte_index:current_byte_index+size[1]]
property_values[-1].append(bytes_to_element(types[1], current_property_bytes))
current_byte_index += size[1]
# self.current_byte += size[1]
else:
print('I have not idea what this means', file=sys.stderr)
# Add element
if self.current_element.name == 'vertex':
vertex = Vertex()
red = None
green = None
blue = None
alpha = None
offset = 0
for property in self.current_element.properties:
if property[0] == 'x':
vertex.x = property_values[offset]
elif property[0] == 'y':
vertex.y = property_values[offset]
elif property[0] == 'z':
vertex.z = property_values[offset]
elif property[0] == 'red':
red = property_values[offset] / 255
elif property[0] == 'green':
green = property_values[offset] / 255
elif property[0] == 'blue':
blue = property_values[offset] / 255
elif property[0] == 'alpha':
alpha = property_values[offset] / 255
offset += 1
self.parent.add_vertex(vertex)
if red is not None:
self.parent.add_color(Color(red, blue, green))
elif self.current_element.name == 'face':
vertex_indices = []
tex_coords = []
material = None
for (i, property) in enumerate(self.current_element.properties):
if property[0] == 'vertex_indices':
vertex_indices.append(property_values[i][0])
vertex_indices.append(property_values[i][1])
vertex_indices.append(property_values[i][2])
elif property[0] == 'texcoord':
# Create texture coords
for j in range(0,6,2):
tex_coord = TexCoord(*property_values[i][j:j+2])
tex_coords.append(tex_coord)
elif property[0] == 'texnumber':
material = self.parent.materials[property_values[i]]
for tex_coord in tex_coords:
self.parent.add_tex_coord(tex_coord)
face = Face(*list(map(lambda x: FaceVertex(x), vertex_indices)))
counter = 3
if len(tex_coords) > 0:
for face_vertex in [face.a, face.b, face.c]:
face_vertex.tex_coord = len(self.parent.tex_coords) - counter
counter -= 1
if material is None and len(self.parent.materials) == 1:
material = self.parent.materials[0]
face.material = material
self.parent.add_face(face)
self.counter += 1
if self.counter == self.current_element.number:
self.next_element()
def next_element(self):
self.counter = 0
self.element_index += 1
if self.element_index < len(self.parent.elements):
self.current_element = self.parent.elements[self.element_index]
class PLYBigEndianContentParser(PLYLittleEndianContentParser):
def __init__(self, parent):
super().__init__(self, parent)
def parse_bytes(self, bytes):
# Reverse bytes, and then
super().parse_bytes(self, bytes)
class PLYExporter(Exporter):
def __init__(self, model):
super().__init__(model)
def __str__(self):
faces = sum([part.faces for part in self.model.parts], [])
# Header
string = "ply\nformat ascii 1.0\ncomment Automatically gnerated by model-converter\n"
for material in self.model.materials:
string += "comment TextureFile " + (material.relative_path_to_texture or 'None') + "\n"
# Types : vertices
string += "element vertex " + str(len(self.model.vertices)) +"\n"
string += "property float x\nproperty float y\nproperty float z\n"
# Types : faces
string += "element face " + str(len(faces)) + "\n"
string += "property list uchar int vertex_indices\n"
if len(self.model.tex_coords) > 0:
string += "property list uchar float texcoord\n"
string += "property int texnumber\n"
# End header
string += "end_header\n"
# Content of the model
for vertex in self.model.vertices:
string += str(vertex.x) + " " + str(vertex.y) + " " + str(vertex.z) + "\n"
for face in faces:
string += "3 " + str(face.a.vertex) + " " + str(face.b.vertex) + " " + str(face.c.vertex)
if len(self.model.tex_coords) > 0:
string += " 6 " \
+ str(self.model.tex_coords[face.a.tex_coord].x) + " " \
+ str(self.model.tex_coords[face.a.tex_coord].y) + " " \
+ str(self.model.tex_coords[face.b.tex_coord].x) + " " \
+ str(self.model.tex_coords[face.b.tex_coord].y) + " " \
+ str(self.model.tex_coords[face.c.tex_coord].x) + " " \
+ str(self.model.tex_coords[face.c.tex_coord].y) + " " \
+ str(self.model.get_material_index(face.material))
string += "\n"
return string

View File

@ -0,0 +1,116 @@
from ..basemodel import TextModelParser, Exporter, Vertex, FaceVertex, Face
from ..mesh import MeshPart
import os.path
def is_stl(filename):
"""Checks that the file is a .stl file
Only checks the extension of the file
:param filename: path to the file
"""
return filename[-4:] == '.stl'
class STLParser(TextModelParser):
"""Parser that parses a .stl file
"""
def __init__(self, up_conversion = None):
super().__init__(up_conversion)
self.parsing_solid = False
self.parsing_face = False
self.parsing_loop = False
self.current_face = None
self.face_vertices = None
def parse_line(self, string):
"""Parses a line of .stl file
:param string: the line to parse
"""
if string == '':
return
split = string.split()
if split[0] == 'solid':
self.parsing_solid = True
return
if split[0] == 'endsolid':
self.parsing_solid = False
return
if self.parsing_solid:
if split[0] == 'facet' and split[1] == 'normal':
self.parsing_face = True
self.face_vertices = [FaceVertex(), FaceVertex(), FaceVertex()]
self.current_face = Face(*self.face_vertices)
return
if self.parsing_face:
if split[0] == 'outer' and split[1] == 'loop':
self.parsing_loop = True
return
if split[0] == 'endloop':
self.parsing_loop = False
return
if self.parsing_loop:
if split[0] == 'vertex':
current_vertex = Vertex().from_array(split[1:])
self.add_vertex(current_vertex)
self.face_vertices[0].vertex = len(self.vertices) - 1
self.face_vertices.pop(0)
return
if split[0] == 'endfacet':
self.parsing_face = False
self.add_face(self.current_face)
self.current_face = None
self.face_vertices = None
class STLExporter(Exporter):
"""Exporter to .stl format
"""
def __init__(self, model):
"""Creates an exporter from the model
:param model: Model to export
"""
super().__init__(model)
super().__init__(model)
def __str__(self):
"""Exports the model
"""
string = 'solid {}\n'.format(os.path.basename(self.model.path[:-4]))
self.model.generate_face_normals()
faces = sum(map(lambda x: x.faces, self.model.parts), [])
for face in faces:
n = self.model.normals[face.a.normal]
v1 = self.model.vertices[face.a.vertex]
v2 = self.model.vertices[face.b.vertex]
v3 = self.model.vertices[face.c.vertex]
string += "facet normal {} {} {}\n".format(n.x, n.y, n.z)
string += "\touter loop\n"
string += "\t\tvertex {} {} {}\n".format(v1.x, v1.y, v1.z)
string += "\t\tvertex {} {} {}\n".format(v2.x, v2.y, v2.z)
string += "\t\tvertex {} {} {}\n".format(v3.x, v3.y, v3.z)
string += "\tendloop\n"
string += "endfacet\n"
string += 'endsolid {}'.format(os.path.basename(self.model.path[:-4]))
return string

View File

@ -0,0 +1,230 @@
class Material:
"""Represents a material
It contains its constants and its texturess. It is also usable with OpenGL
"""
def __init__(self, name):
""" Creates an empty material
:param name: name of the material:
"""
self.name = name
self.Ka = None
self.Kd = None
self.Ks = None
self.relative_path_to_texture = None
self.absolute_path_to_texture = None
self.im = None
self.id = None
def init_texture(self):
""" Initializes the OpenGL texture of the current material
To be simple, calls glGenTextures and stores the given id
"""
import OpenGL.GL as gl
# Already initialized
if self.id is not None:
return
# If no map_Kd, nothing to do
if self.im is None:
if self.absolute_path_to_texture is None:
return
try:
import PIL.Image
self.im = PIL.Image.open(self.absolute_path_to_texture)
except ImportError:
return
try:
ix, iy, image = self.im.size[0], self.im.size[1], self.im.tobytes("raw", "RGBA", 0, -1)
except:
ix, iy, image = self.im.size[0], self.im.size[1], self.im.tobytes("raw", "RGBX", 0, -1)
self.id = gl.glGenTextures(1)
gl.glBindTexture(gl.GL_TEXTURE_2D, self.id)
gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT,1)
gl.glTexImage2D(
gl.GL_TEXTURE_2D, 0, 3, ix, iy, 0,
gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, image
)
def bind(self):
"""Binds the material to OpenGL
"""
from OpenGL import GL as gl
gl.glEnable(gl.GL_TEXTURE_2D)
gl.glTexParameterf(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
gl.glTexParameterf(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
gl.glTexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_DECAL)
gl.glBindTexture(gl.GL_TEXTURE_2D, self.id)
def unbind(self):
"""Disables the GL_TEXTURE_2D flag of OpenGL
"""
from OpenGL import GL as gl
gl.glDisable(gl.GL_TEXTURE_2D)
Material.DEFAULT_MATERIAL=Material('')
"""Material that is used when no material is specified
"""
Material.DEFAULT_MATERIAL.Ka = 1.0
Material.DEFAULT_MATERIAL.Kd = 0.0
Material.DEFAULT_MATERIAL.Ks = 0.0
try:
import PIL.Image
Material.DEFAULT_MATERIAL.im = PIL.Image.new("RGBA", (1,1), "white")
except ImportError:
pass
class MeshPart:
"""A part of a 3D model that is bound to a single material
"""
def __init__(self, parent):
"""Creates a mesh part
:param parent: the global model with all the information
"""
self.parent = parent
self.material = None
self.vertex_vbo = None
self.tex_coord_vbo = None
self.normal_vbo = None
self.color_vbo = None
self.faces = []
def init_texture(self):
"""Initializes the material of the current parent
"""
if self.material is not None:
self.material.init_texture()
def add_face(self, face):
"""Adds a face to this MeshPart
:param face: face to add
"""
self.faces.append(face)
def generate_vbos(self):
"""Generates the vbo for this MeshPart
Creates the arrays that are necessary for smooth rendering
"""
from OpenGL.arrays import vbo
from numpy import array
# Build VBO
v = []
n = []
t = []
c = []
for face in self.faces:
v1 = self.parent.vertices[face.a.vertex]
v2 = self.parent.vertices[face.b.vertex]
v3 = self.parent.vertices[face.c.vertex]
v += [[v1.x, v1.y, v1.z], [v2.x, v2.y, v2.z], [v3.x, v3.y, v3.z]]
if face.a.normal is not None:
n1 = self.parent.normals[face.a.normal]
n2 = self.parent.normals[face.b.normal]
n3 = self.parent.normals[face.c.normal]
n += [[n1.x, n1.y, n1.z], [n2.x, n2.y, n2.z], [n3.x, n3.y, n3.z]]
if face.a.tex_coord is not None:
t1 = self.parent.tex_coords[face.a.tex_coord]
t2 = self.parent.tex_coords[face.b.tex_coord]
t3 = self.parent.tex_coords[face.c.tex_coord]
t += [[t1.x, t1.y], [t2.x, t2.y], [t3.x, t3.y]]
if len(self.parent.colors) > 0: # face.a.color is not None:
c1 = self.parent.colors[face.a.vertex]
c2 = self.parent.colors[face.b.vertex]
c3 = self.parent.colors[face.c.vertex]
c += [[c1.x, c1.y, c1.z], [c2.x, c2.y, c2.z], [c3.x, c3.y, c3.z]]
self.vertex_vbo = vbo.VBO(array(v, 'f'))
if len(n) > 0:
self.normal_vbo = vbo.VBO(array(n, 'f'))
if len(t) > 0:
self.tex_coord_vbo = vbo.VBO(array(t, 'f'))
if len(c) > 0:
self.color_vbo = vbo.VBO(array(c, 'f'))
def draw(self):
"""Draws the current MeshPart
Binds the material, and draws the model
"""
if self.material is not None:
self.material.bind()
if self.vertex_vbo is not None:
self.draw_from_vbos()
else:
self.draw_from_arrays()
if self.material is not None:
self.material.unbind()
def draw_from_vbos(self):
"""Simply calls the OpenGL drawArrays function
Sets the correct vertex arrays and draws the part
"""
import OpenGL.GL as gl
self.vertex_vbo.bind()
gl.glEnableClientState(gl.GL_VERTEX_ARRAY);
gl.glVertexPointerf(self.vertex_vbo)
self.vertex_vbo.unbind()
if self.normal_vbo is not None:
self.normal_vbo.bind()
gl.glEnableClientState(gl.GL_NORMAL_ARRAY)
gl.glNormalPointerf(self.normal_vbo)
self.normal_vbo.unbind()
if self.tex_coord_vbo is not None:
if self.material is not None:
self.material.bind()
self.tex_coord_vbo.bind()
gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY)
gl.glTexCoordPointerf(self.tex_coord_vbo)
self.tex_coord_vbo.unbind()
if self.color_vbo is not None:
self.color_vbo.bind()
gl.glEnableClientState(gl.GL_COLOR_ARRAY)
gl.glColorPointerf(self.color_vbo)
self.color_vbo.unbind()
gl.glDrawArrays(gl.GL_TRIANGLES, 0, len(self.vertex_vbo.data) * 9)
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
gl.glDisableClientState(gl.GL_NORMAL_ARRAY)
gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY)
gl.glDisableClientState(gl.GL_COLOR_ARRAY)
def draw_from_arrays(self):
pass

View File

@ -0,0 +1,99 @@
import os
from importlib import import_module
from . import formats
from .formats import *
from .basemodel import ModelParser, Exporter
from types import ModuleType
supported_formats = []
class ModelType:
"""Represents a type of coding of 3D object, and the module enabling
parsing and exporting
"""
def __init__(self, typename, inner_module):
"""Creates a ModelType
:param typename: the name of the 3D format
:param inner_module: the module that will parse and export the format
"""
self.typename = typename
self.inner_module = inner_module
def test_type(self, file):
"""Tests if a file has the correct type
:param file: path to the file to test
"""
return getattr(self.inner_module, 'is_' + self.typename)(file)
def create_parser(self, *args, **kwargs):
"""Creates a parser of the current type
"""
return getattr(self.inner_module, self.typename.upper() + 'Parser')(*args, **kwargs)
def create_exporter(self, *args, **kwargs):
"""Creates an exporter of the current type
"""
return getattr(self.inner_module, self.typename.upper() + 'Exporter')(*args, **kwargs)
def find_type(filename, supported_formats):
"""Find the correct type from a filename
:param filename: path to the file
:param supported_formats: list of formats that we have modules for
"""
for type in supported_formats:
if type.test_type(filename):
return type
for name in formats.__dict__:
if isinstance(formats.__dict__[name], ModuleType) and name != 'glob':
type = ModelType(name, formats.__dict__[name])
supported_formats.append(type)
def load_model(path, up_conversion = None):
"""Loads a model from a path
:param path: path to the file to load
:param up_conversion: conversion of up vectors
"""
parser = None
type = find_type(path, supported_formats)
if type is None:
raise Exception("File format not supported \"" + str(type) + "\"")
parser = type.create_parser(up_conversion)
parser.parse_file(path)
return parser
def export_model(model, path):
"""Exports a model to a path
:param model: model to export
:param path: path to save the model
"""
exporter = None
type = find_type(path, supported_formats)
if type is None:
raise Exception('File format is not supported')
exporter = type.create_exporter(model)
return exporter
def convert(input, output, up_conversion = None):
"""Converts a model
:param input: path of the input model
:param output: path to the output
:param up_conversion: convert the up vector
"""
model = load_model(input, up_conversion)
exporter = export_model(model, output)
return str(exporter)