Move most of rendering logic to ShaderWrapper

This commit is contained in:
Grant Sanderson 2023-01-25 13:45:18 -08:00
parent c94d8fd3b0
commit 2c737ed540
4 changed files with 134 additions and 129 deletions

View file

@ -70,7 +70,6 @@ class Camera(object):
self.init_context(window) self.init_context(window)
self.init_light_source() self.init_light_source()
self.refresh_perspective_uniforms() self.refresh_perspective_uniforms()
self.init_fill_fbo(self.ctx) # Experimental
# A cached map from mobjects to their associated list of render groups # A cached map from mobjects to their associated list of render groups
# so that these render groups are not regenerated unnecessarily for static # so that these render groups are not regenerated unnecessarily for static
# mobjects # mobjects
@ -87,80 +86,13 @@ class Camera(object):
self.ctx = window.ctx self.ctx = window.ctx
self.fbo = self.ctx.detect_framebuffer() self.fbo = self.ctx.detect_framebuffer()
self.fbo.use() self.fbo.use()
self.set_ctx_blending()
self.ctx.enable(moderngl.PROGRAM_POINT_SIZE) self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)
self.ctx.enable(moderngl.BLEND)
# This is the frame buffer we'll draw into when emitting frames # This is the frame buffer we'll draw into when emitting frames
self.draw_fbo = self.get_fbo(samples=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;
}
''',
)
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: def init_light_source(self) -> None:
self.light_source = Point(self.light_source_position) self.light_source = Point(self.light_source_position)
@ -287,39 +219,11 @@ class Camera(object):
def render(self, render_group: dict[str, Any]) -> None: def render(self, render_group: dict[str, Any]) -> None:
shader_wrapper = render_group["shader_wrapper"] shader_wrapper = render_group["shader_wrapper"]
primitive = int(shader_wrapper.render_primitive) shader_wrapper.render(self.perspective_uniforms)
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)
if render_group["single_use"]: if render_group["single_use"]:
self.release_render_group(render_group) 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]]: def get_render_group_list(self, mobject: Mobject) -> Iterable[dict[str, Any]]:
if mobject.is_changing(): if mobject.is_changing():
return self.generate_render_group_list(mobject) return self.generate_render_group_list(mobject)
@ -341,9 +245,9 @@ class Camera(object):
shader_wrapper: ShaderWrapper, shader_wrapper: ShaderWrapper,
single_use: bool = True single_use: bool = True
) -> dict[str, Any]: ) -> dict[str, Any]:
shader_wrapper.get_vao()
return { return {
"shader_wrapper": shader_wrapper, "shader_wrapper": shader_wrapper,
"vao": shader_wrapper.get_vao(single_use),
"single_use": single_use, "single_use": single_use,
} }

View file

@ -1841,7 +1841,7 @@ class Mobject(object):
def init_shader_data(self, ctx: Context): def init_shader_data(self, ctx: Context):
self.shader_indices = np.zeros(0) self.shader_indices = np.zeros(0)
self.shader_wrapper = ShaderWrapper( self.shader_wrapper = ShaderWrapper(
context=ctx, ctx=ctx,
vert_data=self.data, vert_data=self.data,
shader_folder=self.shader_folder, shader_folder=self.shader_folder,
texture_paths=self.texture_paths, texture_paths=self.texture_paths,

View file

@ -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 normalize_along_axis
from manimlib.utils.space_ops import z_to_vector from manimlib.utils.space_ops import z_to_vector
from manimlib.shader_wrapper import ShaderWrapper from manimlib.shader_wrapper import ShaderWrapper
from manimlib.shader_wrapper import FillShaderWrapper
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -1176,16 +1177,15 @@ class VMobject(Mobject):
) )
fill_data = np.zeros(0, dtype=fill_dtype) fill_data = np.zeros(0, dtype=fill_dtype)
stroke_data = np.zeros(0, dtype=stroke_dtype) stroke_data = np.zeros(0, dtype=stroke_dtype)
self.fill_shader_wrapper = ShaderWrapper( self.fill_shader_wrapper = FillShaderWrapper(
context=ctx, ctx=ctx,
vert_data=fill_data, vert_data=fill_data,
uniforms=self.uniforms, uniforms=self.uniforms,
shader_folder=self.fill_shader_folder, shader_folder=self.fill_shader_folder,
render_primitive=self.fill_render_primitive, render_primitive=self.fill_render_primitive,
is_fill=True,
) )
self.stroke_shader_wrapper = ShaderWrapper( self.stroke_shader_wrapper = ShaderWrapper(
context=ctx, ctx=ctx,
vert_data=stroke_data, vert_data=stroke_data,
uniforms=self.uniforms, uniforms=self.uniforms,
shader_folder=self.stroke_shader_folder, shader_folder=self.stroke_shader_folder,

View file

@ -4,9 +4,12 @@ import copy
import os import os
import re import re
import OpenGL.GL as gl
import moderngl import moderngl
import numpy as np 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.iterables import resize_array
from manimlib.utils.shaders import get_shader_code_from_file from manimlib.utils.shaders import get_shader_code_from_file
from manimlib.utils.shaders import get_shader_program from manimlib.utils.shaders import get_shader_program
@ -30,7 +33,7 @@ if TYPE_CHECKING:
class ShaderWrapper(object): class ShaderWrapper(object):
def __init__( def __init__(
self, self,
context: moderngl.context.Context, ctx: moderngl.context.Context,
vert_data: np.ndarray, vert_data: np.ndarray,
vert_indices: Optional[np.ndarray] = None, vert_indices: Optional[np.ndarray] = None,
shader_folder: Optional[str] = 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. texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures.
depth_test: bool = False, depth_test: bool = False,
render_primitive: int = moderngl.TRIANGLE_STRIP, render_primitive: int = moderngl.TRIANGLE_STRIP,
is_fill: bool = False,
): ):
self.ctx = context self.ctx = ctx
self.vert_data = vert_data self.vert_data = vert_data
self.vert_indices = (vert_indices or np.zeros(0)).astype(int) self.vert_indices = (vert_indices or np.zeros(0)).astype(int)
self.vert_attributes = vert_data.dtype.names self.vert_attributes = vert_data.dtype.names
self.shader_folder = shader_folder self.shader_folder = shader_folder
self.uniforms = uniforms or dict() self.uniforms = uniforms or dict()
self.depth_test = depth_test self.depth_test = depth_test
self.render_primitive = str(render_primitive) self.render_primitive = render_primitive
self.is_fill = is_fill
self.vbo = None self.vbo = None
self.ibo = None self.ibo = None
@ -99,6 +100,9 @@ class ShaderWrapper(object):
self.render_primitive == shader_wrapper.render_primitive, self.render_primitive == shader_wrapper.render_primitive,
)) ))
def __del__(self):
self.release()
def copy(self): def copy(self):
result = copy.copy(self) result = copy.copy(self)
result.vert_data = self.vert_data.copy() result.vert_data = self.vert_data.copy()
@ -148,11 +152,33 @@ class ShaderWrapper(object):
self.init_program() self.init_program()
self.refresh_id() self.refresh_id()
# Changing context
def use_clip_plane(self): def use_clip_plane(self):
if "clip_plane" not in self.uniforms: if "clip_plane" not in self.uniforms:
return False return False
return any(self.uniforms["clip_plane"]) 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: def combine_with(self, *shader_wrappers: ShaderWrapper) -> ShaderWrapper:
if len(shader_wrappers) > 0: if len(shader_wrappers) > 0:
data_list = [self.vert_data, *(sw.vert_data for sw in shader_wrappers)] data_list = [self.vert_data, *(sw.vert_data for sw in shader_wrappers)]
@ -204,30 +230,25 @@ class ShaderWrapper(object):
value = tuple(value) value = tuple(value)
self.program[name].value = value self.program[name].value = value
def get_vao(self, single_use: bool = False): def get_vertex_buffer_object(self, refresh: bool = True):
# Data buffer if refresh:
vert_data = self.vert_data self.vbo = self.ctx.buffer(self.vert_data)
indices = self.vert_indices return self.vbo
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_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 # Vertex array object
self.vao = self.ctx.vertex_array( self.vao = self.ctx.vertex_array(
program=self.program, program=self.program,
content=[(self.vbo, self.vert_format, *self.vert_attributes)], content=[(vbo, self.vert_format, *self.vert_attributes)],
index_buffer=self.ibo, index_buffer=ibo,
) )
return self.vao return self.vao
@ -238,3 +259,83 @@ class ShaderWrapper(object):
self.vbo = None self.vbo = None
self.ibo = None self.ibo = None
self.vao = 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)