model-converter-python/d3/model/basemodel.py

346 lines
11 KiB
Python
Raw Normal View History

2016-11-22 15:13:35 +01:00
from math import sqrt
2016-11-25 14:28:39 +01:00
from ..geometry import Vector
from .mesh import Material, MeshPart
2016-11-25 14:28:39 +01:00
Vertex = Vector
2016-11-21 15:59:03 +01:00
TexCoord = Vertex
Normal = Vertex
Color = Vertex
2016-11-21 15:59:03 +01:00
class FaceVertex:
2016-11-30 12:11:31 +01:00
"""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.
2017-01-18 14:49:37 +01:00
: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
2016-11-30 12:11:31 +01:00
"""
def __init__(self, vertex = None, tex_coord = None, normal = None, color = None):
2016-11-30 12:11:31 +01:00
"""Initializes a FaceVertex from its indices
"""
2016-11-21 15:59:03 +01:00
self.vertex = vertex
2016-11-29 12:13:43 +01:00
self.tex_coord = tex_coord
2016-11-21 15:59:03 +01:00
self.normal = normal
self.color = color
2016-11-21 15:59:03 +01:00
def from_array(self, arr):
2016-11-30 12:11:31 +01:00
"""Initializes a FaceVertex from an array
2017-01-18 14:49:37 +01:00
: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.
2016-11-30 12:11:31 +01:00
"""
self.vertex = int(arr[0]) if len(arr) > 0 else None
2016-11-23 16:36:32 +01:00
try:
2016-11-29 12:13:43 +01:00
self.tex_coord = int(arr[1]) if len(arr) > 1 else None
2016-11-23 16:36:32 +01:00
except:
2016-11-29 12:13:43 +01:00
self.tex_coord = None
2016-11-23 16:36:32 +01:00
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
2016-11-21 15:59:03 +01:00
return self
class Face:
2016-11-30 12:11:31 +01:00
"""Represents a face with 3 vertices
2016-12-02 16:04:05 +01:00
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.
2016-11-30 12:11:31 +01:00
"""
2016-11-29 12:13:43 +01:00
def __init__(self, a = None, b = None, c = None, material = None):
2016-11-30 12:11:31 +01:00
"""Initializes a Face with its three FaceVertex and its Material
2017-01-18 14:49:37 +01:00
:param a: first FaceVertex element
:param b: second FaceVertex element
:param c: third FaceVertex element
:param material: the material to use with this face
2016-11-30 12:11:31 +01:00
"""
2016-11-21 15:59:03 +01:00
self.a = a
self.b = b
self.c = c
2016-11-29 12:13:43 +01:00
self.material = material
2016-11-21 15:59:03 +01:00
# Expects array of array
def from_array(self, arr):
2016-11-30 12:11:31 +01:00
"""Initializes a Face with an array
2017-01-18 14:49:37 +01:00
:param arr: should be an array of array of objects. Each array will
represent a FaceVertex
2016-11-30 12:11:31 +01:00
"""
2016-11-21 15:59:03 +01:00
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:
2016-11-30 12:11:31 +01:00
"""Represents a 3D model
"""
2016-11-30 15:37:58 +01:00
def __init__(self, up_conversion = None):
2016-11-30 12:11:31 +01:00
"""Initializes the model
2017-01-18 14:49:37 +01:00
:param up_conversion: couple of characters, can be y z or z y
2016-11-30 12:11:31 +01:00
"""
2016-11-30 15:37:58 +01:00
self.up_conversion = up_conversion
2016-11-21 15:59:03 +01:00
self.vertices = []
self.colors = []
2016-11-21 15:59:03 +01:00
self.normals = []
self.tex_coords = []
self.parts = []
2017-01-19 11:23:48 +01:00
self.materials = []
self.current_part = None
2016-11-22 12:09:54 +01:00
self.bounding_box = BoundingBox()
self.center_and_scale = True
2016-11-29 12:05:03 +01:00
self.path = None
2016-11-21 15:59:03 +01:00
def init_textures(self):
2016-11-30 12:11:31 +01:00
"""Initializes the textures of the parts of the model
Basically, calls glGenTexture on each texture
"""
for part in self.parts:
part.init_texture()
2016-11-21 15:59:03 +01:00
def add_vertex(self, vertex):
2016-11-30 12:11:31 +01:00
"""Adds a vertex to the current model
2016-12-02 16:01:42 +01:00
Will also update its bounding box, and convert the up vector if
up_conversion was specified.
2017-01-18 14:49:37 +01:00
:param vertex: vertex to add to the model
2016-11-30 12:11:31 +01:00
"""
2016-11-30 15:37:58 +01:00
# 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)
self.bounding_box.add(new_vertex)
2016-11-21 15:59:03 +01:00
def add_tex_coord(self, tex_coord):
2016-11-30 12:11:31 +01:00
"""Adds a texture coordinate element to the current model
2017-01-18 14:49:37 +01:00
:param tex_coord: tex_coord to add to the model
2016-11-30 12:11:31 +01:00
"""
2016-11-21 15:59:03 +01:00
self.tex_coords.append(tex_coord)
def add_normal(self, normal):
2016-11-30 12:11:31 +01:00
"""Adds a normal element to the current model
2017-01-18 14:49:37 +01:00
:param normal: normal to add to the model
2016-11-30 12:11:31 +01:00
"""
2016-11-21 15:59:03 +01:00
self.normals.append(normal)
def add_color(self, color):
"""Adds a color element to the current model
2017-01-18 14:49:37 +01:00
:param color: color to add to the model
"""
self.colors.append(color)
2016-11-21 16:18:49 +01:00
def add_face(self, face):
2016-11-30 12:11:31 +01:00
"""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.
2017-01-18 14:49:37 +01:00
:param face: face to add to the model
2016-11-30 12:11:31 +01:00
"""
2016-11-30 14:57:58 +01:00
if self.current_part is None or (face.material != self.current_part.material and face.material is not None):
self.current_part = MeshPart(self)
2016-11-30 10:08:37 +01:00
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)
2016-11-21 15:59:03 +01:00
2017-01-17 17:16:10 +01:00
def parse_file(self, path, chunk_size = 512):
"""Sets the path of the model and parse bytes by chunk
2017-01-18 14:49:37 +01:00
: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
2017-01-17 17:16:10 +01:00
"""
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
2016-11-21 15:59:03 +01:00
2016-11-25 17:08:57 +01:00
def draw(self):
2016-11-30 12:11:31 +01:00
"""Draws each part of the model with OpenGL
"""
import OpenGL.GL as gl
2016-11-22 12:09:54 +01:00
if self.center_and_scale:
center = self.bounding_box.get_center()
scale = self.bounding_box.get_scale() / 2
gl.glPushMatrix()
gl.glScalef(1/scale, 1/scale, 1/scale)
gl.glTranslatef(-center.x, -center.y, -center.z)
for part in self.parts:
part.draw()
2016-11-22 12:09:54 +01:00
if self.center_and_scale:
gl.glPopMatrix()
def generate_vbos(self):
2016-11-30 12:11:31 +01:00
"""Generates the VBOs of each part of the model
"""
for part in self.parts:
part.generate_vbos()
2016-11-23 12:07:47 +01:00
2016-11-22 15:13:35 +01:00
def generate_vertex_normals(self):
2016-11-30 12:11:31 +01:00
"""Generate the normals for each vertex of the model
2016-11-30 12:11:31 +01:00
A normal will be the average normal of the adjacent faces of a vertex.
"""
2016-11-22 15:13:35 +01:00
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])
cross = Vertex.cross_product(v1, v2)
self.normals[face.a.vertex] += cross
self.normals[face.b.vertex] += cross
self.normals[face.c.vertex] += cross
2016-11-22 15:13:35 +01:00
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
2016-11-22 15:13:35 +01:00
def generate_face_normals(self):
2016-11-30 12:11:31 +01:00
"""Generate the normals for each face of the model
2016-11-22 15:13:35 +01:00
2016-11-30 12:11:31 +01:00
A normal will be the normal of the face
"""
2016-11-30 10:08:37 +01:00
# Build array of faces
faces = sum(map(lambda x: x.faces, self.parts), [])
self.normals = [Normal()] * len(faces)
2016-11-22 15:13:35 +01:00
2016-11-30 10:08:37 +01:00
for (index, face) in enumerate(faces):
2016-11-22 15:13:35 +01:00
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
2017-01-19 11:23:48 +01:00
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]
2017-01-17 10:47:57 +01:00
class TextModelParser(ModelParser):
def parse_file(self, path):
"""Sets the path of the model and parse each line
2017-01-18 14:49:37 +01:00
:param path: path to the text file to parse
2017-01-17 10:47:57 +01:00
"""
self.path = path
with open(path) as f:
for line in f.readlines():
line = line.rstrip()
if line != '':
self.parse_line(line)
2016-11-22 15:13:35 +01:00
2016-11-22 12:09:54 +01:00
class BoundingBox:
2016-11-30 12:11:31 +01:00
"""Represents a bounding box of a 3D model
"""
2016-11-22 12:09:54 +01:00
def __init__(self):
2016-11-30 12:11:31 +01:00
"""Initializes the coordinates of the bounding box
"""
2016-11-22 12:09:54 +01:00
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')
2016-11-30 12:11:31 +01:00
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.
2017-01-18 14:49:37 +01:00
:param vector: the vector that will enlarge the bounding box
2016-11-30 12:11:31 +01:00
"""
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)
2016-11-22 12:09:54 +01:00
2016-11-30 12:11:31 +01:00
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)
2016-11-22 12:09:54 +01:00
def __str__(self):
2016-11-30 12:11:31 +01:00
"""Returns a string that represents the bounding box
"""
2016-11-22 12:09:54 +01:00
return "[{},{}],[{},{}],[{},{}]".format(
self.min_x,
self.min_y,
self.min_z,
self.max_x,
self.max_y,
self.max_z)
def get_center(self):
2016-11-30 12:11:31 +01:00
"""Returns the center of the bounding box
"""
2016-11-22 12:09:54 +01:00
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):
2016-11-30 12:11:31 +01:00
"""Returns the maximum edge of the bounding box
"""
2016-11-22 12:09:54 +01:00
return max(
abs(self.max_x - self.min_x),
abs(self.max_y - self.min_y),
abs(self.max_z - self.min_z))
2016-11-21 15:59:03 +01:00
class Exporter:
2016-11-30 12:11:31 +01:00
"""Represents an object that can export a model into a certain format
"""
2016-11-21 15:59:03 +01:00
def __init__(self, model):
2017-01-18 14:49:37 +01:00
"""Creates a exporter for the model
:param model: model to export
"""
2016-11-21 15:59:03 +01:00
self.model = model
2016-11-22 11:27:42 +01:00