Disclaimer: The following is a stupid implementation of an .obj loader, do not use it in your codebase
import numpy as np
with open("monkey.obj", "r") as fobj:
lines = [line.strip() for line in fobj.readlines()]
vertices = []
vertex_normals = []
faces = []
faces_normals = []
# Extract vertices
for line in lines:
splitted = line.split(" ")
# Vertex
if splitted[0] == "v":
_, v1, v2, v3 = splitted
vertices.append([float(v1), float(v2), float(v3)])
# Normal
if splitted[0] == "vn":
_, v1, v2, v3 = splitted
vertex_normals.append([float(v1), float(v2), float(v3)])
# Extract faces
for line in lines:
splitted = line.split(" ")
# Face
if splitted[0] == "f":
# This file is triangulated, so this is fine
_, v1, v2, v3 = splitted
# It happens that this mesh is flat-shaded, so the normal is
# the same on all vertices
v1_index, _, v1_normal = v1.split("/")
v2_index, _, _ = v2.split("/")
v3_index, _, _ = v3.split("/")
faces.append([int(v1_index) - 1, int(v2_index) - 1, int(v3_index) - 1])
faces_normals.append(int(v1_normal) - 1)
vertices = np.array(vertices)
faces = np.array(faces)
vertex_normals = np.array(vertex_normals)
faces_normals = np.array(faces_normals)
vertices
faces
from ipycanvas import Canvas, hold_canvas
from py3d_engine import OrbitCamera, project_vector
class MonkeyCloud(Canvas):
def __init__(self):
super(MonkeyCloud, self).__init__(width=500, height=500)
self.dragging = False
self.x = vertices[:, 0]
self.y = vertices[:, 1]
self.z = vertices[:, 2]
self.dx = 0
self.dy = 0
self.radius = 10
self.camera = OrbitCamera(self.radius, [0, 0, 0], self.width / self.height)
self.x2, self.y2, self.z2 = project_vector(
self.x, self.y, self.z, self.camera.matrix
)
self.draw()
self.on_mouse_down(self.mouse_down_handler)
self.on_mouse_move(self.mouse_move_handler)
self.on_mouse_up(self.mouse_up_handler)
self.on_mouse_out(self.mouse_out_handler)
def update_matrix(self, dx=None, dy=None):
dx = dx if dx is not None else self.dx
dy = dy if dy is not None else self.dy
self.camera.update_position(dy, dx)
self.x2, self.y2, self.z2 = project_vector(
self.x, self.y, self.z, self.camera.matrix
)
self.draw()
def draw(self):
x = self.x2 * self.width + self.width / 2
y = self.y2 * self.height + self.height / 2
with hold_canvas():
self.clear()
self.fill_circles(x, y, 2)
def mouse_down_handler(self, pixel_x, pixel_y):
self.dragging = True
self.x_mouse = pixel_x
self.y_mouse = pixel_y
def mouse_move_handler(self, pixel_x, pixel_y):
if self.dragging:
self.dx_new = self.dx + pixel_x - self.x_mouse
self.dy_new = self.dy + pixel_y - self.y_mouse
self.update_matrix(self.dx_new, self.dy_new)
def mouse_up_handler(self, pixel_x, pixel_y):
if self.dragging:
self.dragging = False
self.dx = self.dx_new
self.dy = self.dy_new
def mouse_out_handler(self, pixel_x, pixel_y):
if self.dragging:
self.dragging = False
self.dx = self.dx_new
self.dy = self.dy_new
cloud = MonkeyCloud()
cloud
triangles = vertices[faces]
triangles_positions = np.mean(triangles, axis=1)
triangles_normals = vertex_normals[faces_normals]
light_direction = np.array([-1, 1, 0])
class Monkey(Canvas):
def __init__(self):
super(Monkey, self).__init__(width=500, height=500)
self.dragging = False
self.x = vertices[:, 0]
self.y = vertices[:, 1]
self.z = vertices[:, 2]
self.dx = 0
self.dy = 0
self.radius = 10
self.camera = OrbitCamera(self.radius, [0, 0, 0], self.width / self.height)
self.update_matrix()
self.on_mouse_down(self.mouse_down_handler)
self.on_mouse_move(self.mouse_move_handler)
self.on_mouse_up(self.mouse_up_handler)
self.on_mouse_out(self.mouse_out_handler)
def update_matrix(self, dx=None, dy=None):
dx = dx if dx is not None else self.dx
dy = dy if dy is not None else self.dy
self.camera.update_position(dy, dx)
dist = np.linalg.norm(self.camera.position - triangles_positions, axis=1)
# Face culling: Get rid of the triangles that are not facing the camera
triangles_facing_camera = np.dot(triangles_normals, self.camera.front) < 0
self.triangles = triangles[triangles_facing_camera]
self.triangles_normals = triangles_normals[triangles_facing_camera]
self.dist = dist[triangles_facing_camera]
# Face sorting: Sort triangle by depth (distance to camera) so we can draw further triangles first
self.order = np.flip(np.argsort(self.dist))
# Project triangles
triangle_vertices = self.triangles.reshape(
self.triangles.shape[0] * self.triangles.shape[1], 3
)
proj_x, proj_y, _ = project_vector(
triangle_vertices[:, 0],
triangle_vertices[:, 1],
triangle_vertices[:, 2],
self.camera.matrix,
)
proj_x = proj_x * self.width + self.width / 2
proj_y = proj_y * self.height + self.height / 2
self.proj_triangles = np.stack((proj_x, proj_y), axis=1).reshape(
self.triangles.shape[0], self.triangles.shape[1], 2
)
self.draw()
def draw(self):
triangles = []
colors = []
# Now let's draw triangles
for i in self.order:
triangle = np.array(self.proj_triangles[i])
# print(triangle)
normal = self.triangles_normals[i]
# Shading depending on light direction and face normal
light = np.dot(light_direction, normal)
if light < 0.4:
light = 0.4
elif light > 1:
light = 1
triangles.append(triangle)
colors.append([int(214 * light), int(224 * light), int(125 * light)])
with hold_canvas():
self.clear()
self.fill_styled_polygons(triangles, colors)
def mouse_down_handler(self, pixel_x, pixel_y):
self.dragging = True
self.x_mouse = pixel_x
self.y_mouse = pixel_y
def mouse_move_handler(self, pixel_x, pixel_y):
if self.dragging:
self.dx_new = self.dx + pixel_x - self.x_mouse
self.dy_new = self.dy + pixel_y - self.y_mouse
self.update_matrix(self.dx_new, self.dy_new)
def mouse_up_handler(self, pixel_x, pixel_y):
if self.dragging:
self.dragging = False
self.dx = self.dx_new
self.dy = self.dy_new
def mouse_out_handler(self, pixel_x, pixel_y):
if self.dragging:
self.dragging = False
self.dx = self.dx_new
self.dy = self.dy_new
monkey = Monkey()
monkey