mirror of
https://github.com/3b1b/manim.git
synced 2025-09-19 04:41:56 +00:00
Enable 3d rotations
This commit is contained in:
parent
e882356264
commit
725a7e3121
16 changed files with 188 additions and 113 deletions
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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":
|
||||
|
|
12
manimlib/shaders/get_gl_Position.glsl
Normal file
12
manimlib/shaders/get_gl_Position.glsl
Normal 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);
|
||||
}
|
|
@ -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));
|
||||
}
|
13
manimlib/shaders/position_point_into_frame.glsl
Normal file
13
manimlib/shaders/position_point_into_frame.glsl
Normal 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;
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
vec3 rotate_point_for_frame(vec3 point){
|
||||
// TODO, orient in 3d based on certain rotation matrices
|
||||
return point;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Reference in a new issue