2022-02-15 14:49:02 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-04-12 19:19:59 +08:00
|
|
|
import copy
|
2020-02-17 12:14:40 -08:00
|
|
|
import os
|
|
|
|
import re
|
2022-02-15 14:49:02 +08:00
|
|
|
|
2023-01-25 13:45:18 -08:00
|
|
|
import OpenGL.GL as gl
|
2020-02-17 12:14:40 -08:00
|
|
|
import moderngl
|
2020-06-29 18:17:18 -07:00
|
|
|
import numpy as np
|
2020-02-17 12:14:40 -08:00
|
|
|
|
2022-12-27 21:39:20 -08:00
|
|
|
from manimlib.utils.iterables import resize_array
|
2023-01-16 14:18:35 -08:00
|
|
|
from manimlib.utils.shaders import get_shader_code_from_file
|
2023-01-25 11:23:31 -08:00
|
|
|
from manimlib.utils.shaders import get_shader_program
|
2023-01-25 12:10:39 -08:00
|
|
|
from manimlib.utils.shaders import image_path_to_texture
|
|
|
|
from manimlib.utils.shaders import get_texture_id
|
2023-01-27 08:26:54 -08:00
|
|
|
from manimlib.utils.shaders import get_fill_canvas
|
2023-01-25 12:10:39 -08:00
|
|
|
from manimlib.utils.shaders import release_texture
|
2023-02-02 21:13:18 -08:00
|
|
|
from manimlib.utils.shaders import set_program_uniform
|
2020-02-17 12:14:40 -08:00
|
|
|
|
2022-04-12 19:19:59 +08:00
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
2023-01-26 20:01:59 -08:00
|
|
|
from typing import List, Optional, Dict
|
|
|
|
from manimlib.typing import UniformDict
|
2022-04-12 19:19:59 +08:00
|
|
|
|
|
|
|
|
2020-02-17 12:14:40 -08:00
|
|
|
# Mobjects that should be rendered with
|
|
|
|
# the same shader will be organized and
|
|
|
|
# clumped together based on keeping track
|
|
|
|
# of a dict holding all the relevant information
|
|
|
|
# to that shader
|
|
|
|
|
|
|
|
|
2020-06-29 18:17:18 -07:00
|
|
|
class ShaderWrapper(object):
|
2022-02-15 14:49:02 +08:00
|
|
|
def __init__(
|
|
|
|
self,
|
2023-01-25 13:45:18 -08:00
|
|
|
ctx: moderngl.context.Context,
|
2022-12-27 14:53:55 -08:00
|
|
|
vert_data: np.ndarray,
|
2023-01-25 12:10:39 -08:00
|
|
|
shader_folder: Optional[str] = None,
|
2023-02-02 17:45:52 -08:00
|
|
|
mobject_uniforms: Optional[UniformDict] = None, # A dictionary mapping names of uniform variables
|
2023-01-25 12:10:39 -08:00
|
|
|
texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures.
|
2022-02-15 14:49:02 +08:00
|
|
|
depth_test: bool = False,
|
|
|
|
render_primitive: int = moderngl.TRIANGLE_STRIP,
|
|
|
|
):
|
2023-01-25 13:45:18 -08:00
|
|
|
self.ctx = ctx
|
2020-06-29 18:17:18 -07:00
|
|
|
self.vert_data = vert_data
|
|
|
|
self.vert_attributes = vert_data.dtype.names
|
2021-01-05 23:14:16 -08:00
|
|
|
self.shader_folder = shader_folder
|
2020-06-29 18:17:18 -07:00
|
|
|
self.depth_test = depth_test
|
2023-01-25 13:45:18 -08:00
|
|
|
self.render_primitive = render_primitive
|
2023-01-25 12:10:39 -08:00
|
|
|
|
2023-02-02 17:45:52 -08:00
|
|
|
self.program_uniform_mirror: UniformDict = dict()
|
2023-02-04 16:49:32 -08:00
|
|
|
self.bind_to_mobject_uniforms(mobject_uniforms or dict())
|
2023-02-02 17:45:52 -08:00
|
|
|
|
2021-01-09 18:52:54 -08:00
|
|
|
self.init_program_code()
|
2023-01-25 11:23:31 -08:00
|
|
|
self.init_program()
|
2023-06-10 09:25:44 -07:00
|
|
|
self.texture_names_to_ids = dict()
|
2023-01-25 12:10:39 -08:00
|
|
|
if texture_paths is not None:
|
|
|
|
self.init_textures(texture_paths)
|
2024-08-19 08:05:32 -05:00
|
|
|
self.init_vertex_objects()
|
2021-01-09 18:52:54 -08:00
|
|
|
self.refresh_id()
|
2020-06-29 18:17:18 -07:00
|
|
|
|
2023-01-25 11:23:31 -08:00
|
|
|
def init_program_code(self) -> None:
|
|
|
|
def get_code(name: str) -> str | None:
|
|
|
|
return get_shader_code_from_file(
|
|
|
|
os.path.join(self.shader_folder, f"{name}.glsl")
|
|
|
|
)
|
|
|
|
|
|
|
|
self.program_code: dict[str, str | None] = {
|
|
|
|
"vertex_shader": get_code("vert"),
|
|
|
|
"geometry_shader": get_code("geom"),
|
|
|
|
"fragment_shader": get_code("frag"),
|
|
|
|
}
|
|
|
|
|
|
|
|
def init_program(self):
|
|
|
|
if not self.shader_folder:
|
|
|
|
self.program = None
|
|
|
|
self.vert_format = None
|
|
|
|
return
|
|
|
|
self.program = get_shader_program(self.ctx, **self.program_code)
|
|
|
|
self.vert_format = moderngl.detect_format(self.program, self.vert_attributes)
|
2024-08-19 08:05:32 -05:00
|
|
|
self.programs = [self.program]
|
2023-01-25 11:23:31 -08:00
|
|
|
|
2023-01-25 12:10:39 -08:00
|
|
|
def init_textures(self, texture_paths: dict[str, str]):
|
2023-06-10 09:25:44 -07:00
|
|
|
self.texture_names_to_ids = {
|
2023-01-27 19:27:23 -08:00
|
|
|
name: get_texture_id(image_path_to_texture(path, self.ctx))
|
|
|
|
for name, path in texture_paths.items()
|
|
|
|
}
|
2023-01-25 12:10:39 -08:00
|
|
|
|
2024-08-19 08:05:32 -05:00
|
|
|
def init_vertex_objects(self):
|
2023-01-27 10:01:37 -08:00
|
|
|
self.vbo = None
|
|
|
|
self.vao = None
|
|
|
|
|
2023-02-02 17:45:52 -08:00
|
|
|
def bind_to_mobject_uniforms(self, mobject_uniforms: UniformDict):
|
|
|
|
self.mobject_uniforms = mobject_uniforms
|
|
|
|
|
2022-04-23 18:51:03 -07:00
|
|
|
def __eq__(self, shader_wrapper: ShaderWrapper):
|
|
|
|
return all((
|
2024-08-19 08:05:32 -05:00
|
|
|
# np.all(self.vert_data == shader_wrapper.vert_data),
|
2022-04-23 18:51:03 -07:00
|
|
|
self.shader_folder == shader_wrapper.shader_folder,
|
|
|
|
all(
|
2023-02-02 17:45:52 -08:00
|
|
|
self.mobject_uniforms[key] == shader_wrapper.mobject_uniforms[key]
|
|
|
|
for key in self.mobject_uniforms
|
2022-04-23 18:51:03 -07:00
|
|
|
),
|
|
|
|
self.depth_test == shader_wrapper.depth_test,
|
|
|
|
self.render_primitive == shader_wrapper.render_primitive,
|
|
|
|
))
|
|
|
|
|
2020-06-29 18:17:18 -07:00
|
|
|
def copy(self):
|
|
|
|
result = copy.copy(self)
|
2023-01-25 16:43:47 -08:00
|
|
|
result.ctx = self.ctx
|
2023-01-24 15:53:43 -08:00
|
|
|
result.vert_data = self.vert_data.copy()
|
2024-08-19 08:05:32 -05:00
|
|
|
result.init_vertex_objects()
|
2020-06-29 18:17:18 -07:00
|
|
|
return result
|
|
|
|
|
2022-02-15 14:49:02 +08:00
|
|
|
def is_valid(self) -> bool:
|
2020-06-29 18:17:18 -07:00
|
|
|
return all([
|
|
|
|
self.vert_data is not None,
|
2021-01-09 18:52:54 -08:00
|
|
|
self.program_code["vertex_shader"] is not None,
|
|
|
|
self.program_code["fragment_shader"] is not None,
|
2020-06-29 18:17:18 -07:00
|
|
|
])
|
|
|
|
|
2022-02-15 14:49:02 +08:00
|
|
|
def get_id(self) -> str:
|
2020-06-29 18:17:18 -07:00
|
|
|
return self.id
|
|
|
|
|
2022-02-15 14:49:02 +08:00
|
|
|
def create_id(self) -> str:
|
2020-06-29 18:17:18 -07:00
|
|
|
# A unique id for a shader
|
2024-08-19 08:05:32 -05:00
|
|
|
program_id = hash("".join(map(str, self.program_code.values())))
|
2020-06-29 18:17:18 -07:00
|
|
|
return "|".join(map(str, [
|
2023-02-02 20:49:13 -08:00
|
|
|
program_id,
|
2023-02-02 17:45:52 -08:00
|
|
|
self.mobject_uniforms,
|
2020-06-29 18:17:18 -07:00
|
|
|
self.depth_test,
|
2020-12-04 08:09:02 -08:00
|
|
|
self.render_primitive,
|
2024-01-17 15:01:49 -06:00
|
|
|
self.texture_names_to_ids,
|
2020-06-29 18:17:18 -07:00
|
|
|
]))
|
|
|
|
|
2022-02-15 14:49:02 +08:00
|
|
|
def refresh_id(self) -> None:
|
2020-06-29 18:17:18 -07:00
|
|
|
self.id = self.create_id()
|
|
|
|
|
2022-02-15 14:49:02 +08:00
|
|
|
def replace_code(self, old: str, new: str) -> None:
|
2021-01-09 18:52:54 -08:00
|
|
|
code_map = self.program_code
|
2023-02-02 20:49:13 -08:00
|
|
|
for name in code_map:
|
2021-01-09 18:52:54 -08:00
|
|
|
if code_map[name] is None:
|
|
|
|
continue
|
|
|
|
code_map[name] = re.sub(old, new, code_map[name])
|
2023-01-25 11:23:31 -08:00
|
|
|
self.init_program()
|
2021-01-09 18:52:54 -08:00
|
|
|
self.refresh_id()
|
|
|
|
|
2023-01-25 13:45:18 -08:00
|
|
|
# Changing context
|
2023-01-25 10:31:05 -08:00
|
|
|
def use_clip_plane(self):
|
2023-02-02 17:45:52 -08:00
|
|
|
if "clip_plane" not in self.mobject_uniforms:
|
2023-01-25 10:31:05 -08:00
|
|
|
return False
|
2023-02-02 17:45:52 -08:00
|
|
|
return any(self.mobject_uniforms["clip_plane"])
|
2023-01-25 10:31:05 -08:00
|
|
|
|
2023-01-25 13:45:18 -08:00
|
|
|
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)
|
|
|
|
|
2023-01-25 14:13:56 -08:00
|
|
|
# Adding data
|
2023-01-25 13:45:18 -08:00
|
|
|
|
2022-12-27 21:39:20 -08:00
|
|
|
def combine_with(self, *shader_wrappers: ShaderWrapper) -> ShaderWrapper:
|
2022-12-29 12:04:35 -08:00
|
|
|
if len(shader_wrappers) > 0:
|
2024-08-19 08:05:32 -05:00
|
|
|
self.read_in([self.vert_data, (sw.vert_data for sw in shader_wrappers)])
|
|
|
|
vbos = [
|
|
|
|
vbo
|
|
|
|
for vbo in [self.vbo, *(sw.vbo for sw in shader_wrappers)]
|
|
|
|
if vbo is not None
|
|
|
|
]
|
|
|
|
total_size = sum(vbo.size for vbo in vbos)
|
|
|
|
new_vbo = self.ctx.buffer(reserve=total_size)
|
|
|
|
offset = 0
|
|
|
|
for vbo in vbos:
|
|
|
|
new_vbo.write(vbo.read(), offset=offset)
|
|
|
|
offset += vbo.size
|
|
|
|
self.vbo = new_vbo
|
2022-12-27 21:39:20 -08:00
|
|
|
return self
|
|
|
|
|
2023-01-15 20:27:19 -08:00
|
|
|
def read_in(
|
|
|
|
self,
|
2024-08-19 08:05:32 -05:00
|
|
|
data_list: Iterable[np.ndarray],
|
|
|
|
indices_list: Iterable[np.ndarray] | None = None
|
|
|
|
):
|
|
|
|
if indices_list is not None:
|
|
|
|
data_list = [data[indices] for data, indices in zip(data_list, indices_list)]
|
|
|
|
total_len = sum(map(len, indices_list))
|
|
|
|
else:
|
|
|
|
total_len = sum(map(len, data_list))
|
|
|
|
|
2023-01-15 20:27:19 -08:00
|
|
|
if total_len == 0:
|
2024-08-19 08:05:32 -05:00
|
|
|
if self.vbo is not None:
|
|
|
|
self.vbo.clear()
|
|
|
|
return
|
|
|
|
|
|
|
|
# If possible, read concatenated data into existing list
|
|
|
|
if len(self.vert_data) != total_len:
|
|
|
|
self.vert_data = np.concatenate(data_list)
|
|
|
|
else:
|
|
|
|
np.concatenate(data_list, out=self.vert_data)
|
|
|
|
|
|
|
|
# Either create new vbo, or read data into it
|
|
|
|
total_size = self.vert_data.itemsize * total_len
|
|
|
|
if self.vbo is None:
|
|
|
|
self.vbo = self.ctx.buffer(self.vert_data)
|
|
|
|
elif self.vbo.size != total_size:
|
|
|
|
self.vbo.release()
|
|
|
|
self.vbo = self.ctx.buffer(self.vert_data)
|
|
|
|
else:
|
|
|
|
self.vbo.write(self.vert_data)
|
2023-01-25 12:10:39 -08:00
|
|
|
|
2023-01-25 14:13:56 -08:00
|
|
|
# Related to data and rendering
|
|
|
|
def pre_render(self):
|
|
|
|
self.set_ctx_depth_test(self.depth_test)
|
|
|
|
self.set_ctx_clip_plane(self.use_clip_plane())
|
|
|
|
|
|
|
|
def render(self):
|
2024-08-19 08:05:32 -05:00
|
|
|
if self.vao is None:
|
|
|
|
self.generate_vao()
|
2023-01-25 14:13:56 -08:00
|
|
|
self.vao.render()
|
|
|
|
|
2023-02-02 17:45:52 -08:00
|
|
|
def update_program_uniforms(self, camera_uniforms: UniformDict):
|
2024-08-19 08:05:32 -05:00
|
|
|
for program in self.programs:
|
|
|
|
if program is None:
|
|
|
|
continue
|
|
|
|
for uniforms in [self.mobject_uniforms, camera_uniforms, self.texture_names_to_ids]:
|
|
|
|
for name, value in uniforms.items():
|
|
|
|
set_program_uniform(program, name, value)
|
2024-08-17 07:11:56 -05:00
|
|
|
|
|
|
|
def generate_vao(self):
|
2024-08-19 08:05:32 -05:00
|
|
|
if self.vbo is None:
|
|
|
|
self.vbo = self.ctx.buffer(self.vert_data)
|
2023-01-27 10:01:37 -08:00
|
|
|
|
2023-01-25 12:10:39 -08:00
|
|
|
# Vertex array object
|
|
|
|
self.vao = self.ctx.vertex_array(
|
|
|
|
program=self.program,
|
2024-08-19 08:05:32 -05:00
|
|
|
content=[(self.vbo, self.vert_format, *self.vert_attributes)],
|
2023-01-25 14:13:56 -08:00
|
|
|
mode=self.render_primitive,
|
2023-01-25 12:10:39 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
def release(self):
|
2024-08-19 08:05:32 -05:00
|
|
|
for obj in (self.vbo, self.vao):
|
2023-01-25 12:10:39 -08:00
|
|
|
if obj is not None:
|
2023-01-25 16:43:47 -08:00
|
|
|
obj.release()
|
2023-01-25 12:10:39 -08:00
|
|
|
self.vbo = None
|
|
|
|
self.vao = None
|
2023-01-25 13:45:18 -08:00
|
|
|
|
|
|
|
|
2024-08-19 08:05:32 -05:00
|
|
|
class VShaderWrapper(ShaderWrapper):
|
2023-01-25 13:45:18 -08:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
ctx: moderngl.context.Context,
|
2024-08-19 08:05:32 -05:00
|
|
|
vert_data: np.ndarray,
|
|
|
|
shader_folder: Optional[str] = None,
|
|
|
|
mobject_uniforms: Optional[UniformDict] = None, # A dictionary mapping names of uniform variables
|
|
|
|
texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures.
|
|
|
|
depth_test: bool = False,
|
|
|
|
# render_primitive: int = moderngl.TRIANGLES,
|
|
|
|
render_primitive: int = moderngl.TRIANGLE_STRIP,
|
2023-01-25 13:45:18 -08:00
|
|
|
):
|
2024-08-19 08:05:32 -05:00
|
|
|
super().__init__(
|
|
|
|
ctx=ctx,
|
|
|
|
vert_data=vert_data,
|
|
|
|
shader_folder=shader_folder,
|
|
|
|
mobject_uniforms=mobject_uniforms,
|
|
|
|
texture_paths=texture_paths,
|
|
|
|
depth_test=depth_test,
|
|
|
|
render_primitive=render_primitive,
|
|
|
|
)
|
2023-01-27 08:26:54 -08:00
|
|
|
self.fill_canvas = get_fill_canvas(self.ctx)
|
2023-01-25 13:45:18 -08:00
|
|
|
|
2024-08-19 08:05:32 -05:00
|
|
|
def init_program_code(self) -> None:
|
|
|
|
self.program_code = {
|
|
|
|
f"{vtype}_{name}": get_shader_code_from_file(
|
|
|
|
os.path.join(f"quadratic_bezier_{vtype}", f"{name}.glsl")
|
|
|
|
)
|
|
|
|
for vtype in ["stroke", "fill"]
|
|
|
|
for name in ["vert", "geom", "frag"]
|
|
|
|
}
|
|
|
|
|
|
|
|
def init_program(self):
|
|
|
|
self.stroke_program = get_shader_program(
|
|
|
|
self.ctx,
|
|
|
|
vertex_shader=self.program_code["stroke_vert"],
|
|
|
|
geometry_shader=self.program_code["stroke_geom"],
|
|
|
|
fragment_shader=self.program_code["stroke_frag"],
|
|
|
|
)
|
|
|
|
self.fill_program = get_shader_program(
|
|
|
|
self.ctx,
|
|
|
|
vertex_shader=self.program_code["fill_vert"],
|
|
|
|
geometry_shader=self.program_code["fill_geom"],
|
|
|
|
fragment_shader=self.program_code["fill_frag"],
|
|
|
|
)
|
|
|
|
self.programs = [self.stroke_program, self.fill_program]
|
|
|
|
|
|
|
|
# Full vert format looks like this (total of 4x23 = 92 bytes):
|
|
|
|
# point 3
|
|
|
|
# stroke_rgba 4
|
|
|
|
# stroke_width 1
|
|
|
|
# joint_product 4
|
|
|
|
# fill_rgba 4
|
|
|
|
# base_point 3
|
|
|
|
# unit_normal 3
|
|
|
|
# fill_border_width 1
|
|
|
|
self.stroke_vert_format = '3f 4f 1f 4f 44x'
|
|
|
|
self.stroke_vert_attributes = ['point', 'stroke_rgba', 'stroke_width', 'joint_product']
|
|
|
|
|
|
|
|
self.fill_vert_format = '3f 36x 4f 3f 3f 4x'
|
|
|
|
self.fill_vert_attributes = ['point', 'fill_rgba', 'base_point', 'unit_normal']
|
|
|
|
|
|
|
|
def init_vertex_objects(self):
|
|
|
|
self.vbo = None
|
|
|
|
self.stroke_vao = None
|
|
|
|
self.fill_vao = None
|
|
|
|
|
|
|
|
# TODO, think about create_id, replace_code
|
|
|
|
def is_valid(self) -> bool:
|
|
|
|
return self.vert_data is not None
|
|
|
|
|
|
|
|
# TODO, motidify read in to handle triangulation case for non-winding fill?
|
|
|
|
|
|
|
|
# Rendering
|
|
|
|
def render_stroke(self):
|
|
|
|
if self.stroke_vao is None:
|
|
|
|
return
|
|
|
|
self.stroke_vao.render()
|
|
|
|
|
|
|
|
def render_fill(self):
|
|
|
|
if self.fill_vao is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
# TODO, need a new test here
|
|
|
|
winding = True
|
|
|
|
self.fill_program['winding'].value = winding
|
2023-01-25 13:45:18 -08:00
|
|
|
if not winding:
|
2024-08-19 08:05:32 -05:00
|
|
|
self.fill_vao.render()
|
2023-01-25 13:45:18 -08:00
|
|
|
return
|
2023-01-25 19:43:16 -08:00
|
|
|
|
2023-01-25 14:20:36 -08:00
|
|
|
original_fbo = self.ctx.fbo
|
2023-02-02 10:42:24 -08:00
|
|
|
texture_fbo, texture_vao = self.fill_canvas
|
2023-01-26 12:17:21 -08:00
|
|
|
|
2023-02-02 10:42:24 -08:00
|
|
|
texture_fbo.clear()
|
2023-01-27 08:26:54 -08:00
|
|
|
texture_fbo.use()
|
2023-01-26 22:51:14 -08:00
|
|
|
gl.glBlendFuncSeparate(
|
2023-01-26 22:40:29 -08:00
|
|
|
# Ordinary blending for colors
|
2023-01-26 22:51:14 -08:00
|
|
|
gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA,
|
2023-02-02 10:42:24 -08:00
|
|
|
# The effect of blending with -a / (1 - a)
|
|
|
|
# should be to cancel out
|
|
|
|
gl.GL_ONE_MINUS_DST_ALPHA, gl.GL_ONE,
|
2023-01-26 22:40:29 -08:00
|
|
|
)
|
2023-01-26 22:51:14 -08:00
|
|
|
|
2024-08-19 08:05:32 -05:00
|
|
|
self.fill_vao.render()
|
2023-01-26 12:17:21 -08:00
|
|
|
|
2023-01-25 14:20:36 -08:00
|
|
|
original_fbo.use()
|
2023-01-26 22:51:14 -08:00
|
|
|
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
|
|
|
|
|
2023-02-01 11:19:40 -08:00
|
|
|
texture_vao.render()
|
2023-01-26 22:51:14 -08:00
|
|
|
|
|
|
|
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
|
2024-08-19 08:05:32 -05:00
|
|
|
|
|
|
|
def render(self):
|
|
|
|
if self.stroke_vao is None or self.fill_vao is None:
|
|
|
|
self.generate_vao()
|
|
|
|
self.render_fill()
|
|
|
|
self.render_stroke()
|
|
|
|
|
|
|
|
def generate_vao(self):
|
|
|
|
self.stroke_vao = self.ctx.vertex_array(
|
|
|
|
program=self.stroke_program,
|
|
|
|
content=[(self.vbo, self.stroke_vert_format, *self.stroke_vert_attributes)],
|
|
|
|
mode=self.render_primitive,
|
|
|
|
)
|
|
|
|
self.fill_vao = self.ctx.vertex_array(
|
|
|
|
program=self.fill_program,
|
|
|
|
content=[(self.vbo, self.fill_vert_format, *self.fill_vert_attributes)],
|
|
|
|
mode=self.render_primitive,
|
|
|
|
)
|
|
|
|
|
|
|
|
def release(self):
|
|
|
|
attrs = ["vbo", "stroke_vao", "fill_vao"]
|
|
|
|
for attr in attrs:
|
|
|
|
obj = getattr(self, attr)
|
|
|
|
if obj is not None:
|
|
|
|
obj.release()
|
|
|
|
setattr(self, attr, None)
|