Skip to content

Commit 71da505

Browse files
committed
Add opengl_three_dimensions.py
1 parent 5fe27c9 commit 71da505

File tree

5 files changed

+436
-1
lines changed

5 files changed

+436
-1
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import math
2+
3+
from ..constants import *
4+
from ..mobject.types.opengl_surface import OpenGLSurface
5+
from ..mobject.types.opengl_surface import OpenGLSurfaceGroup
6+
from ..mobject.types.opengl_vectorized_mobject import OpenGLVGroup
7+
from ..mobject.types.opengl_vectorized_mobject import OpenGLVMobject
8+
from ..utils.space_ops import get_norm
9+
from ..utils.space_ops import z_to_vector
10+
11+
12+
class OpenGLSurfaceMesh(OpenGLVGroup):
13+
def __init__(
14+
self,
15+
uv_surface,
16+
resolution=None,
17+
stroke_width=1,
18+
normal_nudge=1e-2,
19+
depth_test=True,
20+
flat_stroke=False,
21+
**kwargs
22+
):
23+
if not isinstance(uv_surface, OpenGLSurface):
24+
raise Exception("uv_surface must be of type OpenGLSurface")
25+
self.uv_surface = uv_surface
26+
self.resolution = resolution if resolution is not None else (21, 21)
27+
self.normal_nudge = normal_nudge
28+
super().__init__(
29+
stroke_width=stroke_width,
30+
depth_test=depth_test,
31+
flat_stroke=flat_stroke,
32+
**kwargs
33+
)
34+
35+
def init_points(self):
36+
uv_surface = self.uv_surface
37+
38+
full_nu, full_nv = uv_surface.resolution
39+
part_nu, part_nv = self.resolution
40+
u_indices = np.linspace(0, full_nu, part_nu).astype(int)
41+
v_indices = np.linspace(0, full_nv, part_nv).astype(int)
42+
43+
points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points()
44+
normals = uv_surface.get_unit_normals()
45+
nudge = 1e-2
46+
nudged_points = points + nudge * normals
47+
48+
for ui in u_indices:
49+
path = OpenGLVMobject()
50+
full_ui = full_nv * ui
51+
path.set_points_smoothly(nudged_points[full_ui : full_ui + full_nv])
52+
self.add(path)
53+
for vi in v_indices:
54+
path = OpenGLVMobject()
55+
path.set_points_smoothly(nudged_points[vi::full_nv])
56+
self.add(path)
57+
58+
59+
class OpenGLSphere(OpenGLSurface):
60+
def __init__(self, resolution=None, radius=1, u_range=None, v_range=None, **kwargs):
61+
resolution = resolution if resolution is not None else (101, 51)
62+
u_range = u_range if u_range is not None else (0, TAU)
63+
v_range = v_range if v_range is not None else (0, PI)
64+
self.radius = radius
65+
super().__init__(
66+
resolution=resolution, u_range=u_range, v_range=v_range, **kwargs
67+
)
68+
69+
def uv_func(self, u, v):
70+
return self.radius * np.array(
71+
[np.cos(u) * np.sin(v), np.sin(u) * np.sin(v), -np.cos(v)]
72+
)
73+
74+
75+
class OpenGLTorus(OpenGLSurface):
76+
def __init__(self, u_range=None, v_range=None, r1=3, r2=1, **kwargs):
77+
u_range = u_range if u_range is not None else (0, TAU)
78+
v_range = v_range if v_range is not None else (0, TAU)
79+
self.r1 = r1
80+
self.r2 = r2
81+
super().__init__(u_range=u_range, v_range=v_range, **kwargs)
82+
83+
def uv_func(self, u, v):
84+
P = np.array([math.cos(u), math.sin(u), 0])
85+
return (self.r1 - self.r2 * math.cos(v)) * P - math.sin(v) * OUT

manim/mobject/types/opengl_surface.py

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
import numpy as np
2+
import moderngl
3+
4+
from ...constants import *
5+
from ...mobject.opengl_mobject import OpenGLMobject
6+
from ...utils.bezier import integer_interpolate
7+
from ...utils.bezier import interpolate
8+
from ...utils.images import get_full_raster_image_path
9+
from ...utils.iterables import listify
10+
from ...utils.color import *
11+
from ...utils.space_ops import normalize_along_axis
12+
13+
14+
class OpenGLSurface(OpenGLMobject):
15+
shader_dtype = [
16+
("point", np.float32, (3,)),
17+
("du_point", np.float32, (3,)),
18+
("dv_point", np.float32, (3,)),
19+
("color", np.float32, (4,)),
20+
]
21+
22+
def __init__(
23+
self,
24+
u_range=None,
25+
v_range=None,
26+
# Resolution counts number of points sampled, which for
27+
# each coordinate is one more than the the number of
28+
# rows/columns of approximating squares
29+
resolution=None,
30+
color=GREY,
31+
opacity=1.0,
32+
gloss=0.3,
33+
shadow=0.4,
34+
prefered_creation_axis=1,
35+
# For du and dv steps. Much smaller and numerical error
36+
# can crop up in the shaders.
37+
epsilon=1e-5,
38+
render_primitive=moderngl.TRIANGLES,
39+
depth_test=True,
40+
shader_folder=None,
41+
**kwargs
42+
):
43+
self.u_range = u_range if u_range is not None else (0, 1)
44+
self.v_range = v_range if v_range is not None else (0, 1)
45+
# Resolution counts number of points sampled, which for
46+
# each coordinate is one more than the the number of
47+
# rows/columns of approximating squares
48+
self.resolution = resolution if resolution is not None else (101, 101)
49+
self.prefered_creation_axis = prefered_creation_axis
50+
# For du and dv steps. Much smaller and numerical error
51+
# can crop up in the shaders.
52+
self.epsilon = epsilon
53+
54+
super().__init__(
55+
color=color,
56+
opacity=opacity,
57+
gloss=gloss,
58+
shadow=shadow,
59+
shader_folder=shader_folder if shader_folder is not None else "surface",
60+
render_primitive=render_primitive,
61+
depth_test=depth_test,
62+
**kwargs
63+
)
64+
self.compute_triangle_indices()
65+
66+
def uv_func(self, u, v):
67+
# To be implemented in subclasses
68+
return (u, v, 0.0)
69+
70+
def init_points(self):
71+
dim = self.dim
72+
nu, nv = self.resolution
73+
u_range = np.linspace(*self.u_range, nu)
74+
v_range = np.linspace(*self.v_range, nv)
75+
76+
# Get three lists:
77+
# - Points generated by pure uv values
78+
# - Those generated by values nudged by du
79+
# - Those generated by values nudged by dv
80+
point_lists = []
81+
for (du, dv) in [(0, 0), (self.epsilon, 0), (0, self.epsilon)]:
82+
uv_grid = np.array([[[u + du, v + dv] for v in v_range] for u in u_range])
83+
point_grid = np.apply_along_axis(lambda p: self.uv_func(*p), 2, uv_grid)
84+
point_lists.append(point_grid.reshape((nu * nv, dim)))
85+
# Rather than tracking normal vectors, the points list will hold on to the
86+
# infinitesimal nudged values alongside the original values. This way, one
87+
# can perform all the manipulations they'd like to the surface, and normals
88+
# are still easily recoverable.
89+
self.set_points(np.vstack(point_lists))
90+
91+
def compute_triangle_indices(self):
92+
# TODO, if there is an event which changes
93+
# the resolution of the surface, make sure
94+
# this is called.
95+
nu, nv = self.resolution
96+
if nu == 0 or nv == 0:
97+
self.triangle_indices = np.zeros(0, dtype=int)
98+
return
99+
index_grid = np.arange(nu * nv).reshape((nu, nv))
100+
indices = np.zeros(6 * (nu - 1) * (nv - 1), dtype=int)
101+
indices[0::6] = index_grid[:-1, :-1].flatten() # Top left
102+
indices[1::6] = index_grid[+1:, :-1].flatten() # Bottom left
103+
indices[2::6] = index_grid[:-1, +1:].flatten() # Top right
104+
indices[3::6] = index_grid[:-1, +1:].flatten() # Top right
105+
indices[4::6] = index_grid[+1:, :-1].flatten() # Bottom left
106+
indices[5::6] = index_grid[+1:, +1:].flatten() # Bottom right
107+
self.triangle_indices = indices
108+
109+
def get_triangle_indices(self):
110+
return self.triangle_indices
111+
112+
def get_surface_points_and_nudged_points(self):
113+
points = self.get_points()
114+
k = len(points) // 3
115+
return points[:k], points[k : 2 * k], points[2 * k :]
116+
117+
def get_unit_normals(self):
118+
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
119+
normals = np.cross(
120+
(du_points - s_points) / self.epsilon,
121+
(dv_points - s_points) / self.epsilon,
122+
)
123+
return normalize_along_axis(normals, 1)
124+
125+
def pointwise_become_partial(self, smobject, a, b, axis=None):
126+
assert isinstance(smobject, Surface)
127+
if axis is None:
128+
axis = self.prefered_creation_axis
129+
if a <= 0 and b >= 1:
130+
self.match_points(smobject)
131+
return self
132+
133+
nu, nv = smobject.resolution
134+
self.set_points(
135+
np.vstack(
136+
[
137+
self.get_partial_points_array(
138+
arr.copy(), a, b, (nu, nv, 3), axis=axis
139+
)
140+
for arr in smobject.get_surface_points_and_nudged_points()
141+
]
142+
)
143+
)
144+
return self
145+
146+
def get_partial_points_array(self, points, a, b, resolution, axis):
147+
if len(points) == 0:
148+
return points
149+
nu, nv = resolution[:2]
150+
points = points.reshape(resolution)
151+
max_index = resolution[axis] - 1
152+
lower_index, lower_residue = integer_interpolate(0, max_index, a)
153+
upper_index, upper_residue = integer_interpolate(0, max_index, b)
154+
if axis == 0:
155+
points[:lower_index] = interpolate(
156+
points[lower_index], points[lower_index + 1], lower_residue
157+
)
158+
points[upper_index + 1 :] = interpolate(
159+
points[upper_index], points[upper_index + 1], upper_residue
160+
)
161+
else:
162+
shape = (nu, 1, resolution[2])
163+
points[:, :lower_index] = interpolate(
164+
points[:, lower_index], points[:, lower_index + 1], lower_residue
165+
).reshape(shape)
166+
points[:, upper_index + 1 :] = interpolate(
167+
points[:, upper_index], points[:, upper_index + 1], upper_residue
168+
).reshape(shape)
169+
return points.reshape((nu * nv, *resolution[2:]))
170+
171+
def sort_faces_back_to_front(self, vect=OUT):
172+
tri_is = self.triangle_indices
173+
indices = list(range(len(tri_is) // 3))
174+
points = self.get_points()
175+
176+
def index_dot(index):
177+
return np.dot(points[tri_is[3 * index]], vect)
178+
179+
indices.sort(key=index_dot)
180+
for k in range(3):
181+
tri_is[k::3] = tri_is[k::3][indices]
182+
return self
183+
184+
# For shaders
185+
def get_shader_data(self):
186+
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
187+
shader_data = np.zeros(len(s_points), dtype=OpenGLSurface.shader_dtype)
188+
if "points" not in self.locked_data_keys:
189+
shader_data["point"] = s_points
190+
shader_data["du_point"] = du_points
191+
shader_data["dv_point"] = dv_points
192+
self.fill_in_shader_color_info(shader_data)
193+
return shader_data
194+
195+
def fill_in_shader_color_info(self, shader_data):
196+
self.read_data_to_shader(shader_data, "color", "rgbas")
197+
return shader_data
198+
199+
def get_shader_vert_indices(self):
200+
return self.get_triangle_indices()
201+
202+
203+
class OpenGLSurfaceGroup(OpenGLSurface):
204+
def __init__(self, *parametric_surfaces, resolution=None, **kwargs):
205+
self.resolution = (0, 0) if resolution is None else resolution
206+
super().__init__(uv_func=None, **kwargs)
207+
self.add(*parametric_surfaces)
208+
209+
def init_points(self):
210+
pass # Needed?
211+
212+
213+
class OpenGLTexturedSurface(OpenGLSurface):
214+
shader_dtype = [
215+
("point", np.float32, (3,)),
216+
("du_point", np.float32, (3,)),
217+
("dv_point", np.float32, (3,)),
218+
("im_coords", np.float32, (2,)),
219+
("opacity", np.float32, (1,)),
220+
]
221+
222+
def __init__(
223+
self, uv_surface, image_file, dark_image_file=None, shader_folder=None, **kwargs
224+
):
225+
if not isinstance(uv_surface, OpenGLSurface):
226+
raise Exception("uv_surface must be of type OpenGLSurface")
227+
shader_folder = (
228+
shader_folder if shader_folder is not None else "textured_surface"
229+
)
230+
# Set texture information
231+
if dark_image_file is None:
232+
dark_image_file = image_file
233+
self.num_textures = 1
234+
else:
235+
self.num_textures = 2
236+
self.texture_paths = {
237+
"LightTexture": get_full_raster_image_path(image_file),
238+
"DarkTexture": get_full_raster_image_path(dark_image_file),
239+
}
240+
241+
self.uv_surface = uv_surface
242+
self.uv_func = uv_surface.uv_func
243+
self.u_range = uv_surface.u_range
244+
self.v_range = uv_surface.v_range
245+
self.resolution = uv_surface.resolution
246+
self.gloss = self.uv_surface.gloss
247+
super().__init__(**kwargs)
248+
249+
def init_data(self):
250+
super().init_data()
251+
self.data["im_coords"] = np.zeros((0, 2))
252+
self.data["opacity"] = np.zeros((0, 1))
253+
254+
def init_points(self):
255+
nu, nv = self.uv_surface.resolution
256+
self.set_points(self.uv_surface.get_points())
257+
self.data["im_coords"] = np.array(
258+
[
259+
[u, v]
260+
for u in np.linspace(0, 1, nu)
261+
for v in np.linspace(1, 0, nv) # Reverse y-direction
262+
]
263+
)
264+
265+
def init_uniforms(self):
266+
super().init_uniforms()
267+
self.uniforms["num_textures"] = self.num_textures
268+
269+
def init_colors(self):
270+
self.data["opacity"] = np.array([self.uv_surface.data["rgbas"][:, 3]])
271+
272+
def set_opacity(self, opacity, recurse=True):
273+
for mob in self.get_family(recurse):
274+
mob.data["opacity"] = np.array([[o] for o in listify(opacity)])
275+
return self
276+
277+
def pointwise_become_partial(self, tsmobject, a, b, axis=1):
278+
super().pointwise_become_partial(tsmobject, a, b, axis)
279+
im_coords = self.data["im_coords"]
280+
im_coords[:] = tsmobject.data["im_coords"]
281+
if a <= 0 and b >= 1:
282+
return self
283+
nu, nv = tsmobject.resolution
284+
im_coords[:] = self.get_partial_points_array(im_coords, a, b, (nu, nv, 2), axis)
285+
return self
286+
287+
def fill_in_shader_color_info(self, shader_data):
288+
self.read_data_to_shader(shader_data, "opacity", "opacity")
289+
self.read_data_to_shader(shader_data, "im_coords", "im_coords")
290+
return shader_data

manim/mobject/types/opengl_vectorized_mobject.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ...utils.bezier import bezier
1515

1616
# from manimlib.utils.bezier import get_smooth_quadratic_bezier_handle_points
17-
# from manimlib.utils.bezier import get_smooth_cubic_bezier_handle_points
17+
from ...utils.bezier import get_smooth_cubic_bezier_handle_points
1818
from ...utils.bezier import get_quadratic_approximation_of_cubic
1919
from ...utils.bezier import interpolate
2020
from ...utils.bezier import integer_interpolate

manim/opengl/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from ..mobject.types.opengl_vectorized_mobject import *
33
from ..mobject.opengl_geometry import *
44
from ..mobject.types.opengl_surface import *
5+
from ..mobject.opengl_three_dimensions import *

0 commit comments

Comments
 (0)