Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] master from 3b1b:master #25

Merged
merged 38 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8ce5dc7
Add DieFace to drawings.py
3b1b Jan 23, 2023
1d4fcf0
Refer directly to fbo viewports in get_raw_fbo_data
3b1b Jan 23, 2023
e8b7594
Get rid of pixel_width and pixel_height attrs on Camera
3b1b Jan 23, 2023
8d729ee
Rename perspective to view_matrix
3b1b Jan 23, 2023
03080a1
Small style tweaks
3b1b Jan 23, 2023
c13495d
Blit based on window's viewport, when in preview mode
3b1b Jan 24, 2023
b0cca9e
Camera pixel_shape should reflect the drawn fbo
3b1b Jan 24, 2023
e2421a6
Don't disable clip plane
3b1b Jan 24, 2023
8a6deb4
Enable recording during a Scene embed
3b1b Jan 24, 2023
1dda706
Small cleanup
3b1b Jan 24, 2023
b99b88f
Update Scene.get_image to resize window if needed
3b1b Jan 24, 2023
b1f0270
Change threshold for bevel reduction
3b1b Jan 24, 2023
97789ff
Swap buffers when resetting to default position
3b1b Jan 24, 2023
d01658b
Fix multi-color setting
3b1b Jan 24, 2023
4774d2b
First pass at a winding-based fill approach
3b1b Jan 24, 2023
6e56c31
Use gl_InstanceID instead of hacking triangle_strip
3b1b Jan 24, 2023
87afdac
Small clean up
3b1b Jan 24, 2023
aa6c321
Change InteractiveScene dot config
3b1b Jan 24, 2023
945aa97
Fix aligned subpaths bug
3b1b Jan 24, 2023
88ed1a2
Have init_fill_fbo take in ctx as an argument
3b1b Jan 24, 2023
b93e284
In aligning families, scale inserted submobjects to 0
3b1b Jan 24, 2023
516fe91
Small tweaks
3b1b Jan 24, 2023
72da978
Use null array for vert indices in place of None
3b1b Jan 24, 2023
e9c70db
Ensure vert_indices are always of type int
3b1b Jan 25, 2023
f0df5c7
Make winding fill optional, and make winding additive rather than tog…
3b1b Jan 25, 2023
98eccab
Ensure background rectangle matches orientation
3b1b Jan 25, 2023
307487e
Don't pre-normalize joint_products
3b1b Jan 25, 2023
088a2f6
Misc. clean up
3b1b Jan 25, 2023
346d252
Don't save triangulation, but do orient svg paths positively
3b1b Jan 25, 2023
0e2d21b
Don't necessarily use VGroup with FadeTransform
3b1b Jan 25, 2023
272925f
Change winding fill blend_func
3b1b Jan 25, 2023
6cf8c8d
Do refresh in pointwise_become_partial
3b1b Jan 25, 2023
4cb9c9c
Remove unnecessary normalize
3b1b Jan 25, 2023
93dd9f6
Ensure align_family works well with VMobject fill
3b1b Jan 25, 2023
7deaf4c
Small clean up
3b1b Jan 25, 2023
0ea91f2
Merge pull request #1971 from 3b1b/video-work
3b1b Jan 25, 2023
bc5c78d
Add winding fill to VMobject args
3b1b Jan 25, 2023
a9a3ca0
Merge pull request #1972 from 3b1b/winding-fill
3b1b Jan 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions manimlib/animation/fading.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,8 @@ def __init__(
self.stretch = stretch
self.dim_to_match = dim_to_match

group_type = Group
if isinstance(mobject, VMobject) and isinstance(target_mobject, VMobject):
group_type = VGroup

mobject.save_state()
super().__init__(group_type(mobject, target_mobject.copy()), **kwargs)
super().__init__(Group(mobject, target_mobject.copy()), **kwargs)

def begin(self) -> None:
self.ending_mobject = self.mobject.copy()
Expand Down
195 changes: 138 additions & 57 deletions manimlib/camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
if TYPE_CHECKING:
from manimlib.shader_wrapper import ShaderWrapper
from manimlib.typing import ManimColor, Vect3
from manimlib.window import Window
from typing import Any, Iterable

class CameraFrame(Mobject):
Expand All @@ -39,7 +40,7 @@ def __init__(
self.frame_shape = frame_shape
self.center_point = center_point
self.focal_dist_to_height = focal_dist_to_height
self.perspective_transform = np.identity(4)
self.view_matrix = np.identity(4)
super().__init__(**kwargs)

def init_uniforms(self) -> None:
Expand Down Expand Up @@ -83,12 +84,12 @@ def get_gamma(self):
def get_inverse_camera_rotation_matrix(self):
return self.get_orientation().as_matrix().T

def get_perspective_transform(self):
def get_view_matrix(self):
"""
Returns a 4x4 for the affine transformation mapping a point
into the camera's internal coordinate system
"""
result = self.perspective_transform
result = self.view_matrix
result[:] = np.identity(4)
result[:3, 3] = -self.get_center()
rotation = np.identity(4)
Expand Down Expand Up @@ -187,7 +188,7 @@ def get_implied_camera_location(self) -> np.ndarray:
class Camera(object):
def __init__(
self,
ctx: moderngl.Context | None = None,
window: Window | None = None,
background_image: str | None = None,
frame_config: dict = dict(),
pixel_width: int = DEFAULT_PIXEL_WIDTH,
Expand All @@ -209,8 +210,8 @@ def __init__(
samples: int = 0,
):
self.background_image = background_image
self.pixel_width = pixel_width
self.pixel_height = pixel_height
self.window = window
self.default_pixel_shape = (pixel_width, pixel_height)
self.fps = fps
self.max_allowable_norm = max_allowable_norm
self.image_mode = image_mode
Expand All @@ -225,11 +226,12 @@ def __init__(
))
self.perspective_uniforms = dict()
self.init_frame(**frame_config)
self.init_context(ctx)
self.init_context(window)
self.init_shaders()
self.init_textures()
self.init_light_source()
self.refresh_perspective_uniforms()
self.init_fill_fbo(self.ctx) # Experimental
# A cached map from mobjects to their associated list of render groups
# so that these render groups are not regenerated unnecessarily for static
# mobjects
Expand All @@ -238,21 +240,73 @@ def __init__(
def init_frame(self, **config) -> None:
self.frame = CameraFrame(**config)

def init_context(self, ctx: moderngl.Context | None = None) -> None:
if ctx is None:
ctx = moderngl.create_standalone_context()
fbo = self.get_fbo(ctx, self.samples)
def init_context(self, window: Window | None = None) -> None:
if window is None:
self.ctx = moderngl.create_standalone_context()
self.fbo = self.get_fbo(self.samples)
else:
fbo = ctx.detect_framebuffer()
self.ctx = ctx
self.fbo = fbo
self.ctx = window.ctx
self.fbo = self.ctx.detect_framebuffer()
self.fbo.use()
self.set_ctx_blending()

self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)

# This is the frame buffer we'll draw into when emitting frames
self.draw_fbo = self.get_fbo(ctx, 0)
self.draw_fbo = self.get_fbo(samples=0)

def init_fill_fbo(self, ctx: moderngl.context.Context):
# Experimental
size = self.get_pixel_shape()
self.fill_texture = ctx.texture(
size=size,
components=4,
# Important to make sure floating point (not fixed point) is
# used so that alpha values are not clipped
dtype='f2',
)
# TODO, depth buffer is not really used yet
fill_depth = ctx.depth_renderbuffer(size)
self.fill_fbo = ctx.framebuffer(self.fill_texture, fill_depth)
self.fill_prog = ctx.program(
vertex_shader='''
#version 330

in vec2 texcoord;
out vec2 v_textcoord;

void main() {
gl_Position = vec4((2.0 * texcoord - 1.0), 0.0, 1.0);
v_textcoord = texcoord;
}
''',
fragment_shader='''
#version 330

uniform sampler2D Texture;

in vec2 v_textcoord;
out vec4 frag_color;

void main() {
frag_color = texture(Texture, v_textcoord);
frag_color = abs(frag_color);
if(frag_color.a == 0) discard;
//TODO, set gl_FragDepth;
}
''',
)

tid = self.n_textures
self.fill_texture.use(tid)
self.fill_prog['Texture'].value = tid
self.n_textures += 1
verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
self.fill_texture_vao = ctx.simple_vertex_array(
self.fill_prog,
ctx.buffer(verts.astype('f4').tobytes()),
'texcoord',
)

def set_ctx_blending(self, enable: bool = True) -> None:
if enable:
Expand All @@ -269,46 +323,43 @@ def set_ctx_depth_test(self, enable: bool = True) -> None:
def set_ctx_clip_plane(self, enable: bool = True) -> None:
if enable:
gl.glEnable(gl.GL_CLIP_DISTANCE0)
else:
gl.glDisable(gl.GL_CLIP_DISTANCE0)

def init_light_source(self) -> None:
self.light_source = Point(self.light_source_position)

# Methods associated with the frame buffer
def get_fbo(
self,
ctx: moderngl.Context,
samples: int = 0
) -> moderngl.Framebuffer:
pw = self.pixel_width
ph = self.pixel_height
return ctx.framebuffer(
color_attachments=ctx.texture(
(pw, ph),
return self.ctx.framebuffer(
color_attachments=self.ctx.texture(
self.default_pixel_shape,
components=self.n_channels,
samples=samples,
),
depth_attachment=ctx.depth_renderbuffer(
(pw, ph),
depth_attachment=self.ctx.depth_renderbuffer(
self.default_pixel_shape,
samples=samples
)
)

def clear(self) -> None:
self.fbo.clear(*self.background_rgba)

def reset_pixel_shape(self, new_width: int, new_height: int) -> None:
self.pixel_width = new_width
self.pixel_height = new_height
self.refresh_perspective_uniforms()

def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes:
# Copy blocks from fbo into draw_fbo using Blit
pw, ph = (self.pixel_width, self.pixel_height)
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo.glo)
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.draw_fbo.glo)
gl.glBlitFramebuffer(0, 0, pw, ph, 0, 0, pw, ph, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR)
if self.window is not None:
src_viewport = self.window.viewport
else:
src_viewport = self.fbo.viewport
gl.glBlitFramebuffer(
*src_viewport,
*self.draw_fbo.viewport,
gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR
)
return self.draw_fbo.read(
viewport=self.draw_fbo.viewport,
components=self.n_channels,
Expand All @@ -326,7 +377,7 @@ def get_image(self) -> Image.Image:
def get_pixel_array(self) -> np.ndarray:
raw = self.get_raw_fbo_data(dtype='f4')
flat_arr = np.frombuffer(raw, dtype='f4')
arr = flat_arr.reshape([*reversed(self.fbo.size), self.n_channels])
arr = flat_arr.reshape([*reversed(self.draw_fbo.size), self.n_channels])
arr = arr[::-1]
# Convert from float
return (self.rgb_max_val * arr).astype(self.pixel_array_dtype)
Expand All @@ -342,16 +393,22 @@ def get_texture(self) -> moderngl.Texture:
return texture

# Getting camera attributes
def get_pixel_size(self) -> float:
return self.frame.get_shape()[0] / self.get_pixel_shape()[0]

def get_pixel_shape(self) -> tuple[int, int]:
return self.fbo.viewport[2:4]
# return (self.pixel_width, self.pixel_height)
return self.draw_fbo.size

def get_pixel_width(self) -> int:
return self.get_pixel_shape()[0]

def get_pixel_height(self) -> int:
return self.get_pixel_shape()[1]

def get_aspect_ratio(self):
pw, ph = self.get_pixel_shape()
return pw / ph

def get_frame_height(self) -> float:
return self.frame.get_height()

Expand All @@ -374,17 +431,15 @@ def resize_frame_shape(self, fixed_dimension: bool = False) -> None:
whether frame_height or frame_width
remains fixed while the other changes accordingly.
"""
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)
aspect_ratio = self.get_aspect_ratio()
if not fixed_dimension:
frame_height = frame_width / aspect_ratio
else:
frame_width = aspect_ratio * frame_height
self.frame.set_height(frame_height)
self.frame.set_width(frame_width)
self.frame.set_height(frame_height, stretch=true)
self.frame.set_width(frame_width, stretch=true)

# Rendering
def capture(self, *mobjects: Mobject) -> None:
Expand All @@ -396,13 +451,39 @@ def capture(self, *mobjects: Mobject) -> None:
def render(self, render_group: dict[str, Any]) -> None:
shader_wrapper = render_group["shader_wrapper"]
shader_program = render_group["prog"]
primitive = int(shader_wrapper.render_primitive)
self.set_shader_uniforms(shader_program, shader_wrapper)
self.set_ctx_depth_test(shader_wrapper.depth_test)
self.set_ctx_clip_plane(shader_wrapper.use_clip_plane)
render_group["vao"].render(int(shader_wrapper.render_primitive))

if shader_wrapper.is_fill:
self.render_fill(render_group["vao"], primitive, shader_wrapper.vert_indices)
else:
render_group["vao"].render(primitive)

if render_group["single_use"]:
self.release_render_group(render_group)

def render_fill(self, vao, render_primitive: int, indices: np.ndarray):
"""
VMobject fill is handled in a special way, where emited triangles
must be blended with moderngl.FUNC_SUBTRACT so as to effectively compute
a winding number around each pixel. This is rendered to a separate texture,
then that texture is overlayed onto the current fbo
"""
winding = (len(indices) == 0)
vao.program['winding'].value = winding
if not winding:
vao.render(moderngl.TRIANGLES)
return
self.fill_fbo.clear()
self.fill_fbo.use()
self.ctx.blend_func = (moderngl.ONE, moderngl.ONE)
vao.render(render_primitive)
self.ctx.blend_func = moderngl.DEFAULT_BLENDING
self.fbo.use()
self.fill_texture_vao.render(moderngl.TRIANGLE_STRIP)

def get_render_group_list(self, mobject: Mobject) -> Iterable[dict[str, Any]]:
if mobject.is_changing():
return self.generate_render_group_list(mobject)
Expand All @@ -427,26 +508,28 @@ def get_render_group(
# Data buffer
vert_data = shader_wrapper.vert_data
indices = shader_wrapper.vert_indices
if indices is None:
if len(indices) == 0:
ibo = None
elif single_use:
ibo = self.ctx.buffer(indices.astype(np.uint32))
else:
# The vao.render call is strangely longer
# when an index buffer is used, so if the
# mobject is not changing, meaning only its
# uniforms are being updated, just create
# a larger data array based on the indices
# and don't bother with the ibo
vert_data = vert_data[indices]
ibo = None
ibo = self.ctx.buffer(indices.astype(np.uint32))
# # The vao.render call is strangely longer
# # when an index buffer is used, so if the
# # mobject is not changing, meaning only its
# # uniforms are being updated, just create
# # a larger data array based on the indices
# # and don't bother with the ibo
# vert_data = vert_data[indices]
# ibo = None
vbo = self.ctx.buffer(vert_data)

# Program and vertex array
shader_program, vert_format = self.get_shader_program(shader_wrapper)
attributes = shader_wrapper.vert_attributes
vao = self.ctx.vertex_array(
program=shader_program,
content=[(vbo, vert_format, *shader_wrapper.vert_attributes)],
content=[(vbo, vert_format, *attributes)],
index_buffer=ibo,
)
return {
Expand Down Expand Up @@ -502,16 +585,14 @@ def set_shader_uniforms(

def refresh_perspective_uniforms(self) -> None:
frame = self.frame
# Orient light
perspective_transform = frame.get_perspective_transform()
view_matrix = frame.get_view_matrix()
light_pos = self.light_source.get_location()
cam_pos = self.frame.get_implied_camera_location()
frame_shape = frame.get_shape()

self.perspective_uniforms.update(
frame_shape=frame_shape,
pixel_size=frame_shape[0] / self.get_pixel_shape()[0],
perspective=tuple(perspective_transform.T.flatten()),
frame_shape=frame.get_shape(),
pixel_size=self.get_pixel_size(),
view=tuple(view_matrix.T.flatten()),
camera_position=tuple(cam_pos),
light_position=tuple(light_pos),
focal_distance=frame.get_focal_distance(),
Expand Down
7 changes: 4 additions & 3 deletions manimlib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,10 @@ def get_window_config(args: Namespace, custom_config: dict, camera_config: dict)
if not (args.full_screen or custom_config["full_screen"]):
window_width //= 2
window_height = int(window_width / aspect_ratio)
return {
"size": (window_width, window_height),
}
return dict(
full_size=(camera_config["pixel_width"], camera_config["pixel_height"]),
size=(window_width, window_height),
)


def get_camera_config(args: Namespace, custom_config: dict) -> dict:
Expand Down
Loading