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