Skip to content

Commit 3b6fd31

Browse files
committed
plot3d example: Making the projection with NumPy only
Signed-off-by: martinRenou <[email protected]>
1 parent 8b10297 commit 3b6fd31

File tree

4 files changed

+116
-54
lines changed

4 files changed

+116
-54
lines changed

environment.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,3 @@ dependencies:
55
- branca
66
- ipycanvas=0.7.0
77
- ipyevents
8-
- numpy
9-
- matplotlib

examples/plot3d.ipynb

Lines changed: 52 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,8 @@
66
"metadata": {},
77
"outputs": [],
88
"source": [
9-
"import matplotlib.pyplot as plt\n",
10-
"from mpl_toolkits.mplot3d import Axes3D, proj3d\n",
119
"import numpy as np\n",
12-
"from ipycanvas import Canvas, hold_canvas\n",
13-
"from ipywidgets import FloatSlider, Output\n",
14-
"from math import pi"
10+
"from ipycanvas import Canvas, hold_canvas"
1511
]
1612
},
1713
{
@@ -20,7 +16,23 @@
2016
"metadata": {},
2117
"outputs": [],
2218
"source": [
23-
"out = Output(layout={'border': '1px solid black'})"
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"
2436
]
2537
},
2638
{
@@ -35,40 +47,42 @@
3547
" \n",
3648
" self.width = 500\n",
3749
" self.height = 500\n",
38-
" \n",
39-
" plt.ioff()\n",
40-
" fig = plt.figure()\n",
41-
" self.ax = Axes3D(fig)\n",
4250
"\n",
4351
" self.dragging = False\n",
4452
" self.n = 200\n",
45-
" self.x = np.random.rand(self.n)\n",
46-
" self.y = np.random.rand(self.n)\n",
47-
" self.z = np.random.rand(self.n)\n",
48-
" \n",
49-
" self.zoom = 4\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",
5058
" self.dx = 0\n",
5159
" self.dy = 0\n",
52-
" self.ax.view_init(elev=self.dx, azim=self.dy)\n",
53-
" self.x2, self.y2, _ = proj3d.proj_transform(self.x, self.y, self.z, self.ax.get_proj())\n",
54-
" self.draw()\n",
55-
" \n",
60+
" self.update_matrix()\n",
61+
"\n",
5662
" self.on_mouse_down(self.mouse_down_handler)\n",
5763
" self.on_mouse_move(self.mouse_move_handler)\n",
5864
" self.on_mouse_up(self.mouse_up_handler)\n",
5965
" self.on_mouse_out(self.mouse_out_handler)\n",
6066
"\n",
61-
" #@out.capture()\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",
6279
" def draw(self):\n",
63-
" x = self.x2 * self.width * self.zoom + self.width / 2\n",
64-
" y = self.y2 * self.width * self.zoom + self.height / 2\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",
6583
" with hold_canvas(self):\n",
6684
" self.clear()\n",
67-
" self.save()\n",
68-
" for i in range(self.n):\n",
69-
" self.fill_arc(x[i], y[i], self.zoom, 0, 2*pi)\n",
70-
" self.stroke_arc(x[i], y[i], self.zoom, 0, 2*pi)\n",
71-
" self.restore()\n",
85+
" self.fill_circles(x, y, size)\n",
7286
"\n",
7387
" def mouse_down_handler(self, pixel_x, pixel_y):\n",
7488
" self.dragging = True\n",
@@ -79,9 +93,8 @@
7993
" if self.dragging:\n",
8094
" self.dx_new = self.dx + pixel_x - self.x_mouse\n",
8195
" self.dy_new = self.dy + pixel_y - self.y_mouse\n",
82-
" self.ax.view_init(elev=self.dy_new, azim=self.dx_new)\n",
83-
" self.x2, self.y2, _ = proj3d.proj_transform(self.x, self.y, self.z, self.ax.get_proj())\n",
84-
" self.draw()\n",
96+
" \n",
97+
" self.update_matrix(- self.dx_new, self.dy_new)\n",
8598
" \n",
8699
" def mouse_up_handler(self, pixel_x, pixel_y):\n",
87100
" if self.dragging:\n",
@@ -114,31 +127,18 @@
114127
"metadata": {},
115128
"outputs": [],
116129
"source": [
117-
"slider = FloatSlider(description='Zoom:', value=p.zoom, min=1, max=10)\n",
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",
118135
"\n",
119-
"def on_slider_change(change):\n",
120-
" p.zoom = slider.value\n",
121-
" p.draw()\n",
136+
" p.update_matrix(radius=slider_value)\n",
137+
"\n",
138+
"slider.observe(on_slider_move, 'value')\n",
122139
"\n",
123-
"slider.observe(on_slider_change, 'value')\n",
124140
"slider"
125141
]
126-
},
127-
{
128-
"cell_type": "markdown",
129-
"metadata": {},
130-
"source": [
131-
"## Drag the figure with the mouse to rotate, use the slider to zoom"
132-
]
133-
},
134-
{
135-
"cell_type": "code",
136-
"execution_count": null,
137-
"metadata": {},
138-
"outputs": [],
139-
"source": [
140-
"out"
141-
]
142142
}
143143
],
144144
"metadata": {
@@ -157,7 +157,7 @@
157157
"name": "python",
158158
"nbconvert_exporter": "python",
159159
"pygments_lexer": "ipython3",
160-
"version": "3.8.5"
160+
"version": "3.9.0"
161161
}
162162
},
163163
"nbformat": 4,

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)