From 725a7e31218219160f26e9b0455902c8056f8afe Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 1 Jun 2020 16:21:18 -0700 Subject: [PATCH] Enable 3d rotations --- .../active/diffyq/part3/temperature_graphs.py | 4 +- manimlib/camera/camera.py | 139 ++++++++++++------ manimlib/constants.py | 1 - manimlib/mobject/frame.py | 3 - manimlib/scene/scene.py | 5 +- manimlib/shaders/get_gl_Position.glsl | 12 ++ manimlib/shaders/image_vert.glsl | 16 +- .../shaders/position_point_into_frame.glsl | 13 ++ .../shaders/quadratic_bezier_fill_geom.glsl | 15 +- .../shaders/quadratic_bezier_fill_vert.glsl | 10 +- .../shaders/quadratic_bezier_stroke_geom.glsl | 12 +- .../shaders/quadratic_bezier_stroke_vert.glsl | 25 ++-- manimlib/shaders/rotate_point_for_frame.glsl | 4 - .../scale_and_shift_point_for_frame.glsl | 7 +- manimlib/shaders/simple_vert.glsl | 9 +- manimlib/utils/space_ops.py | 26 ++-- 16 files changed, 188 insertions(+), 113 deletions(-) create mode 100644 manimlib/shaders/get_gl_Position.glsl create mode 100644 manimlib/shaders/position_point_into_frame.glsl delete mode 100644 manimlib/shaders/rotate_point_for_frame.glsl diff --git a/from_3b1b/active/diffyq/part3/temperature_graphs.py b/from_3b1b/active/diffyq/part3/temperature_graphs.py index 13d2c196..e1c2f1c1 100644 --- a/from_3b1b/active/diffyq/part3/temperature_graphs.py +++ b/from_3b1b/active/diffyq/part3/temperature_graphs.py @@ -1369,9 +1369,7 @@ class SineWaveScaledByExp(TemperatureGraphScene): theta=-80 * DEGREES, distance=50, ) - self.camera.set_frame_center( - 2 * RIGHT, - ) + self.camera.frame.move_to(2 * RIGHT) def show_sine_wave(self): time_tracker = ValueTracker(0) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 59f18cfb..15e9b470 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -15,6 +15,11 @@ from manimlib.utils.simple_functions import fdiv from manimlib.utils.shaders import shader_info_to_id from manimlib.utils.shaders import shader_id_to_info from manimlib.utils.shaders import get_shader_code_from_file +from manimlib.utils.space_ops import cross +from manimlib.utils.space_ops import rotation_matrix_transpose_from_quaternion +from manimlib.utils.space_ops import normalize +from manimlib.utils.space_ops import quaternion_from_angle_axis +from manimlib.utils.space_ops import quaternion_mult # TODO, think about how to incorporate perspective, @@ -23,25 +28,100 @@ class CameraFrame(Mobject): CONFIG = { "width": FRAME_WIDTH, "height": FRAME_HEIGHT, - "center": ORIGIN, + "center_point": ORIGIN, + # The quaternion describing how to rotate into + # the position of the camera. + "rotation_quaternion": [1, 0, 0, 0], + "focal_distance": 5, } def init_points(self): - self.points = np.array([UL, UR, DR, DL]) - self.set_width(self.width, stretch=True) - self.set_height(self.height, stretch=True) - self.move_to(self.center) - self.save_state() + self.points = np.array([self.center_point]) + self.rotation_quaternion = quaternion_from_angle_axis(angle=0, axis=OUT) + + def to_default_state(self): + self.center() + self.set_height(FRAME_HEIGHT) + self.set_width(FRAME_WIDTH) + self.set_rotation_quaternion([1, 0, 0, 0]) + return self + + def get_transform_to_screen_space(self): + # Map from real space into camera space + result = np.identity(4) + # First shift so that origin of real space coincides with camera origin + result[:3, 3] = -self.get_center().T + # Rotate based on camera orientation + result[:3, :3] = np.dot( + rotation_matrix_transpose_from_quaternion(self.rotation_quaternion), + result[:3, :3] + ) + # Scale to have height 2 (matching the height of the box [-1, 1]^2) + result *= 2 / self.height + return result + + def rotate(self, angle, axis=OUT, **kwargs): + self.rotation_quaternion = normalize(quaternion_mult( + quaternion_from_angle_axis(angle, axis), + self.rotation_quaternion + )) + return self + + def set_rotation(self, phi=0, theta=0, gamma=0): + self.rotation_quaternion = normalize(quaternion_mult( + quaternion_from_angle_axis(theta, OUT), + quaternion_from_angle_axis(phi, RIGHT), + quaternion_from_angle_axis(gamma, OUT), + )) + return self + + def set_rotation_quaternion(self, quat): + self.rotation_quaternion = quat + return self + + def increment_theta(self, dtheta): + self.rotate(dtheta, OUT) + + def increment_phi(self, dphi): + camera_point = rotation_matrix_transpose_from_quaternion(self.rotation_quaternion)[2] + axis = cross(OUT, camera_point) + axis = normalize(axis, fall_back=RIGHT) + self.rotate(dphi, axis) + + def scale(self, scale_factor, **kwargs): + # TODO, handle about_point and about_edge? + self.height *= scale_factor + self.width *= scale_factor + return self + + def set_height(self, height): + self.height = height + return self + + def set_width(self, width): + self.width = width + return self + + def get_height(self): + return self.height + + def get_width(self): + return self.width + + def get_center(self): + return self.points[0] + + def get_focal_distance(self): + return self.focal_distance + + def interpolate(self, mobject1, mobject2, alpha, **kwargs): + pass class Camera(object): CONFIG = { "background_image": None, - "frame_config": { - "width": FRAME_WIDTH, - "height": FRAME_HEIGHT, - "center": ORIGIN, - }, + "frame_config": {}, "pixel_height": DEFAULT_PIXEL_HEIGHT, "pixel_width": DEFAULT_PIXEL_WIDTH, "frame_rate": DEFAULT_FRAME_RATE, # TODO, move this elsewhere @@ -107,8 +187,8 @@ class Camera(object): frame_height = frame_width / aspect_ratio else: frame_width = aspect_ratio * frame_height - self.set_frame_height(frame_height) - self.set_frame_width(frame_width) + self.frame.set_height(frame_height) + self.frame.set_width(frame_width) def clear(self): rgba = (*Color(self.background_color).get_rgb(), self.background_opacity) @@ -163,7 +243,6 @@ class Camera(object): def get_pixel_height(self): return self.get_pixel_shape()[1] - # TODO, make these work for a rotated frame def get_frame_height(self): return self.frame.get_height() @@ -176,18 +255,9 @@ class Camera(object): def get_frame_center(self): return self.frame.get_center() - def set_frame_height(self, height): - self.frame.set_height(height, stretch=True) - - def set_frame_width(self, width): - self.frame.set_width(width, stretch=True) - - def set_frame_center(self, center): - self.frame.move_to(center) - def pixel_coords_to_space_coords(self, px, py, relative=False): # pw, ph = self.fbo.size - # Back hack, not sure why this is needed. + # Bad hack, not sure why this is needed. pw, ph = self.get_pixel_shape() pw //= 2 ph //= 2 @@ -200,19 +270,6 @@ class Camera(object): scale = fh / ph return fc + scale * np.array([(px - pw / 2), (py - ph / 2), 0]) - # TODO, account for 3d - # Also, move this to CameraFrame? - def is_in_frame(self, mobject): - fc = self.get_frame_center() - fh = self.get_frame_height() - fw = self.get_frame_width() - return not reduce(op.or_, [ - mobject.get_right()[0] < fc[0] - fw, - mobject.get_bottom()[1] > fc[1] + fh, - mobject.get_left()[0] > fc[0] + fw, - mobject.get_top()[1] < fc[1] - fh, - ]) - # Rendering def capture(self, *mobjects, **kwargs): self.refresh_shader_uniforms() @@ -270,15 +327,13 @@ class Camera(object): if shader is None: return # TODO, think about how uniforms come from mobjects as well. - fh = self.get_frame_height() - fc = self.get_frame_center() pw, ph = self.get_pixel_shape() mapping = { - 'scale': fh / 2, # Scale based on frame size + 'to_screen_space': tuple(self.frame.get_transform_to_screen_space().T.flatten()), 'aspect_ratio': (pw / ph), # AR based on pixel shape - 'anti_alias_width': ANTI_ALIAS_WIDTH_OVER_FRAME_HEIGHT * fh, - 'frame_center': tuple(fc), + 'focal_distance': self.frame.get_focal_distance(), + 'anti_alias_width': 3 / ph, # 1.5 Pixel widths } for key, value in mapping.items(): try: diff --git a/manimlib/constants.py b/manimlib/constants.py index 7422b13d..d6dd1770 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -154,7 +154,6 @@ DEFAULT_PIXEL_WIDTH = PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_width"] DEFAULT_FRAME_RATE = 60 DEFAULT_STROKE_WIDTH = 4 -ANTI_ALIAS_WIDTH_OVER_FRAME_HEIGHT = 1e-3 FRAME_HEIGHT = 8.0 FRAME_WIDTH = FRAME_HEIGHT * DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT diff --git a/manimlib/mobject/frame.py b/manimlib/mobject/frame.py index 312a3ce0..4ea2260d 100644 --- a/manimlib/mobject/frame.py +++ b/manimlib/mobject/frame.py @@ -3,9 +3,6 @@ from manimlib.mobject.geometry import Rectangle from manimlib.utils.config_ops import digest_config -# TODO, put CameraFrame in here? - - class ScreenRectangle(Rectangle): CONFIG = { "aspect_ratio": 16.0 / 9.0, diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 5d1960cc..412c64a3 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -529,6 +529,9 @@ class Scene(Container): def on_mouse_drag(self, point, d_point, buttons, modifiers): self.mouse_drag_point.move_to(point) + # Only if 3d rotation is enabled? + self.camera.frame.increment_theta(-d_point[0]) + self.camera.frame.increment_phi(d_point[1]) def on_mouse_press(self, point, button, mods): pass @@ -550,7 +553,7 @@ class Scene(Container): def on_key_press(self, symbol, modifiers): if chr(symbol) == "r": - self.camera.frame.restore() + self.camera.frame.to_default_state() elif chr(symbol) == "z": self.zoom_on_scroll = True elif chr(symbol) == "q": diff --git a/manimlib/shaders/get_gl_Position.glsl b/manimlib/shaders/get_gl_Position.glsl new file mode 100644 index 00000000..55fc2ef8 --- /dev/null +++ b/manimlib/shaders/get_gl_Position.glsl @@ -0,0 +1,12 @@ +// Assumes the following uniforms exist in the surrounding context: +// uniform float aspect_ratio; + +vec4 get_gl_Position(vec3 point){ + // Extremely minimal modification, but that might change later... + point.x /= aspect_ratio; + // Todo, does this discontinuity add weirdness? Theoretically, by this point, + // the z-coordiante of gl_Position only matter for z-indexing. The reason + // for thie line is to avoid agressive clipping of distant points. + if(point.z < 0) point.z *= 0.1; + return vec4(point, 1); +} \ No newline at end of file diff --git a/manimlib/shaders/image_vert.glsl b/manimlib/shaders/image_vert.glsl index 1e81a472..f7e48932 100644 --- a/manimlib/shaders/image_vert.glsl +++ b/manimlib/shaders/image_vert.glsl @@ -1,8 +1,9 @@ #version 330 -uniform float scale; uniform float aspect_ratio; -uniform vec3 frame_center; +uniform float anti_alias_width; +uniform mat4 to_screen_space; +uniform float focal_distance; uniform sampler2D Texture; @@ -14,16 +15,11 @@ out vec2 v_im_coords; out float v_opacity; // Analog of import for manim only -#INSERT rotate_point_for_frame.glsl -#INSERT scale_and_shift_point_for_frame.glsl +#INSERT get_gl_Position.glsl +#INSERT position_point_into_frame.glsl void main(){ v_im_coords = im_coords; v_opacity = opacity; - gl_Position = vec4( - rotate_point_for_frame( - scale_and_shift_point_for_frame(point) - ), - 1.0 - ); + gl_Position = get_gl_Position(position_point_into_frame(point)); } \ No newline at end of file diff --git a/manimlib/shaders/position_point_into_frame.glsl b/manimlib/shaders/position_point_into_frame.glsl new file mode 100644 index 00000000..7e2eaf6a --- /dev/null +++ b/manimlib/shaders/position_point_into_frame.glsl @@ -0,0 +1,13 @@ +// Must be used in an environment with the following uniforms: +// uniform mat4 to_screen_space; +// uniform float focal_distance; + +vec3 position_point_into_frame(vec3 point){ + // Most of the heavy lifting is done by the pre-computed + // to_screen_space matrix; here's there just a little added + // perspective morphing. + vec4 new_point = to_screen_space * vec4(point, 1); + new_point.z /= focal_distance; + new_point.xy /= max(1 - new_point.z, 0); + return new_point.xyz; +} diff --git a/manimlib/shaders/quadratic_bezier_fill_geom.glsl b/manimlib/shaders/quadratic_bezier_fill_geom.glsl index 877b1ee6..a9f21476 100644 --- a/manimlib/shaders/quadratic_bezier_fill_geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill_geom.glsl @@ -3,10 +3,9 @@ layout (triangles) in; layout (triangle_strip, max_vertices = 5) out; -uniform float scale; +// Needed for get_gl_Position uniform float aspect_ratio; uniform float anti_alias_width; -uniform vec3 frame_center; in vec3 bp[3]; in vec4 v_color[3]; @@ -25,15 +24,12 @@ out float bezier_degree; // so to share functionality between this and others, the caller // in manim replaces this line with the contents of named file #INSERT quadratic_bezier_geometry_functions.glsl -#INSERT scale_and_shift_point_for_frame.glsl +#INSERT get_gl_Position.glsl void emit_simple_triangle(){ for(int i = 0; i < 3; i++){ color = v_color[i]; - gl_Position = vec4( - scale_and_shift_point_for_frame(bp[i]), - 1.0 - ); + gl_Position = get_gl_Position(bp[i]); EmitVertex(); } EndPrimitive(); @@ -79,10 +75,7 @@ void emit_pentagon(vec2 bp0, vec2 bp1, vec2 bp2){ if(i < 2) color = v_color[0]; else if(i == 2) color = v_color[1]; else color = v_color[2]; - gl_Position = vec4( - scale_and_shift_point_for_frame(vec3(corner, z)), - 1.0 - ); + gl_Position = get_gl_Position(vec3(corner, z)); EmitVertex(); } EndPrimitive(); diff --git a/manimlib/shaders/quadratic_bezier_fill_vert.glsl b/manimlib/shaders/quadratic_bezier_fill_vert.glsl index 6284070e..8316f162 100644 --- a/manimlib/shaders/quadratic_bezier_fill_vert.glsl +++ b/manimlib/shaders/quadratic_bezier_fill_vert.glsl @@ -1,5 +1,8 @@ #version 330 +uniform mat4 to_screen_space; +uniform float focal_distance; + in vec3 point; in vec4 color; in float fill_all; // Either 0 or 1 @@ -8,10 +11,13 @@ out vec3 bp; // Bezier control point out vec4 v_color; out float v_fill_all; -#INSERT rotate_point_for_frame.glsl +// To my knowledge, there is no notion of #include for shaders, +// so to share functionality between this and others, the caller +// replaces this line with the contents of named file +#INSERT position_point_into_frame.glsl void main(){ - bp = rotate_point_for_frame(point); + bp = position_point_into_frame(point); v_color = color; v_fill_all = fill_all; } \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke_geom.glsl b/manimlib/shaders/quadratic_bezier_stroke_geom.glsl index 9434fb74..d9217cb1 100644 --- a/manimlib/shaders/quadratic_bezier_stroke_geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke_geom.glsl @@ -3,10 +3,9 @@ layout (triangles) in; layout (triangle_strip, max_vertices = 5) out; -uniform float scale; +// Needed for get_gl_Position uniform float aspect_ratio; uniform float anti_alias_width; -uniform vec3 frame_center; in vec3 bp[3]; in vec3 prev_bp[3]; @@ -43,7 +42,7 @@ const float MITER_JOINT = 3; // so to share functionality between this and others, the caller // replaces this line with the contents of named file #INSERT quadratic_bezier_geometry_functions.glsl -#INSERT scale_and_shift_point_for_frame.glsl +#INSERT get_gl_Position.glsl float angle_between_vectors(vec2 v1, vec2 v2){ @@ -160,7 +159,7 @@ int get_corners(vec2 controls[3], int degree, out vec2 corners[5]){ create_joint(-angle_to_next, v21, buff2, bevel_end, c2, c2, c3, c3); } - // Linear case is the simplets + // Linear case is the simplest if(degree == 1){ // Swap between 2 and 3 is deliberate, the order of corners // should be for a triangle_strip. Last entry is a dummy @@ -306,10 +305,7 @@ void main() { uv_stroke_width = stroke_widths[i] / scale_factor; color = stroke_colors[i]; - gl_Position = vec4( - scale_and_shift_point_for_frame(vec3(corner, z_values[i])), - 1.0 - ); + gl_Position = get_gl_Position(vec3(corner, z_values[i])); EmitVertex(); } EndPrimitive(); diff --git a/manimlib/shaders/quadratic_bezier_stroke_vert.glsl b/manimlib/shaders/quadratic_bezier_stroke_vert.glsl index 60b70580..fb60f651 100644 --- a/manimlib/shaders/quadratic_bezier_stroke_vert.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke_vert.glsl @@ -1,5 +1,8 @@ #version 330 +uniform mat4 to_screen_space; +uniform float focal_distance; + in vec3 point; in vec3 prev_point; in vec3 next_point; @@ -8,7 +11,8 @@ in float stroke_width; in vec4 color; in float joint_type; -out vec3 bp; // Bezier control point +// Bezier control point +out vec3 bp; out vec3 prev_bp; out vec3 next_bp; @@ -16,19 +20,20 @@ out float v_stroke_width; out vec4 v_color; out float v_joint_type; -// TODO, this should maybe depend on scale -const float STROKE_WIDTH_CONVERSION = 0.01; - - -#INSERT rotate_point_for_frame.glsl +const float STROKE_WIDTH_CONVERSION = 0.0025; +// To my knowledge, there is no notion of #include for shaders, +// so to share functionality between this and others, the caller +// replaces this line with the contents of named file +#INSERT position_point_into_frame.glsl void main(){ + bp = position_point_into_frame(point); + prev_bp = position_point_into_frame(prev_point); + next_bp = position_point_into_frame(next_point); + v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width; + v_stroke_width /= (1 - bp.z); // Change stroke width by perspective v_color = color; v_joint_type = joint_type; - - bp = rotate_point_for_frame(point); - prev_bp = rotate_point_for_frame(prev_point); - next_bp = rotate_point_for_frame(next_point); } \ No newline at end of file diff --git a/manimlib/shaders/rotate_point_for_frame.glsl b/manimlib/shaders/rotate_point_for_frame.glsl deleted file mode 100644 index 5e36077e..00000000 --- a/manimlib/shaders/rotate_point_for_frame.glsl +++ /dev/null @@ -1,4 +0,0 @@ -vec3 rotate_point_for_frame(vec3 point){ - // TODO, orient in 3d based on certain rotation matrices - return point; -} diff --git a/manimlib/shaders/scale_and_shift_point_for_frame.glsl b/manimlib/shaders/scale_and_shift_point_for_frame.glsl index e299f929..4c13eced 100644 --- a/manimlib/shaders/scale_and_shift_point_for_frame.glsl +++ b/manimlib/shaders/scale_and_shift_point_for_frame.glsl @@ -1,11 +1,8 @@ // Assumes the following uniforms exist in the surrounding context: -// uniform float scale; // uniform float aspect_ratio; -// uniform float frame_center; +// TODO, rename -vec3 scale_and_shift_point_for_frame(vec3 point){ - point -= frame_center; - point /= scale; +vec3 get_gl_Position(vec3 point){ point.x /= aspect_ratio; return point; } \ No newline at end of file diff --git a/manimlib/shaders/simple_vert.glsl b/manimlib/shaders/simple_vert.glsl index 829ab02e..c5d8c546 100644 --- a/manimlib/shaders/simple_vert.glsl +++ b/manimlib/shaders/simple_vert.glsl @@ -1,15 +1,16 @@ #version 330 -uniform float scale; uniform float aspect_ratio; uniform float anti_alias_width; -uniform vec3 frame_center; +uniform mat4 to_screen_space; +uniform float focal_distance; in vec3 point; // Analog of import for manim only -#INSERT set_gl_Position.glsl +#INSERT get_gl_Position.glsl +#INSERT position_point_into_frame.glsl void main(){ - set_gl_Position(point); + gl_Position = get_gl_Position(position_point_into_frame(point)); } \ No newline at end of file diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 6bc974fb..f106b259 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -84,6 +84,22 @@ def thick_diagonal(dim, thickness=2): return (np.abs(row_indices - col_indices) < thickness).astype('uint8') +def rotation_matrix_transpose_from_quaternion(quat): + quat_inv = quaternion_conjugate(quat) + return [ + quaternion_mult(quat, [0, *basis], quat_inv)[1:] + for basis in [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + ] + ] + + +def rotation_matrix_from_quaternion(quat): + return np.transpose(rotation_matrix_transpose_from_quaternion(quat)) + + def rotation_matrix_transpose(angle, axis): if axis[0] == 0 and axis[1] == 0: # axis = [0, 0, z] case is common enough it's worth @@ -97,15 +113,7 @@ def rotation_matrix_transpose(angle, axis): [0, 0, 1], ] quat = quaternion_from_angle_axis(angle, axis) - quat_inv = quaternion_conjugate(quat) - return [ - quaternion_mult(quat, [0, *basis], quat_inv)[1:] - for basis in [ - [1, 0, 0], - [0, 1, 0], - [0, 0, 1], - ] - ] + return rotation_matrix_transpose_from_quaternion(quat) def rotation_matrix(angle, axis):