.
This commit is contained in:
@ -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
|
||||
|
||||
|
@ -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)]
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
230
extensions/fablabchemnitz/papercraft_unfold/d3/model/mesh.py
Normal file
230
extensions/fablabchemnitz/papercraft_unfold/d3/model/mesh.py
Normal 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
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user