Skip to content

Commit 02acf7b

Browse files
authored
Merge pull request #59 from davidbrochart/plot3d
Add 3D plot example
2 parents 657333e + 3b6fd31 commit 02acf7b

File tree

3 files changed

+229
-0
lines changed

3 files changed

+229
-0
lines changed

examples/plot3d.ipynb

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"import numpy as np\n",
10+
"from ipycanvas import Canvas, hold_canvas"
11+
]
12+
},
13+
{
14+
"cell_type": "code",
15+
"execution_count": null,
16+
"metadata": {},
17+
"outputs": [],
18+
"source": [
19+
"from ipywidgets import FloatSlider"
20+
]
21+
},
22+
{
23+
"cell_type": "markdown",
24+
"metadata": {},
25+
"source": [
26+
"This module is local to the Notebook"
27+
]
28+
},
29+
{
30+
"cell_type": "code",
31+
"execution_count": null,
32+
"metadata": {},
33+
"outputs": [],
34+
"source": [
35+
"from py3d_engine import get_orbit_projection, project_vector"
36+
]
37+
},
38+
{
39+
"cell_type": "code",
40+
"execution_count": null,
41+
"metadata": {},
42+
"outputs": [],
43+
"source": [
44+
"class Plot3d(Canvas):\n",
45+
" def __init__(self):\n",
46+
" super(Plot3d, self).__init__(size=(500, 500))\n",
47+
" \n",
48+
" self.width = 500\n",
49+
" self.height = 500\n",
50+
"\n",
51+
" self.dragging = False\n",
52+
" self.n = 200\n",
53+
" self.x = np.random.rand(self.n) - 0.5\n",
54+
" self.y = np.random.rand(self.n) - 0.5\n",
55+
" self.z = np.random.rand(self.n) - 0.5\n",
56+
"\n",
57+
" self.radius = 5\n",
58+
" self.dx = 0\n",
59+
" self.dy = 0\n",
60+
" self.update_matrix()\n",
61+
"\n",
62+
" self.on_mouse_down(self.mouse_down_handler)\n",
63+
" self.on_mouse_move(self.mouse_move_handler)\n",
64+
" self.on_mouse_up(self.mouse_up_handler)\n",
65+
" self.on_mouse_out(self.mouse_out_handler)\n",
66+
"\n",
67+
" def update_matrix(self, dx=None, dy=None, radius=None):\n",
68+
" dx = dx if dx is not None else self.dx\n",
69+
" dy = dy if dy is not None else self.dy\n",
70+
" self.radius = radius if radius is not None else self.radius\n",
71+
"\n",
72+
" self.matrix = get_orbit_projection(\n",
73+
" dy, dx, self.radius, \n",
74+
" aspect=self.width / self.height\n",
75+
" )\n",
76+
" self.x2, self.y2, self.z2 = project_vector(self.x, self.y, self.z, self.matrix)\n",
77+
" self.draw()\n",
78+
"\n",
79+
" def draw(self):\n",
80+
" x = self.x2 * self.width + self.width / 2\n",
81+
" y = self.y2 * self.height + self.height / 2\n",
82+
" size = 10 * (1 - self.z2) + 1\n",
83+
" with hold_canvas(self):\n",
84+
" self.clear()\n",
85+
" self.fill_circles(x, y, size)\n",
86+
"\n",
87+
" def mouse_down_handler(self, pixel_x, pixel_y):\n",
88+
" self.dragging = True\n",
89+
" self.x_mouse = pixel_x\n",
90+
" self.y_mouse = pixel_y\n",
91+
"\n",
92+
" def mouse_move_handler(self, pixel_x, pixel_y):\n",
93+
" if self.dragging:\n",
94+
" self.dx_new = self.dx + pixel_x - self.x_mouse\n",
95+
" self.dy_new = self.dy + pixel_y - self.y_mouse\n",
96+
" \n",
97+
" self.update_matrix(- self.dx_new, self.dy_new)\n",
98+
" \n",
99+
" def mouse_up_handler(self, pixel_x, pixel_y):\n",
100+
" if self.dragging:\n",
101+
" self.dragging = False\n",
102+
" self.dx = self.dx_new\n",
103+
" self.dy = self.dy_new\n",
104+
" \n",
105+
" def mouse_out_handler(self, pixel_x, pixel_y):\n",
106+
" if self.dragging:\n",
107+
" self.dragging = False\n",
108+
" self.dx = self.dx_new\n",
109+
" self.dy = self.dy_new"
110+
]
111+
},
112+
{
113+
"cell_type": "code",
114+
"execution_count": null,
115+
"metadata": {
116+
"scrolled": false
117+
},
118+
"outputs": [],
119+
"source": [
120+
"p = Plot3d()\n",
121+
"p"
122+
]
123+
},
124+
{
125+
"cell_type": "code",
126+
"execution_count": null,
127+
"metadata": {},
128+
"outputs": [],
129+
"source": [
130+
"# Link Camera position to a slider widget\n",
131+
"slider = FloatSlider(description='Radius:', min=1., max=7., value=p.radius)\n",
132+
"\n",
133+
"def on_slider_move(change):\n",
134+
" slider_value = change['new']\n",
135+
"\n",
136+
" p.update_matrix(radius=slider_value)\n",
137+
"\n",
138+
"slider.observe(on_slider_move, 'value')\n",
139+
"\n",
140+
"slider"
141+
]
142+
}
143+
],
144+
"metadata": {
145+
"kernelspec": {
146+
"display_name": "Python 3",
147+
"language": "python",
148+
"name": "python3"
149+
},
150+
"language_info": {
151+
"codemirror_mode": {
152+
"name": "ipython",
153+
"version": 3
154+
},
155+
"file_extension": ".py",
156+
"mimetype": "text/x-python",
157+
"name": "python",
158+
"nbconvert_exporter": "python",
159+
"pygments_lexer": "ipython3",
160+
"version": "3.9.0"
161+
}
162+
},
163+
"nbformat": 4,
164+
"nbformat_minor": 2
165+
}

examples/py3d_engine/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .py3d_engine import *
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import numpy as np
2+
from math import pi, cos, sin, tan
3+
4+
5+
def pad_ones(x, y, z):
6+
return np.array([x, y, z, np.ones_like(x)])
7+
8+
9+
def normalize(vec):
10+
return vec / np.linalg.norm(vec)
11+
12+
13+
def project_vector(x, y, z, matrix):
14+
vec = np.dot(matrix, pad_ones(x, y, z))
15+
16+
return vec[0]/vec[3], vec[1]/vec[3], vec[2]/vec[3]
17+
18+
19+
def get_look_at_matrix(eye, center, up):
20+
n = normalize(eye - center)
21+
u = normalize(np.cross(up, n))
22+
v = np.cross(n, u)
23+
24+
matrix_r = [[u[0], u[1], u[2], 0],
25+
[v[0], v[1], v[2], 0],
26+
[n[0], n[1], n[2], 0],
27+
[0, 0, 0, 1]]
28+
29+
matrix_t = [[1, 0, 0, -eye[0]],
30+
[0, 1, 0, -eye[1]],
31+
[0, 0, 1, -eye[2]],
32+
[0, 0, 0, 1]]
33+
34+
return np.dot(matrix_r, matrix_t)
35+
36+
37+
def get_perspective_matrix(fovy, aspect, near, far):
38+
f = 1. / tan(fovy * pi / 360.)
39+
40+
return np.array([
41+
[f/aspect, 0, 0, 0],
42+
[ 0, f, 0, 0],
43+
[ 0, 0, (near + far)/(near - far), 2 * near * far/(near - far)],
44+
[ 0, 0, -1, 0]
45+
])
46+
47+
48+
def get_orbit_projection(elev, azim, radius, center=[0, 0, 0], aspect=1., near=0.5, far=5.):
49+
relev, razim = np.pi * elev/180, np.pi * azim/180
50+
51+
xp = center[0] + cos(razim) * cos(relev) * radius
52+
yp = center[1] + sin(razim) * cos(relev) * radius
53+
zp = center[2] + sin(relev) * radius
54+
eye = np.array((xp, yp, zp))
55+
56+
if abs(relev) > pi / 2.:
57+
up = np.array((0, 0, -1))
58+
else:
59+
up = np.array((0, 0, 1))
60+
61+
view_matrix = get_look_at_matrix(eye, center, up)
62+
projection_matrix = get_perspective_matrix(50, aspect, 0.5, 2 * radius)
63+
return np.dot(projection_matrix, view_matrix)

0 commit comments

Comments
 (0)