class SoftwareRenderer(object): def __init__(self): self.bitmap = None # Inicializa el objeto bitmap def glInit(self): self.bitmap = Bitmap() # Sirve para definir el tamano de la imagen def glCreateWindow(self, width, height): self.bitmap.createWindow(width, height) # Sirve para definir el area en la que se desea dibujar def glViewPort(self, x, y, width, height): self.bitmap.setViewPort(x, y, width, height) # Sirve para definir el color con el que se limpia la ventana def glClearColor(self, r,g,b): r=int(r*255) g=int(g*255) b=int(b*255) self.bitmap.setClearColor(r,g,b) # Sirve para definir el color del vertex def glColor(self, r,g,b): r=int(r*255) g=int(g*255) b=int(b*255) self.bitmap.setVertexColor(r,g,b) # Sirve para definir la posicion de un punto def glVertex(self, x, y): newX = int(self.bitmap.viewPortX + ((self.bitmap.viewPortWidth)*((x+1)/2))) newY = int(self.bitmap.viewPortY + ((self.bitmap.viewPortHeigth)*((y+1)/2))) self.bitmap.drawPoint(newX, newY) # Sirve para definir la posicion absoluta de un punto def glVertexAbs(self, x, y): self.bitmap.drawPoint(x, y) # Sirve para limpiar toda la imagen con un color def glClear(self): self.bitmap.clearWindow() # Sirve para generar el archivo def glFinish(self, filename): self.bitmap.write(filename) # Sirve para dibujar una linea con posiciones absolutas def glLineAbsPos(self, x1, y1, x2, y2): # La distancia entre la posicion inicial y la final dy = abs(y2 - y1) dx = abs(x2 - x1) # Nos indica si el avance en y va a depender del avance en x o viceversa steep = dy > dx # Si la inclinacion es mayor a 45 grados entonces el avance en x dependera de y, se invierten las variables x y y if steep: tempX1 = x1 x1 = y1 y1 = tempX1 tempX2 = x2 x2 = y2 y2 = tempX2 # Si la linea va de derecha a izquierda se invierten las variables de x/y inicial y final para que siga funcionando el algoritmo if x1 > x2: tempX1 = x1 x1 = x2 x2 = tempX1 tempY1 = y1 y1 = y2 y2 = tempY1 # La distancia entre la posicion inicial y la final dy = abs(y2 - y1) dx = abs(x2 - x1) # El offset de la posicion y con respecto a la posicion inicial offset = 0 # El limite que debe sobrepasar la posicion y para que se avance un pixel en y. Lo iniciamos en dx ya que cada ciclo de x le sumaremos 2*dy al offset y lo compararemos con 2*dx. Es otra forma de hacer lo siguiente pero SIN introducir decimales: iniciar el offset en 0, cada ciclo sumarle dx y compararlo con 0.5dy. threshold = dx # La variable y empieza en la y inicial y = y1 # Se recorren todos los pixeles en x for x in range(x1, x2 + 1): # Si la linea tenia una pendiente mayor a 45 grados entonces se invirtieron las variables, por lo que se llama al comando con x y y invertidas if steep: self.bitmap.drawPoint(y, x) else: self.bitmap.drawPoint(x, y) # Se le suma al offset dos veces la diferencia de distancias en y offset += dy * 2 # Si el offset supera al threshold establecido entonces se mueve 1 en y if offset >= threshold: if y1 < y2: y += 1 else: y -= 1 # El treshold se ajusta para cuando toque moverse otro pixel en y sumandole dos veces dx threshold += dx * 2 # Sirve para dibujar una linea con posiciones relativas (-1,1) def glLine(self, x1, y1, x2, y2): x1 = int(self.bitmap.viewPortWidth * ((x1+1)/2)) y1 = int(self.bitmap.viewPortHeigth * ((y1+1)/2)) x2 = int(self.bitmap.viewPortWidth * ((x2+1)/2)) y2 = int(self.bitmap.viewPortHeigth * ((y2+1)/2)) self.glLineAbsPos(x1, y1, x2, y2) # Sirve para cargar archivos .obj solo lineas def glLoadObjWireFrame(self, filename, translateX, translateY, scaleX, scaleY): # Se instancia la clase Obj obj = Obj(filename) for face in obj.faces: # Se establece la cantidad de vertices en cada cara vertexCount = len(face) for i in range(vertexCount): # Para hacer una linea se toma el vertice que indica i y el siguiente despues de i f1 = face[i] - 1 nextVertex = i + 1 # Si i es el ultimo vertice se conectara con el primero if nextVertex >= vertexCount: nextVertex -= vertexCount f2 = face[nextVertex] - 1 # Se encuentran las coordenadas de los vertices de la linea v1 = obj.vertices[f1] v2 = obj.vertices[f2] # Se establecen los puntos inicial y final de la linea, se aplica la traslacion y la escala x1 = (v1[0] + translateX) * scaleX y1 = (v1[1] + translateY) * scaleY x2 = (v2[0] + translateX) * scaleX y2 = (v2[1] + translateY) * scaleY # Se dibuja la linea self.glLine(x1, y1, x2, y2) # Sirve para cargar archivos .obj con las caras pintadas def glLoadObjSolid(self, filename, filenameMTL, translateX, translateY, scaleX, scaleY): # Se instancia la clase Obj obj = Obj(filename) mtl = Mtl(filenameMTL) # Direccion de la luz light = V3(0,0,1) # Se hace un contador para saber en que cara vamos para despues asignar los materiales faceCount = 0 #Se crea el color default color = [1,1,1] for face in obj.faces: # Se obtienen las coordenadas de los tres vertices de cada cara x1 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[0]-1][0]+1)/2)) y1 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[0]-1][1]+1)/2)) z1 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[0]-1][2]+1)/2)) x2 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[1]-1][0]+1)/2)) y2 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[1]-1][1]+1)/2)) z2 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[1]-1][2]+1)/2)) x3 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[2]-1][0]+1)/2)) y3 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[2]-1][1]+1)/2)) z3 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[2]-1][2]+1)/2)) # Se colocan las coordenadas de los vertices en vectores v1 = V3(x1, y1, z1) v2 = V3(x2, y2, z2) v3 = V3(x3, y3, z3) # Se calcula la normal de la cara normal = unitary(cross(sub(v2, v1), sub(v3, v1))) # Se calcula el producto punto de la normal con un vector (0,0,1) para obtener el valor de intensidad de la luz intensity = abs(dot(normal, light)) # Para asignar el nuevo material en la lista miramos si ya pasamos el index que nos indica un cambio de material if(len(obj.materialIndex) > 0 and faceCount >= obj.materialIndex[0]): # Se busca el nombre del material en la lista de materiales y luego se obtiene el RGB de ese material color = mtl.materials[obj.materialNames[0]] # Se eliminan esos materiales de la lista para que la siguiente vez que toque asignar material se asigne el siguiente de la lista obj.materialNames.pop(0) obj.materialIndex.pop(0) # Se le suma uno al contador de las caras faceCount += 1 # Si la intensidad es negativa quiere decir que la cara da para el otro lado, por lo que no se debe dibujar if intensity < 0: continue #self.glColor((255-randint(0,255))/255, (255-randint(0,255))/255, (255-randint(0,255))/255) # Se configura el color de la cara self.glColor(color[0]*intensity, color[1]*intensity, color[2]*intensity) # Se dibuja la cara y se rellena self.glFillTriangleBarycentric(v1,v2,v3) def transform(self, vertex): # Se coloca el vector en una matriz para poder multiplicarlo con la funcion matrix_mult y se agrega una dimension mas para poder multiplicarlo con la matriz 4x4 vertexR4 = [ [vertex.x,0,0,0], [vertex.y,0,0,0], [vertex.z,0,0,0], [1,0,0,0] ] # Se hace la multiplicacion de la matriz con el vector transformed_vertex = matrix_mult(self.ViewPortMatrix,matrix_mult(self.ProjectionMatrix,matrix_mult(self.ViewMatrix,matrix_mult(self.ModelMatrix, vertexR4)))) # Se selecciona solamente el los valores de la matriz que contienen el resultado de la multiplicacion y se forma un vector de tres dimensiones transformed_vertex = [ int(transformed_vertex[0][0]/transformed_vertex[3][0]), int(transformed_vertex[1][0]/transformed_vertex[3][0]), int(transformed_vertex[2][0]/transformed_vertex[3][0]), ] return V3(*transformed_vertex) # Sirve para cargar archivos .obj con las caras pintadas def glLoadObjTexture(self, filename, filenameMTL, filenameT, translate, scale, rotate): self.LoadModelMatrix(translate,scale,rotate) self.LoadViewPortMatrix() # Se instancia la clase Obj obj = Obj(filename) if(filenameMTL): # Se instancia la clase Mtl mtl = Mtl(filenameMTL) else: mtl = None if(filenameT): # Se instancia la clase Texture texture = Texture(filenameT) else: texture = None # Direccion de la luz light = V3(0,0,1) # Se hace un contador para saber en que cara vamos para despues asignar los materiales faceCount = 0 #Se crea el color default color = [1,1,1] for face in obj.faces: # Se obtienen las coordenadas de los tres vertices de cada cara x1 = obj.vertices[face[0][0]-1][0] y1 = obj.vertices[face[0][0]-1][1] z1 = obj.vertices[face[0][0]-1][2] x2 = obj.vertices[face[1][0]-1][0] y2 = obj.vertices[face[1][0]-1][1] z2 = obj.vertices[face[1][0]-1][2] x3 = obj.vertices[face[2][0]-1][0] y3 = obj.vertices[face[2][0]-1][1] z3 = obj.vertices[face[2][0]-1][2] v1 = V3(x1, y1, z1) v2 = V3(x2, y2, z2) v3 = V3(x3, y3, z3) nx1 = obj.normals[face[0][2]-1][0] ny1 = obj.normals[face[0][2]-1][1] nz1 = obj.normals[face[0][2]-1][2] nx2 = obj.normals[face[1][2]-1][0] ny2 = obj.normals[face[1][2]-1][1] nz2 = obj.normals[face[1][2]-1][2] nx3 = obj.normals[face[2][2]-1][0] ny3 = obj.normals[face[2][2]-1][1] nz3 = obj.normals[face[2][2]-1][2] n1 = V3(nx1, ny1, nz1) n2 = V3(nx2, ny2, nz2) n3 = V3(nx3, ny3, nz3) # Se calcula la normal de la cara #normal = unitary(cross(sub(v2, v1), sub(v3, v1))) # Se calcula el producto punto de la normal con un vector (0,0,1) para obtener el valor de intensidad de la luz #intensity = dot(normal, light) # Si la intensidad es negativa quiere decir que la cara da para el otro lado, por lo que no se debe dibujar #if intensity < 0: # continue if(texture): # Se obtienen las coordenadas de los vertices de la textura xt1 = int(texture.width * obj.tVertices[face[0][1]-1][0]) - 1 yt1 = int(texture.width * obj.tVertices[face[0][1]-1][1]) - 1 xt2 = int(texture.width * obj.tVertices[face[1][1]-1][0]) - 1 yt2 = int(texture.width * obj.tVertices[face[1][1]-1][1]) - 1 xt3 = int(texture.width * obj.tVertices[face[2][1]-1][0]) - 1 yt3 = int(texture.width * obj.tVertices[face[2][1]-1][1]) - 1 # Se colocan las coordenadas de los vertices de textura en vectores vt1 = V3(xt1, yt1, 0) vt2 = V3(xt2, yt2, 0) vt3 = V3(xt3, yt3, 0) # Se dibuja la cara y se rellena self.glFillTriangleBarycentricTexture(v1,v2,v3, texture, texture_vertex=(vt1,vt2,vt3), intensity=intensity) # Se asignan colores de los materiales si no hay archivo de textura elif(mtl): # Para asignar el nuevo material en la lista miramos si ya pasamos el index que nos indica un cambio de material if(len(obj.materialIndex) > 0 and faceCount >= obj.materialIndex[0]): # Se busca el nombre del material en la lista de materiales y luego se obtiene el RGB de ese material color = mtl.materials[obj.materialNames[0]] # Se eliminan esos materiales de la lista para que la siguiente vez que toque asignar material se asigne el siguiente de la lista obj.materialNames.pop(0) obj.materialIndex.pop(0) # Se configura el color de la cara self.glFillTriangleBarycentric((v1,v2,v3), color, normal_vectors=(n1,n2,n3)) # Se asignan colores random si no hay archivo .mtl else: color = (0.973,0.376,0.051) #color = (0.878,0.788,0.667) self.glFillTriangleBarycentric((v1,v2,v3), color, normal_vectors=(n1,n2,n3)) # Se le suma uno al contador de las caras faceCount += 1 def Shader(self, base_color, obj_vertices, barycentric_coord, normal_vectors): A, B, C = obj_vertices w, v, u = barycentric_coord nA, nB, nC = normal_vectors x = A.x * w + B.x * u + C.x * v y = A.y * w + B.y * u + C.y * v z = A.z * w + B.z * u + C.z * v nx = nA.x * w + nB.x * u + nC.x * v ny = nA.y * w + nB.y * u + nC.y * v nz = nA.z * w + nB.z * u + nC.z * v vn = V3(nx, ny, nz) intensity = dot(vn, V3(0,0.1,1)) if intensity < 0: intensity = 0 elif intensity > 1: intensity = 1 base_color = (0.71,0.62,0.54) base_color = (base_color[0] + 0.2 * abs(y),base_color[1] + 0.2 * abs(y),base_color[2] + 0.2 * abs(y)) if y > 0.35: base_color = (base_color[0] + 1.2 * abs(y-0.35),base_color[1] + 1.2 * abs(y-0.35),base_color[2] + 1.2 * abs(y-0.35)) if y > 0.32 and y < 0.36: base_color = (base_color[0] + 3* abs(y-0.32),base_color[1] + 3 * abs(y-0.32),base_color[2] + 3 * abs(y-0.32)) if y > 0.36 and y < 0.4: base_color = (base_color[0] + 3* abs(y-0.4),base_color[1] + 3 * abs(y-0.4),base_color[2] + 3 * abs(y-0.4)) if y > 0.203 and y < 0.213: base_color = (base_color[0] - 11* abs(y-0.203),base_color[1] - 11 * abs(y-0.203),base_color[2] - 11 * abs(y-0.203)) if y > 0.213 and y < 0.223: base_color = (base_color[0] - 11* abs(y-0.223),base_color[1] - 11 * abs(y-0.223),base_color[2] - 11 * abs(y-0.223)) if y > 0.1 and y < 0.15: base_color = (base_color[0] + 2* abs(y-0.1),base_color[1] + 2 * abs(y-0.1),base_color[2] + 2 * abs(y-0.1)) if y > 0.15 and y < 0.2: base_color = (base_color[0] + 2* abs(y-0.2),base_color[1] + 2 * abs(y-0.2),base_color[2] + 2 * abs(y-0.2)) if y < 0.1 and y > -0.05: base_color = (base_color[0] + 1.5* abs(y-0.1),base_color[1] + 1.3 * abs(y-0.1),base_color[2] + 0.8 * abs(y-0.1)) if y < -0.05 and y > -0.2: base_color = (base_color[0] + 1.5* abs(y+0.2),base_color[1] + 1.3 * abs(y+0.2),base_color[2] + 0.8 * abs(y+0.2)) if base_color[0] > 1: base_color = (1, base_color[1], base_color[2]) if base_color[1] > 1: base_color = (base_color[0], 1, base_color[2]) if base_color[2] > 1: base_color = (base_color[0], base_color[1], 1) if y < -0.35: base_color = (base_color[0] + 1.2 * abs(y+0.35),base_color[1] + 1.2 * abs(y+0.35),base_color[2] + 1.2 * abs(y+0.35)) self.glColor( base_color[0] * intensity, base_color[1] * intensity, base_color[2] * intensity, ) def LookAt(self, eye, center, up): # Se obtiene el eje z de la direccion desde la camara hasta el centro de la escena z = unitary(sub(eye,center)) # Se obtiene el eje x de la direccion desde la camara hasta el centro de la escena x = unitary(cross(up,z)) # Se obtiene el eje y de la direccion desde la camara hasta el centro de la escena y = unitary(cross(z,x)) self.LoadViewMatrix(x, y, z, center) self.LoadProjectionMatrix(-1/length(sub(eye,center))) def LoadModelMatrix(self, translate, scale, rotate): # Se define la matriz de traslacion translateMatrix = [ [1,0,0,translate[0]], [0,1,0,translate[1]], [0,0,1,translate[2]], [0,0,0,1] ] # Se define la matriz de escala scaleMatrix = [ [scale[0],0,0,0], [0,scale[1],0,0], [0,0,scale[2],0], [0,0, 0, 1] ] # Se obtienen los tres ejes de la rotaci a = rotate[0] b = rotate[1] c = rotate[2] # Se define la matriz de rotacion en el eje x rotationMatrixX = [ [1,0,0,0], [0,cos(a),-sin(a),0], [0,sin(a),cos(a),0], [0,0,0,1] ] # Se define la matriz de rotacion en el eje y rotationMatrixY = [ [cos(b),0,sin(b),0], [0,1,0,0], [-sin(b),0,cos(b),0], [0,0,0,1] ] # Se define la matriz de rotacion en el eje z rotationMatrixZ = [ [cos(c),-sin(c),0,0], [sin(c),cos(c),0,0], [0,0,1,0], [0,0,0,1] ] # Se define la matriz de rotacion final rotationMatrix = matrix_mult(rotationMatrixX, matrix_mult(rotationMatrixY, rotationMatrixZ)) # Se define la matriz de transformacion del modelo self.ModelMatrix = matrix_mult(translateMatrix, matrix_mult(rotationMatrix, scaleMatrix)) def LoadViewMatrix(self, x, y, z, center): # Se definen las matrices de transformacion de la vista M = [ [x.x, x.y, x.z, 0], [y.x, y.y, y.z, 0], [z.x, z.y, z.z, 0], [0,0,0,1] ] O = [ [1,0,0,-center.x], [0,1,0,-center.y], [0,0,1,-center.z], [0,0,0,1] ] self.ViewMatrix = matrix_mult(M,O) def LoadProjectionMatrix(self, coeff): # Se define la matriz de proyeccion self.ProjectionMatrix = [ [1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,coeff,1] ] def LoadViewPortMatrix(self, x=0, y=0): # Se define la matriz de transformacion del view port self.ViewPortMatrix = [ [self.bitmap.width/2, 0, 0, x+self.bitmap.width/2], [0,self.bitmap.height/2, 0, y+self.bitmap.height/2], [0,0,128,128], [0,0,0,1] ] # Sirve para rellenar poligonos previamente dibujados def glFillPolygons(self): # Se establece un "color" para marcar las orillas, se usan numeros negativos para asegurarse que no se confunda con ningun otro color edgeColor = [-1, -1, -1] # Se convierten todas las orillas de los poligonos al color establecido arriba for y in range(self.bitmap.height): for x in range(self.bitmap.width): if self.bitmap.framebuffer[y][x] != self.bitmap.clearColor: self.bitmap.framebuffer[y][x] = edgeColor # Se recorren todos los pixeles de el archivo de abajo para arriba y de izquierda a derecha for y in range(self.bitmap.height): # Esta varible indica si el pixel esta adentro de algun poligono inside = False # Estas dos variables nos ayudan a determinar si se esta entrando o saliendo de un poligono edgeAbove = False edgeBelow = False for x in range(self.bitmap.width): # Si el pixel es del color de las orillas se inicia el proceso de verificacion para saber si se esta entrando o saliendo de un poligono if self.bitmap.framebuffer[y][x] == edgeColor: for a in range(-1,2): # Para determinar si se esta entrando o saliendo de un poligono se revisa que alguno de los tres pixeles adyacentes en la linea de arriba tambien sea una orilla if self.bitmap.framebuffer[y+1][x+a] == edgeColor: edgeAbove = True # Para determinar si se esta entrando o saliendo de un poligono se revisa que alguno de los tres pixeles adyacentes en la linea de abajo tambien sea una orilla if self.bitmap.framebuffer[y-1][x+a] == edgeColor: edgeBelow = True # Si las dos condiciones de arriba se cumplen quiere decir que se esta entrando o saliendo de un poligono if edgeBelow and edgeAbove: # Se invierte la variable que nos dice si esta adentro o afuera de un poligono inside = not inside edgeBelow = False edgeAbove = False else: # Si actualmente se esta en un pixel que no sea una orilla y se esta adentro de un poligono entonces se debe pintar el pixel if inside: self.bitmap.framebuffer[y][x] = self.bitmap.vertexColor # Por ultimo se repintan todas las orillas de los poligonos a su color original for y in range(self.bitmap.height): for x in range(self.bitmap.width): if self.bitmap.framebuffer[y][x] == edgeColor: self.bitmap.framebuffer[y][x] = self.bitmap.vertexColor # Sirve para definir la caja en la que se van a verificar las coordenadas barycentricas def boundingBox(self, A,B,C): x = sorted([A.x, B.x, C.x]) y = sorted([A.y, B.y, C.y]) return V2(x[0], y[0]), V2(x[2], y[2]) # Sirve para encontrar las coordenadas barycentricas de un triangulo def barycentric(self, A, B, C, P): cx, cy, cz = cross(V3(B.x - A.x, C.x - A.x, A.x - P.x), V3(B.y - A.y, C.y - A.y, A.y - P.y)) # Se contempla el caso en el que cz sea cero if cz == 0: w = -1 v = -1 u = -1 else: # Se obtienen las coordenadas barycentricas u = cx/cz v = cy/cz w = 1 - (u + v) return w, v, u # Se pintan los triangulos con ayuda de coordenadas barycentricas def glFillTriangleBarycentric(self, obj_vertices, color, normal_vectors): # A = self.transform(obj_vertices[0]) B = self.transform(obj_vertices[1]) C = self.transform(obj_vertices[2]) # Se calcula la caja en la que estara el triangulo bbox_min, bbox_max = self.boundingBox(A, B, C) # Para cada punto de la caja se calculara si se encuentra dentro del triangulo para saber si se pinta for x in range(bbox_min.x, bbox_max.x + 1): for y in range(bbox_min.y, bbox_max.y + 1): # Se obtienen las coordenadas barycentricas w, v, u = self.barycentric(A, B, C, V2(x, y)) # Si alguna de las coordenadas es menor a cero se salta este punto ya que no esta if w < 0 or v < 0 or u < 0: continue self.Shader(color, obj_vertices, (w,v,u), normal_vectors) # Se calcula el valor de z para saber donde va en el zbuffer z = A.z * w + B.z * v + C.z * u # Si la z de este punto de esta cara es mayor a la que ya estaba en esta posicion entonces se dibuja if x>0 and x<self.bitmap.width and y>0 and y<self.bitmap.height and z > self.bitmap.zbuffer[x][y]: self.bitmap.drawPoint(x,y) self.bitmap.zbuffer[x][y] = z # Se pintan los triangulos con ayuda de coordenadas barycentricas def glFillTriangleBarycentricTexture(self, obj_vertices, texture, texture_vertex, intensity): # A = self.transform(obj_vertices[0]) B = self.transform(obj_vertices[1]) C = self.transform(obj_vertices[2]) # Se calcula la caja en la que estara el triangulo bbox_min, bbox_max = self.boundingBox(A, B, C) # Para cada punto de la caja se calculara si se encuentra dentro del triangulo para saber si se pinta for x in range(bbox_min.x, bbox_max.x + 1): for y in range(bbox_min.y, bbox_max.y + 1): # Se obtienen las coordenadas barycentricas w, v, u = self.barycentric(A, B, C, V2(x, y)) # Si alguna de las coordenadas es menor a cero se salta este punto ya que no esta if w < 0 or v < 0 or u < 0: continue # Se calcula la x y y de la posicion en la imagen de textura tA, tB, tC = texture_vertex tx = tA.x * w + tB.x * v + tC.x * u ty = tA.y * w + tB.y * v + tC.y * u # Se obtiene el color de la textura y se calcula su intensidad color = texture.get_color_at_pos(tx, ty) color1 = (color[0] * intensity)/255.0 color2 = (color[1] * intensity)/255.0 color3 = (color[2] * intensity)/255.0 # Se cambia el color glColor(color1,color2,color3) # Se calcula el valor de z para saber donde va en el zbuffer z = A.z * w + B.z * v + C.z * u # Si la z de este punto de esta cara es mayor a la que ya estaba en esta posicion entonces se dibuja if x>0 and x<self.bitmap.width and y>0 and y<self.bitmap.height and z > self.bitmap.zbuffer[y][x]: self.bitmap.drawPoint(x,y) self.bitmap.zbuffer[y][x] = z
class SoftwareRenderer(object): def __init__(self): self.bitmap = None # Inicializa el objeto bitmap def glInit(self): self.bitmap = Bitmap() # Sirve para definir el tamaño d la imagen def glCreateWindow(self, width, height): self.bitmap.createWindow(width, height) # Sirve para definir el area en la que se desea dibujar def glViewPort(self, x, y, width, height): self.bitmap.setViewPort(x, y, width, height) # Sirve para definir el color con el que se limpia la ventana def glClearColor(self, r, g, b): r = int(r * 255) g = int(g * 255) b = int(b * 255) self.bitmap.setClearColor(r, g, b) # Sirve para definir el color del vertex def glColor(self, r, g, b): r = int(r * 255) g = int(g * 255) b = int(b * 255) self.bitmap.setVertexColor(r, g, b) # Sirve para definir la posición de un punto def glVertex(self, x, y): newX = int(self.bitmap.viewPortX + ((self.bitmap.viewPortWidth) * ((x + 1) / 2))) newY = int(self.bitmap.viewPortY + ((self.bitmap.viewPortHeigth) * ((y + 1) / 2))) self.bitmap.drawPoint(newX, newY) # Sirve para definir la posición absoluta de un punto def glVertexAbs(self, x, y): self.bitmap.drawPoint(x, y) # Sirve para limpiar toda la imagen con un color def glClear(self): self.bitmap.clearWindow() # Sirve para generar el archivo def glFinish(self, filename): self.bitmap.write(filename) # Sirve para dibujar una linea con posiciones absolutas def glLineAbsPos(self, x1, y1, x2, y2): # La distancia entre la posicion inicial y la final dy = abs(y2 - y1) dx = abs(x2 - x1) # Nos indica si el avance en y va a depender del avance en x o viceversa steep = dy > dx # Si la inclinacion es mayor a 45 grados entonces el avance en x dependera de y, se invierten las variables x y y if steep: tempX1 = x1 x1 = y1 y1 = tempX1 tempX2 = x2 x2 = y2 y2 = tempX2 # Si la linea va de derecha a izquierda se invierten las variables de x/y inicial y final para que siga funcionando el algoritmo if x1 > x2: tempX1 = x1 x1 = x2 x2 = tempX1 tempY1 = y1 y1 = y2 y2 = tempY1 # La distancia entre la posicion inicial y la final dy = abs(y2 - y1) dx = abs(x2 - x1) # El offset de la posicion y con respecto a la posicion inicial offset = 0 # El limite que debe sobrepasar la posicion y para que se avance un pixel en y. Lo iniciamos en dx ya que cada ciclo de x le sumaremos 2*dy al offset y lo compararemos con 2*dx. Es otra forma de hacer lo siguiente pero SIN introducir decimales: iniciar el offset en 0, cada ciclo sumarle dx y compararlo con 0.5dy. threshold = dx # La variable y empieza en la y inicial y = y1 # Se recorren todos los pixeles en x for x in range(x1, x2 + 1): # Si la linea tenia una pendiente mayor a 45 grados entonces se invirtieron las variables, por lo que se llama al comando con x y y invertidas if steep: self.bitmap.drawPoint(y, x) else: self.bitmap.drawPoint(x, y) # Se le suma al offset dos veces la diferencia de distancias en y offset += dy * 2 # Si el offset supera al threshold establecido entonces se mueve 1 en y if offset >= threshold: if y1 < y2: y += 1 else: y -= 1 # El treshold se ajusta para cuando toque moverse otro pixel en y sumandole dos veces dx threshold += dx * 2 # Sirve para dibujar una linea con posiciones relativas (-1,1) def glLine(self, x1, y1, x2, y2): x1 = int(self.bitmap.viewPortWidth * ((x1 + 1) / 2)) y1 = int(self.bitmap.viewPortHeigth * ((y1 + 1) / 2)) x2 = int(self.bitmap.viewPortWidth * ((x2 + 1) / 2)) y2 = int(self.bitmap.viewPortHeigth * ((y2 + 1) / 2)) self.glLineAbsPos(x1, y1, x2, y2) # Sirve para cargar archivos .obj solo lineas def glLoadObjWireFrame(self, filename, translateX, translateY, scaleX, scaleY): # Se instancia la clase Obj obj = Obj(filename) for face in obj.faces: # Se establece la cantidad de vertices en cada cara vertexCount = len(face) for i in range(vertexCount): # Para hacer una linea se toma el vertice que indica i y el siguiente despues de i f1 = face[i] - 1 nextVertex = i + 1 # Si i es el ultimo vertice se conectara con el primero if nextVertex >= vertexCount: nextVertex -= vertexCount f2 = face[nextVertex] - 1 # Se encuentran las coordenadas de los vertices de la linea v1 = obj.vertices[f1] v2 = obj.vertices[f2] # Se establecen los puntos inicial y final de la linea, se aplica la traslacion y la escala x1 = (v1[0] + translateX) * scaleX y1 = (v1[1] + translateY) * scaleY x2 = (v2[0] + translateX) * scaleX y2 = (v2[1] + translateY) * scaleY # Se dibuja la linea self.glLine(x1, y1, x2, y2) # Sirve para cargar archivos .obj con las caras pintadas def glLoadObjSolid(self, filename, filenameMTL, translateX, translateY, scaleX, scaleY): # Se instancia la clase Obj obj = Obj(filename) mtl = Mtl(filenameMTL) # Direccion de la luz light = V3(0, 0, 1) # Se hace un contador para saber en que cara vamos para despues asignar los materiales faceCount = 0 #Se crea el color default color = [1, 1, 1] for face in obj.faces: # Se obtienen las coordenadas de los tres vertices de cada cara x1 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[0] - 1][0] + 1) / 2)) y1 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[0] - 1][1] + 1) / 2)) z1 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[0] - 1][2] + 1) / 2)) x2 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[1] - 1][0] + 1) / 2)) y2 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[1] - 1][1] + 1) / 2)) z2 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[1] - 1][2] + 1) / 2)) x3 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[2] - 1][0] + 1) / 2)) y3 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[2] - 1][1] + 1) / 2)) z3 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[2] - 1][2] + 1) / 2)) # Se colocan las coordenadas de los vertices en vectores v1 = V3(x1, y1, z1) v2 = V3(x2, y2, z2) v3 = V3(x3, y3, z3) # Se calcula la normal de la cara normal = unitary(cross(sub(v2, v1), sub(v3, v1))) # Se calcula el producto punto de la normal con un vector (0,0,1) para obtener el valor de intensidad de la luz intensity = abs(dot(normal, light)) # Para asignar el nuevo material en la lista miramos si ya pasamos el index que nos indica un cambio de material if (len(obj.materialIndex) > 0 and faceCount >= obj.materialIndex[0]): # Se busca el nombre del material en la lista de materiales y luego se obtiene el RGB de ese material color = mtl.materials[obj.materialNames[0]] # Se eliminan esos materiales de la lista para que la siguiente vez que toque asignar material se asigne el siguiente de la lista obj.materialNames.pop(0) obj.materialIndex.pop(0) # Se le suma uno al contador de las caras faceCount += 1 # Si la intensidad es negativa quiere decir que la cara da para el otro lado, por lo que no se debe dibujar if intensity < 0: continue #self.glColor((255-randint(0,255))/255, (255-randint(0,255))/255, (255-randint(0,255))/255) # Se configura el color de la cara self.glColor(color[0] * intensity, color[1] * intensity, color[2] * intensity) # Se dibuja la cara y se rellena self.glFillTriangleBarycentric(v1, v2, v3) # Sirve para cargar archivos .obj con las caras pintadas def glLoadObjTexture(self, filename, filenameT, translateX, translateY, scaleX, scaleY): # Se instancia la clase Obj obj = Obj(filename) texture = Texture(filenameT) # Direccion de la luz light = V3(0, 0, 1) # Se hace un contador para saber en que cara vamos para despues asignar los materiales faceCount = 0 #Se crea el color default color = [1, 1, 1] for face in obj.faces: # Se obtienen las coordenadas de los tres vertices de cada cara x1 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[0][0] - 1][0] + 1) / 2)) y1 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[0][0] - 1][1] + 1) / 2)) z1 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[0][0] - 1][2] + 1) / 2)) x2 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[1][0] - 1][0] + 1) / 2)) y2 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[1][0] - 1][1] + 1) / 2)) z2 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[1][0] - 1][2] + 1) / 2)) x3 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[2][0] - 1][0] + 1) / 2)) y3 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[2][0] - 1][1] + 1) / 2)) z3 = int(self.bitmap.viewPortWidth * ((obj.vertices[face[2][0] - 1][2] + 1) / 2)) # Se colocan las coordenadas de los vertices en vectores v1 = V3(x1, y1, z1) v2 = V3(x2, y2, z2) v3 = V3(x3, y3, z3) # Se obtienen las coordenadas de los vertices de la textura xt1 = int(texture.width * obj.tVertices[face[0][1] - 1][0]) - 1 yt1 = int(texture.width * obj.tVertices[face[0][1] - 1][1]) - 1 xt2 = int(texture.width * obj.tVertices[face[1][1] - 1][0]) - 1 yt2 = int(texture.width * obj.tVertices[face[1][1] - 1][1]) - 1 xt3 = int(texture.width * obj.tVertices[face[2][1] - 1][0]) - 1 yt3 = int(texture.width * obj.tVertices[face[2][1] - 1][1]) - 1 # Se colocan las coordenadas de los vertices de textura en vectores vt1 = V3(xt1, yt1, 0) vt2 = V3(xt2, yt2, 0) vt3 = V3(xt3, yt3, 0) # Se calcula la normal de la cara normal = unitary(cross(sub(v2, v1), sub(v3, v1))) # Se calcula el producto punto de la normal con un vector (0,0,1) para obtener el valor de intensidad de la luz intensity = abs(dot(normal, light)) # Se le suma uno al contador de las caras faceCount += 1 # Si la intensidad es negativa quiere decir que la cara da para el otro lado, por lo que no se debe dibujar if intensity < 0: continue #self.glColor((255-randint(0,255))/255, (255-randint(0,255))/255, (255-randint(0,255))/255) # Se configura el color de la cara #self.glColor(color[0]*intensity, color[1]*intensity, color[2]*intensity) # Se dibuja la cara y se rellena self.glFillTriangleBarycentricTexture(v1, v2, v3, texture, texture_vertex=(vt1, vt2, vt3), intensity=intensity) # Sirve para rellenar poligonos previamente dibujados def glFillPolygons(self): # Se establece un "color" para marcar las orillas, se usan numeros negativos para asegurarse que no se confunda con ningun otro color edgeColor = [-1, -1, -1] # Se convierten todas las orillas de los poligonos al color establecido arriba for y in range(self.bitmap.height): for x in range(self.bitmap.width): if self.bitmap.framebuffer[y][x] != self.bitmap.clearColor: self.bitmap.framebuffer[y][x] = edgeColor # Se recorren todos los pixeles de el archivo de abajo para arriba y de izquierda a derecha for y in range(self.bitmap.height): # Esta varible indica si el pixel esta adentro de algun poligono inside = False # Estas dos variables nos ayudan a determinar si se esta entrando o saliendo de un poligono edgeAbove = False edgeBelow = False for x in range(self.bitmap.width): # Si el pixel es del color de las orillas se inicia el proceso de verificacion para saber si se esta entrando o saliendo de un poligono if self.bitmap.framebuffer[y][x] == edgeColor: for a in range(-1, 2): # Para determinar si se esta entrando o saliendo de un poligono se revisa que alguno de los tres pixeles adyacentes en la linea de arriba tambien sea una orilla if self.bitmap.framebuffer[y + 1][x + a] == edgeColor: edgeAbove = True # Para determinar si se esta entrando o saliendo de un poligono se revisa que alguno de los tres pixeles adyacentes en la linea de abajo tambien sea una orilla if self.bitmap.framebuffer[y - 1][x + a] == edgeColor: edgeBelow = True # Si las dos condiciones de arriba se cumplen quiere decir que se esta entrando o saliendo de un poligono if edgeBelow and edgeAbove: # Se invierte la variable que nos dice si esta adentro o afuera de un poligono inside = not inside edgeBelow = False edgeAbove = False else: # Si actualmente se esta en un pixel que no sea una orilla y se esta adentro de un poligono entonces se debe pintar el pixel if inside: self.bitmap.framebuffer[y][x] = self.bitmap.vertexColor # Por ultimo se repintan todas las orillas de los poligonos a su color original for y in range(self.bitmap.height): for x in range(self.bitmap.width): if self.bitmap.framebuffer[y][x] == edgeColor: self.bitmap.framebuffer[y][x] = self.bitmap.vertexColor # Sirve para definir la caja en la que se van a verificar las coordenadas barycentricas def boundingBox(self, A, B, C): x = sorted([A.x, B.x, C.x]) y = sorted([A.y, B.y, C.y]) return V2(x[0], y[0]), V2(x[2], y[2]) # Sirve para encontrar las coordenadas barycentricas de un triangulo def barycentric(self, A, B, C, P): cx, cy, cz = cross(V3(B.x - A.x, C.x - A.x, A.x - P.x), V3(B.y - A.y, C.y - A.y, A.y - P.y)) # Se contempla el caso en el que cz sea cero if cz == 0: w = -1 v = -1 u = -1 else: # Se obtienen las coordenadas barycentricas u = cx / cz v = cy / cz w = 1 - (u + v) return w, v, u # Se pintan los triangulos con ayuda de coordenadas barycentricas def glFillTriangleBarycentric(self, A, B, C): # Se calcula la caja en la que estara el triangulo bbox_min, bbox_max = self.boundingBox(A, B, C) # Para cada punto de la caja se calculara si se encuentra dentro del triangulo para saber si se pinta for x in range(bbox_min.x, bbox_max.x + 1): for y in range(bbox_min.y, bbox_max.y + 1): # Se obtienen las coordenadas barycentricas w, v, u = self.barycentric(A, B, C, V2(x, y)) # Si alguna de las coordenadas es menor a cero se salta este punto ya que no esta if w < 0 or v < 0 or u < 0: continue # Se calcula el valor de z para saber donde va en el zbuffer z = A.z * w + B.z * v + C.z * u # Si la z de este punto de esta cara es mayor a la que ya estaba en esta posicion entonces se dibuja if z > self.bitmap.zbuffer[x][y]: self.bitmap.drawPoint(x, y) self.bitmap.zbuffer[x][y] = z # Se pintan los triangulos con ayuda de coordenadas barycentricas def glFillTriangleBarycentricTexture(self, A, B, C, texture, texture_vertex, intensity): # Se calcula la caja en la que estara el triangulo bbox_min, bbox_max = self.boundingBox(A, B, C) # Para cada punto de la caja se calculara si se encuentra dentro del triangulo para saber si se pinta for x in range(bbox_min.x, bbox_max.x + 1): for y in range(bbox_min.y, bbox_max.y + 1): # Se obtienen las coordenadas barycentricas w, v, u = self.barycentric(A, B, C, V2(x, y)) # Si alguna de las coordenadas es menor a cero se salta este punto ya que no esta if w < 0 or v < 0 or u < 0: continue # Se calcula la x y y de la posicion en la imagen de textura tA, tB, tC = texture_vertex tx = tA.x * w + tB.x * v + tC.x * u ty = tA.y * w + tB.y * v + tC.y * u # Se obtiene el color de la textura y se calcula su intensidad color = texture.get_color_at_pos(tx, ty) color1 = (color[0] * intensity) / 255.0 color2 = (color[1] * intensity) / 255.0 color3 = (color[2] * intensity) / 255.0 # Se cambia el color glColor(color1, color2, color3) # Se calcula el valor de z para saber donde va en el zbuffer z = A.z * w + B.z * v + C.z * u # Si la z de este punto de esta cara es mayor a la que ya estaba en esta posicion entonces se dibuja if z > self.bitmap.zbuffer[x][y]: self.bitmap.drawPoint(x, y) self.bitmap.zbuffer[x][y] = z