diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 1bd3c4be..6aac7a81 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -70,7 +70,6 @@ class Camera(object): self.init_context(window) 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 @@ -87,80 +86,13 @@ class Camera(object): self.ctx = window.ctx self.fbo = self.ctx.detect_framebuffer() self.fbo.use() - self.set_ctx_blending() self.ctx.enable(moderngl.PROGRAM_POINT_SIZE) + self.ctx.enable(moderngl.BLEND) # This is the frame buffer we'll draw into when emitting frames 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; - } - ''', - ) - - self.fill_prog['Texture'].value = get_texture_id(self.fill_texture) - - 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: - self.ctx.enable(moderngl.BLEND) - else: - self.ctx.disable(moderngl.BLEND) - - def set_ctx_depth_test(self, enable: bool = True) -> None: - if enable: - self.ctx.enable(moderngl.DEPTH_TEST) - else: - self.ctx.disable(moderngl.DEPTH_TEST) - - def set_ctx_clip_plane(self, enable: bool = True) -> None: - if enable: - gl.glEnable(gl.GL_CLIP_DISTANCE0) - def init_light_source(self) -> None: self.light_source = Point(self.light_source_position) @@ -287,39 +219,11 @@ class Camera(object): def render(self, render_group: dict[str, Any]) -> None: shader_wrapper = render_group["shader_wrapper"] - primitive = int(shader_wrapper.render_primitive) - shader_wrapper.update_program_uniforms(self.perspective_uniforms) - self.set_ctx_depth_test(shader_wrapper.depth_test) - self.set_ctx_clip_plane(shader_wrapper.use_clip_plane()) - - if shader_wrapper.is_fill: - self.render_fill(render_group["vao"], primitive, shader_wrapper.vert_indices) - else: - render_group["vao"].render(primitive) + shader_wrapper.render(self.perspective_uniforms) 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) @@ -341,9 +245,9 @@ class Camera(object): shader_wrapper: ShaderWrapper, single_use: bool = True ) -> dict[str, Any]: + shader_wrapper.get_vao() return { "shader_wrapper": shader_wrapper, - "vao": shader_wrapper.get_vao(single_use), "single_use": single_use, } diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index f5de726d..69bdd88c 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1841,7 +1841,7 @@ class Mobject(object): def init_shader_data(self, ctx: Context): self.shader_indices = np.zeros(0) self.shader_wrapper = ShaderWrapper( - context=ctx, + ctx=ctx, vert_data=self.data, shader_folder=self.shader_folder, texture_paths=self.texture_paths, diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index d9cb4d2c..c04577ed 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -40,6 +40,7 @@ from manimlib.utils.space_ops import midpoint from manimlib.utils.space_ops import normalize_along_axis from manimlib.utils.space_ops import z_to_vector from manimlib.shader_wrapper import ShaderWrapper +from manimlib.shader_wrapper import FillShaderWrapper from typing import TYPE_CHECKING @@ -1176,16 +1177,15 @@ class VMobject(Mobject): ) fill_data = np.zeros(0, dtype=fill_dtype) stroke_data = np.zeros(0, dtype=stroke_dtype) - self.fill_shader_wrapper = ShaderWrapper( - context=ctx, + self.fill_shader_wrapper = FillShaderWrapper( + ctx=ctx, vert_data=fill_data, uniforms=self.uniforms, shader_folder=self.fill_shader_folder, render_primitive=self.fill_render_primitive, - is_fill=True, ) self.stroke_shader_wrapper = ShaderWrapper( - context=ctx, + ctx=ctx, vert_data=stroke_data, uniforms=self.uniforms, shader_folder=self.stroke_shader_folder, diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 8f02a977..ce95c090 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -4,9 +4,12 @@ import copy import os import re +import OpenGL.GL as gl import moderngl import numpy as np +from manimlib.constants import DEFAULT_PIXEL_HEIGHT +from manimlib.constants import DEFAULT_PIXEL_WIDTH from manimlib.utils.iterables import resize_array from manimlib.utils.shaders import get_shader_code_from_file from manimlib.utils.shaders import get_shader_program @@ -30,7 +33,7 @@ if TYPE_CHECKING: class ShaderWrapper(object): def __init__( self, - context: moderngl.context.Context, + ctx: moderngl.context.Context, vert_data: np.ndarray, vert_indices: Optional[np.ndarray] = None, shader_folder: Optional[str] = None, @@ -38,17 +41,15 @@ class ShaderWrapper(object): texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures. depth_test: bool = False, render_primitive: int = moderngl.TRIANGLE_STRIP, - is_fill: bool = False, ): - self.ctx = context + self.ctx = ctx self.vert_data = vert_data self.vert_indices = (vert_indices or np.zeros(0)).astype(int) self.vert_attributes = vert_data.dtype.names self.shader_folder = shader_folder self.uniforms = uniforms or dict() self.depth_test = depth_test - self.render_primitive = str(render_primitive) - self.is_fill = is_fill + self.render_primitive = render_primitive self.vbo = None self.ibo = None @@ -99,6 +100,9 @@ class ShaderWrapper(object): self.render_primitive == shader_wrapper.render_primitive, )) + def __del__(self): + self.release() + def copy(self): result = copy.copy(self) result.vert_data = self.vert_data.copy() @@ -148,11 +152,33 @@ class ShaderWrapper(object): self.init_program() self.refresh_id() + # Changing context def use_clip_plane(self): if "clip_plane" not in self.uniforms: return False return any(self.uniforms["clip_plane"]) + def set_ctx_depth_test(self, enable: bool = True) -> None: + if enable: + self.ctx.enable(moderngl.DEPTH_TEST) + else: + self.ctx.disable(moderngl.DEPTH_TEST) + + def set_ctx_clip_plane(self, enable: bool = True) -> None: + if enable: + gl.glEnable(gl.GL_CLIP_DISTANCE0) + + + # Related to data and rendering + def render(self, camera_uniforms: dict): + self.update_program_uniforms(camera_uniforms) + self.set_ctx_depth_test(self.depth_test) + self.set_ctx_clip_plane(self.use_clip_plane()) + + # TODO, generate on the fly? + assert(self.vao is not None) + self.vao.render(self.render_primitive) + def combine_with(self, *shader_wrappers: ShaderWrapper) -> ShaderWrapper: if len(shader_wrappers) > 0: data_list = [self.vert_data, *(sw.vert_data for sw in shader_wrappers)] @@ -204,30 +230,25 @@ class ShaderWrapper(object): value = tuple(value) self.program[name].value = value - def get_vao(self, single_use: bool = False): - # Data buffer - vert_data = self.vert_data - indices = self.vert_indices - if len(indices) == 0: - self.ibo = None - elif single_use or self.is_fill: - self.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] - self.ibo = None - self.vbo = self.ctx.buffer(vert_data) + def get_vertex_buffer_object(self, refresh: bool = True): + if refresh: + self.vbo = self.ctx.buffer(self.vert_data) + return self.vbo + def get_index_buffer_object(self, refresh: bool = True): + if refresh and len(self.vert_indices) > 0: + self.ibo = self.ctx.buffer(self.vert_indices.astype(np.uint32)) + return self.ibo + + def get_vao(self, refresh: bool = True): + # Data buffer + vbo = self.get_vertex_buffer_object(refresh) + ibo = self.get_index_buffer_object(refresh) # Vertex array object self.vao = self.ctx.vertex_array( program=self.program, - content=[(self.vbo, self.vert_format, *self.vert_attributes)], - index_buffer=self.ibo, + content=[(vbo, self.vert_format, *self.vert_attributes)], + index_buffer=ibo, ) return self.vao @@ -238,3 +259,83 @@ class ShaderWrapper(object): self.vbo = None self.ibo = None self.vao = None + + +class FillShaderWrapper(ShaderWrapper): + def __init__( + self, + ctx: moderngl.context.Context, + *args, + **kwargs + ): + super().__init__(ctx, *args, **kwargs) + + size = (2 * DEFAULT_PIXEL_WIDTH, 2 * DEFAULT_PIXEL_HEIGHT) + 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; + } + ''', + ) + + self.fill_prog['Texture'].value = get_texture_id(self.fill_texture) + + 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 render(self, camera_uniforms: dict): + # TODO, these are copied... + self.update_program_uniforms(camera_uniforms) + self.set_ctx_depth_test(self.depth_test) + self.set_ctx_clip_plane(self.use_clip_plane()) + # + vao = self.vao + assert(vao is not None) + winding = (len(self.vert_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(self.render_primitive) + self.ctx.blend_func = moderngl.DEFAULT_BLENDING + self.ctx.screen.use() + self.fill_texture_vao.render(moderngl.TRIANGLE_STRIP)