Enable 3d rotations

This commit is contained in:
Grant Sanderson 2020-06-01 16:21:18 -07:00
parent e882356264
commit 725a7e3121
16 changed files with 188 additions and 113 deletions

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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,

View file

@ -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":

View file

@ -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);
}

View file

@ -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));
}

View file

@ -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;
}

View file

@ -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();

View file

@ -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;
}

View file

@ -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();

View file

@ -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);
}

View file

@ -1,4 +0,0 @@
vec3 rotate_point_for_frame(vec3 point){
// TODO, orient in 3d based on certain rotation matrices
return point;
}

View file

@ -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;
}

View file

@ -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));
}

View file

@ -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):