2018-12-24 12:37:51 -08:00
|
|
|
from functools import reduce
|
2018-05-21 12:11:46 -07:00
|
|
|
import operator as op
|
2020-02-04 15:27:21 -08:00
|
|
|
import moderngl
|
|
|
|
import re
|
|
|
|
from colour import Color
|
2016-02-23 22:29:32 -08:00
|
|
|
|
2018-03-31 15:11:35 -07:00
|
|
|
from PIL import Image
|
2018-12-24 12:37:51 -08:00
|
|
|
import numpy as np
|
2020-02-04 15:27:21 -08:00
|
|
|
import itertools as it
|
2018-03-31 15:11:35 -07:00
|
|
|
|
2018-12-24 12:37:51 -08:00
|
|
|
from manimlib.constants import *
|
|
|
|
from manimlib.mobject.mobject import Mobject
|
|
|
|
from manimlib.utils.config_ops import digest_config
|
|
|
|
from manimlib.utils.iterables import batch_by_property
|
|
|
|
from manimlib.utils.iterables import list_difference_update
|
2020-01-15 18:30:58 -08:00
|
|
|
from manimlib.utils.family_ops import extract_mobject_family_members
|
2018-12-24 12:37:51 -08:00
|
|
|
from manimlib.utils.simple_functions import fdiv
|
2020-02-04 15:27:21 -08:00
|
|
|
|
|
|
|
|
|
|
|
# TODO, think about how to incorporate perspective,
|
|
|
|
# and change get_height, etc. to take orientation into account
|
|
|
|
class CameraFrame(Mobject):
|
|
|
|
CONFIG = {
|
|
|
|
"width": FRAME_WIDTH,
|
|
|
|
"height": FRAME_HEIGHT,
|
|
|
|
"center": ORIGIN,
|
|
|
|
}
|
|
|
|
|
|
|
|
def generate_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)
|
2018-02-11 18:21:31 -08:00
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2016-02-23 22:29:32 -08:00
|
|
|
class Camera(object):
|
2016-02-27 16:32:53 -08:00
|
|
|
CONFIG = {
|
2018-04-06 13:58:59 -07:00
|
|
|
"background_image": None,
|
2020-02-04 15:27:21 -08:00
|
|
|
"frame_config": {
|
|
|
|
"width": FRAME_WIDTH,
|
|
|
|
"height": FRAME_HEIGHT,
|
|
|
|
"center": ORIGIN,
|
|
|
|
},
|
2018-05-21 12:11:46 -07:00
|
|
|
"pixel_height": DEFAULT_PIXEL_HEIGHT,
|
|
|
|
"pixel_width": DEFAULT_PIXEL_WIDTH,
|
2019-02-03 12:09:20 -08:00
|
|
|
"frame_rate": DEFAULT_FRAME_RATE,
|
2018-05-21 12:11:46 -07:00
|
|
|
# Note: frame height and width will be resized to match
|
|
|
|
# the pixel aspect ratio
|
2018-04-06 13:58:59 -07:00
|
|
|
"background_color": BLACK,
|
2018-08-08 11:50:34 -07:00
|
|
|
"background_opacity": 1,
|
2018-04-06 13:58:59 -07:00
|
|
|
# Points in vectorized mobjects with norm greater
|
|
|
|
# than this value will be rescaled.
|
|
|
|
"max_allowable_norm": FRAME_WIDTH,
|
|
|
|
"image_mode": "RGBA",
|
2018-08-09 17:56:05 -07:00
|
|
|
"n_channels": 4,
|
2018-04-06 13:58:59 -07:00
|
|
|
"pixel_array_dtype": 'uint8',
|
2020-02-04 15:27:21 -08:00
|
|
|
"line_width_multiple": 0.01,
|
|
|
|
"background_fbo": None,
|
2016-02-23 22:29:32 -08:00
|
|
|
}
|
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
def __init__(self, background=None, **kwargs):
|
2016-02-23 22:29:32 -08:00
|
|
|
digest_config(self, kwargs, locals())
|
2018-02-16 12:15:16 -08:00
|
|
|
self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max
|
2020-02-04 15:27:21 -08:00
|
|
|
self.init_frame()
|
|
|
|
self.init_context()
|
|
|
|
self.init_frame_buffer()
|
|
|
|
self.init_shaders()
|
|
|
|
|
|
|
|
def init_frame(self):
|
|
|
|
self.frame = CameraFrame(**self.frame_config)
|
|
|
|
|
|
|
|
def init_context(self):
|
|
|
|
# TODO, context with a window?
|
|
|
|
ctx = moderngl.create_standalone_context()
|
|
|
|
ctx.enable(moderngl.BLEND)
|
|
|
|
ctx.blend_func = (
|
|
|
|
moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA,
|
|
|
|
moderngl.ONE, moderngl.ONE
|
|
|
|
)
|
|
|
|
self.ctx = ctx
|
2018-05-21 12:11:46 -07:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
# Methods associated with the frame buffer
|
|
|
|
def init_frame_buffer(self):
|
|
|
|
# TODO, account for live window
|
|
|
|
self.fbo = self.get_fbo()
|
|
|
|
self.fbo.use()
|
|
|
|
self.clear()
|
2018-05-21 12:11:46 -07:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def get_fbo(self):
|
|
|
|
return self.ctx.simple_framebuffer(self.get_pixel_shape())
|
2018-05-21 12:11:46 -07:00
|
|
|
|
2018-04-06 13:58:59 -07:00
|
|
|
def resize_frame_shape(self, fixed_dimension=0):
|
2016-02-27 13:33:46 -08:00
|
|
|
"""
|
2018-04-06 13:58:59 -07:00
|
|
|
Changes frame_shape to match the aspect ratio
|
2018-05-21 12:11:46 -07:00
|
|
|
of the pixels, where fixed_dimension determines
|
|
|
|
whether frame_height or frame_width
|
2016-02-27 13:33:46 -08:00
|
|
|
remains fixed while the other changes accordingly.
|
|
|
|
"""
|
2018-05-21 12:11:46 -07:00
|
|
|
pixel_height = self.get_pixel_height()
|
|
|
|
pixel_width = self.get_pixel_width()
|
|
|
|
frame_height = self.get_frame_height()
|
|
|
|
frame_width = self.get_frame_width()
|
|
|
|
aspect_ratio = fdiv(pixel_width, pixel_height)
|
2016-02-27 13:33:46 -08:00
|
|
|
if fixed_dimension == 0:
|
2018-05-21 12:11:46 -07:00
|
|
|
frame_height = frame_width / aspect_ratio
|
2016-02-27 13:33:46 -08:00
|
|
|
else:
|
2018-05-21 12:11:46 -07:00
|
|
|
frame_width = aspect_ratio * frame_height
|
|
|
|
self.set_frame_height(frame_height)
|
|
|
|
self.set_frame_width(frame_width)
|
2016-02-23 22:29:32 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def clear(self):
|
|
|
|
if self.background_fbo:
|
|
|
|
self.ctx.copy_framebuffer(self.fbo, self.background_fbo)
|
2016-02-23 22:29:32 -08:00
|
|
|
else:
|
2020-02-04 15:27:21 -08:00
|
|
|
rgba = (*Color(self.background_color).get_rgb(), self.background_opacity)
|
|
|
|
self.fbo.clear(*rgba)
|
|
|
|
|
|
|
|
def lock_state_as_background(self):
|
|
|
|
self.background_fbo = self.get_fbo()
|
|
|
|
self.ctx.copy_framebuffer(self.background_fbo, self.fbo)
|
|
|
|
|
|
|
|
def unlock_background(self):
|
|
|
|
self.background_fbo = None
|
|
|
|
|
|
|
|
def reset_pixel_shape(self, new_height, new_width):
|
|
|
|
self.pixel_width = new_width
|
|
|
|
self.pixel_height = new_height
|
|
|
|
self.fbo.release()
|
|
|
|
self.init_frame_buffer()
|
|
|
|
|
|
|
|
# Various ways to read from the fbo
|
|
|
|
def get_raw_fbo_data(self, dtype='f1'):
|
|
|
|
return self.fbo.read(components=self.n_channels, dtype=dtype)
|
2016-02-23 22:29:32 -08:00
|
|
|
|
2018-08-11 20:54:27 -07:00
|
|
|
def get_image(self, pixel_array=None):
|
2020-02-04 15:27:21 -08:00
|
|
|
return Image.frombytes(
|
|
|
|
'RGBA', self.fbo.size,
|
|
|
|
self.get_raw_fbo_data(),
|
|
|
|
'raw', 'RGBA', 0, -1
|
2017-09-26 17:41:45 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
def get_pixel_array(self):
|
2020-02-04 15:27:21 -08:00
|
|
|
raw = self.get_raw_fbo_data(dtype='f4')
|
|
|
|
flat_arr = np.frombuffer(raw, dtype='f4')
|
|
|
|
arr = flat_arr.reshape([*self.fbo.size, self.n_channels])
|
|
|
|
# Convert from float
|
|
|
|
return (self.rgb_max_val * arr).astype(self.pixel_array_dtype)
|
|
|
|
|
|
|
|
# Needed?
|
|
|
|
def get_texture(self):
|
|
|
|
texture = self.ctx.texture(
|
|
|
|
size=self.fbo.size,
|
|
|
|
components=4,
|
|
|
|
data=self.get_raw_fbo_data(),
|
|
|
|
dtype='f4'
|
|
|
|
)
|
|
|
|
return texture
|
2016-02-23 22:29:32 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
# Getting camera attributes
|
|
|
|
def get_pixel_shape(self):
|
|
|
|
return (self.pixel_width, self.pixel_height)
|
2018-01-31 17:17:58 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def get_pixel_width(self):
|
|
|
|
return self.get_pixel_shape()[0]
|
2018-01-31 17:17:58 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def get_pixel_height(self):
|
|
|
|
return self.get_pixel_shape()[1]
|
2018-01-31 17:17:58 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
# TODO, make these work for a rotated frame
|
|
|
|
def get_frame_height(self):
|
|
|
|
return self.frame.get_height()
|
2018-03-09 10:32:19 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def get_frame_width(self):
|
|
|
|
return self.frame.get_width()
|
2018-01-31 17:17:58 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def get_frame_center(self):
|
|
|
|
return self.frame.get_center()
|
2016-11-23 17:50:25 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def set_frame_height(self, height):
|
|
|
|
self.frame.set_height(height, stretch=True)
|
2016-02-23 22:29:32 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def set_frame_width(self, width):
|
|
|
|
self.frame.set_width(width, stretch=True)
|
|
|
|
|
|
|
|
def set_frame_center(self, center):
|
|
|
|
self.frame.move_to(center)
|
2018-01-24 12:14:37 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
# TODO, account for 3d
|
2018-05-30 12:01:47 -07:00
|
|
|
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_, [
|
2018-06-18 13:03:56 -07:00
|
|
|
mobject.get_right()[0] < fc[0] - fw,
|
2018-05-30 12:01:47 -07:00
|
|
|
mobject.get_bottom()[1] > fc[1] + fh,
|
2018-06-18 13:03:56 -07:00
|
|
|
mobject.get_left()[0] > fc[0] + fw,
|
2018-05-30 12:01:47 -07:00
|
|
|
mobject.get_top()[1] < fc[1] - fh,
|
|
|
|
])
|
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
# Rendering
|
|
|
|
def get_mobjects_to_display(self, mobjects, excluded_mobjects=None):
|
|
|
|
mobjects = extract_mobject_family_members(
|
|
|
|
mobjects, only_those_with_points=True,
|
|
|
|
)
|
|
|
|
if excluded_mobjects:
|
|
|
|
all_excluded = extract_mobject_family_members(excluded_mobjects)
|
|
|
|
mobjects = list_difference_update(mobjects, all_excluded)
|
|
|
|
return mobjects
|
|
|
|
|
2018-01-24 12:14:37 -08:00
|
|
|
def capture_mobject(self, mobject, **kwargs):
|
|
|
|
return self.capture_mobjects([mobject], **kwargs)
|
|
|
|
|
|
|
|
def capture_mobjects(self, mobjects, **kwargs):
|
|
|
|
mobjects = self.get_mobjects_to_display(mobjects, **kwargs)
|
2020-02-04 15:27:21 -08:00
|
|
|
shader_infos = list(it.chain(*[
|
|
|
|
mob.get_shader_info_list()
|
|
|
|
for mob in mobjects
|
|
|
|
]))
|
|
|
|
# TODO, batching works well when the mobjects are already organized,
|
|
|
|
# but can we somehow use z-buffering to better effect here?
|
|
|
|
batches = batch_by_property(shader_infos, self.get_shader_id)
|
|
|
|
for info_group, sid in batches:
|
|
|
|
shader = self.get_shader(sid)
|
2020-02-07 09:37:21 -08:00
|
|
|
data = np.hstack([info["data"] for info in info_group])
|
|
|
|
render_primative = info_group[0]["render_primative"]
|
|
|
|
self.render_from_shader(shader, data, render_primative)
|
2020-02-04 15:27:21 -08:00
|
|
|
|
|
|
|
# Shader stuff
|
|
|
|
def init_shaders(self):
|
|
|
|
self.id_to_shader = {}
|
|
|
|
|
|
|
|
def get_shader_id(self, shader_info):
|
|
|
|
# A unique id for a shader based on the names of the files holding its code
|
|
|
|
return "|".join([
|
|
|
|
shader_info.get(key, "")
|
|
|
|
for key in ["vert", "geom", "frag"]
|
|
|
|
])
|
2018-02-16 10:57:04 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def get_shader(self, sid):
|
|
|
|
if sid not in self.id_to_shader:
|
|
|
|
vert, geom, frag = sid.split("|")
|
|
|
|
shader = self.ctx.program(
|
|
|
|
vertex_shader=self.get_shader_code_from_file(vert),
|
|
|
|
geometry_shader=self.get_shader_code_from_file(geom),
|
|
|
|
fragment_shader=self.get_shader_code_from_file(frag),
|
2018-02-16 10:57:04 -08:00
|
|
|
)
|
2020-02-04 15:27:21 -08:00
|
|
|
self.set_shader_uniforms(shader)
|
|
|
|
self.id_to_shader[sid] = shader
|
|
|
|
return self.id_to_shader[sid]
|
2016-09-06 16:48:04 -07:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def get_shader_code_from_file(self, filename):
|
|
|
|
if len(filename) == 0:
|
|
|
|
return None
|
2018-02-10 18:37:34 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
filepath = os.path.join(SHADER_DIR, filename)
|
|
|
|
if not os.path.exists(filepath):
|
|
|
|
warnings.warn(f"No file at {file_path}")
|
|
|
|
return
|
2018-08-12 12:46:57 -07:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
with open(filepath, "r") as f:
|
|
|
|
result = f.read()
|
|
|
|
|
|
|
|
# To share functionality between shaders, some functions are read in
|
|
|
|
# from other files an inserted into the relevant strings before
|
|
|
|
# passing to ctx.program for compiling
|
|
|
|
# Replace "#INSERT " lines with relevant code
|
|
|
|
insertions = re.findall(r"^#INSERT .*\.glsl$", result, flags=re.MULTILINE)
|
|
|
|
for line in insertions:
|
|
|
|
inserted_code = self.get_shader_code_from_file(line.replace("#INSERT ", ""))
|
|
|
|
result = result.replace(line, inserted_code)
|
|
|
|
return result
|
|
|
|
|
|
|
|
def set_shader_uniforms(self, shader):
|
|
|
|
# TODO, think about how uniforms come from mobjects
|
|
|
|
# as well.
|
2018-08-10 15:12:49 -07:00
|
|
|
fw = self.get_frame_width()
|
|
|
|
fh = self.get_frame_height()
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
shader['scale'].value = fh / 2
|
|
|
|
shader['aspect_ratio'].value = fw / fh
|
|
|
|
shader['anti_alias_width'].value = ANTI_ALIAS_WIDTH
|
2016-02-23 22:29:32 -08:00
|
|
|
|
2020-02-07 09:37:21 -08:00
|
|
|
def render_from_shader(self, shader, data, render_primative):
|
2020-02-04 15:27:21 -08:00
|
|
|
vbo = shader.ctx.buffer(data.tobytes())
|
|
|
|
vao = shader.ctx.simple_vertex_array(shader, vbo, *data.dtype.names)
|
2020-02-07 09:37:21 -08:00
|
|
|
vao.render(render_primative)
|
2018-02-16 10:57:04 -08:00
|
|
|
|
2017-09-26 17:41:45 -07:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def get_vmob_shader(ctx, type):
|
|
|
|
vert_file = f"quadratic_bezier_{type}_vert.glsl"
|
|
|
|
geom_file = f"quadratic_bezier_{type}_geom.glsl"
|
|
|
|
frag_file = f"quadratic_bezier_{type}_frag.glsl"
|
2017-09-19 13:12:45 -07:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
shader = ctx.program(
|
|
|
|
vertex_shader=get_code_from_file(vert_file),
|
|
|
|
geometry_shader=get_code_from_file(geom_file),
|
|
|
|
fragment_shader=get_code_from_file(frag_file),
|
|
|
|
)
|
|
|
|
set_shader_uniforms(shader)
|
|
|
|
return shader
|
2018-02-16 12:15:16 -08:00
|
|
|
|
2018-02-20 12:33:58 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def get_stroke_shader(ctx):
|
|
|
|
return get_vmob_shader(ctx, "stroke")
|
2018-05-21 12:11:46 -07:00
|
|
|
|
2018-05-09 18:55:12 -07:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def get_fill_shader(ctx):
|
|
|
|
return get_vmob_shader(ctx, "fill")
|
2017-09-18 17:15:49 -07:00
|
|
|
|
2018-05-21 12:11:46 -07:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def render_vmob_stroke(shader, vmobs):
|
|
|
|
assert(len(vmobs) > 0)
|
|
|
|
data_arrays = [vmob.get_stroke_shader_data() for vmob in vmobs]
|
|
|
|
data = join_arrays(*data_arrays)
|
|
|
|
send_data_to_shader(shader, data)
|
2018-02-01 16:32:19 -08:00
|
|
|
|
2018-02-11 18:21:31 -08:00
|
|
|
|
2020-02-04 15:27:21 -08:00
|
|
|
def render_vmob_fill(shader, vmobs):
|
|
|
|
assert(len(vmobs) > 0)
|
|
|
|
data_arrays = [vmob.get_fill_shader_data() for vmob in vmobs]
|
|
|
|
data = join_arrays(*data_arrays)
|
|
|
|
send_data_to_shader(shader, data)
|