wip: copy data functions to opengl_renderer, preparing uniforms and shaders

This commit is contained in:
Tristan Schulz 2023-09-10 14:05:00 +02:00
commit 9904627b19
13 changed files with 483 additions and 885 deletions

View file

@ -0,0 +1,14 @@
import manim.utils.color.manim_colors as col
from manim.camera.camera import OpenGLCamera, OpenGLCameraFrame
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
from manim.renderer.opengl_renderer import OpenGLRenderer
renderer = OpenGLRenderer(1920, 1080)
vm = OpenGLVMobject([col.RED, col.GREEN])
vm.set_points_as_corners([[0, 0, 0], [1, 0, 0], [1, 1, 0]])
# print(vm.color)
# print(vm.fill_color)
# print(vm.stroke_color)
camera = OpenGLCameraFrame((1920, 1090))
renderer.render_vmobject(vm, camera)

View file

@ -30,6 +30,7 @@ from ..utils.simple_functions import fdiv
from ..utils.space_ops import normalize
# TODO: This becomes the new camera in the future
class OpenGLCameraFrame(OpenGLMobject):
def __init__(
self,
@ -41,13 +42,11 @@ class OpenGLCameraFrame(OpenGLMobject):
self.frame_shape = frame_shape
self.center_point = center_point
self.focal_dist_to_height = focal_dist_to_height
self.orientation = Rotation.identity().as_quat()
super().__init__(**kwargs)
def init_uniforms(self):
super().init_uniforms()
# as a quarternion
self.uniforms["orientation"] = Rotation.identity().as_quat()
self.uniforms["focal_dist_to_height"] = self.focal_dist_to_height
def init_points(self) -> None:
self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP])
@ -56,11 +55,11 @@ class OpenGLCameraFrame(OpenGLMobject):
self.move_to(self.center_point)
def set_orientation(self, rotation: Rotation):
self.uniforms["orientation"] = rotation.as_quat()
self.orientation = rotation.as_quat()
return self
def get_orientation(self):
return Rotation.from_quat(self.uniforms["orientation"])
return Rotation.from_quat(self.orientation)
def to_default_state(self):
self.center()
@ -137,11 +136,11 @@ class OpenGLCameraFrame(OpenGLMobject):
return self
def set_focal_distance(self, focal_distance: float):
self.uniforms["focal_dist_to_height"] = focal_distance / self.get_height()
self.focal_dist_to_height = focal_distance / self.get_height()
return self
def set_field_of_view(self, field_of_view: float):
self.uniforms["focal_dist_to_height"] = 2 * math.tan(field_of_view / 2)
self.focal_dist_to_height = 2 * math.tan(field_of_view / 2)
return self
def get_shape(self):
@ -160,10 +159,10 @@ class OpenGLCameraFrame(OpenGLMobject):
return points[4, 1] - points[3, 1]
def get_focal_distance(self) -> float:
return self.uniforms["focal_dist_to_height"] * self.get_height() # type: ignore
return self.focal_dist_to_height * self.get_height() # type: ignore
def get_field_of_view(self) -> float:
return 2 * math.atan(self.uniforms["focal_dist_to_height"] / 2)
return 2 * math.atan(self.focal_dist_to_height / 2)
def get_implied_camera_location(self) -> np.ndarray:
to_camera = self.get_inverse_camera_rotation_matrix()[2]
@ -171,6 +170,7 @@ class OpenGLCameraFrame(OpenGLMobject):
return self.get_center() + dist * to_camera
# TODO: This is already ported to the renderer and now is useless, leavefor now for compoatibilty reasons
class OpenGLCamera:
def __init__(
self,

View file

@ -13,6 +13,7 @@ from typing import TYPE_CHECKING
import moderngl
import numpy as np
from typing_extensions import TypeVar
from manim import config, logger
from manim.constants import *
@ -23,16 +24,27 @@ from manim.renderer.shader_wrapper import ShaderWrapper, get_colormap_code
from manim.utils.bezier import integer_interpolate, interpolate
from manim.utils.color import *
from manim.utils.deprecation import deprecated
# from ..utils.iterables import batch_by_property
from manim.utils.iterables import (batch_by_property, list_update, listify,
make_even, resize_array,
resize_preserving_order,
resize_with_interpolation, uniq_chain)
from manim.utils.iterables import (
batch_by_property,
list_update,
listify,
make_even,
resize_array,
resize_preserving_order,
resize_with_interpolation,
uniq_chain,
)
from manim.utils.paths import straight_path
from manim.utils.simple_functions import get_parameters
from manim.utils.space_ops import (angle_between_vectors, angle_of_vector,
get_norm, normalize,
rotation_matrix_transpose)
from manim.utils.space_ops import (
angle_between_vectors,
angle_of_vector,
get_norm,
normalize,
rotation_matrix_transpose,
)
if TYPE_CHECKING:
from typing import Callable, Iterable, Sequence, Tuple, Union
@ -43,6 +55,9 @@ if TYPE_CHECKING:
NonTimeUpdater: TypeAlias = Callable[[OpenGLMobject], OpenGLMobject | None]
Updater: TypeAlias = Union[TimeBasedUpdater, NonTimeUpdater]
PointUpdateFunction: TypeAlias = Callable[[np.ndarray], np.ndarray]
from manim.renderer.renderer import RendererData
T = TypeVar("T", bound=RendererData)
UNIFORM_DTYPE = np.float64
@ -135,6 +150,10 @@ class OpenGLMobject:
self.data: dict[str, np.ndarray] = {}
self.uniforms: dict[str, float | np.ndarray] = {}
self.renderer_data: T | None = None
self.colors_changed: bool = False
self.points_changed: bool = False
self.init_data()
self.init_uniforms()
self.init_updaters()
@ -156,7 +175,7 @@ class OpenGLMobject:
return self.__class__.__name__
def __repr__(self):
return str(self.name)
return str(self)
def __add__(self, other: OpenGLMobject) -> Self:
if not isinstance(other, OpenGLMobject):

View file

@ -10,24 +10,41 @@ import numpy as np
from manim import config
from manim.constants import *
from manim.mobject.opengl.opengl_mobject import (UNIFORM_DTYPE, OpenGLMobject,
OpenGLPoint)
from manim.mobject.opengl.opengl_mobject import (
UNIFORM_DTYPE,
OpenGLMobject,
OpenGLPoint,
)
from manim.renderer.shader_wrapper import ShaderWrapper
from manim.utils.bezier import (bezier, get_quadratic_approximation_of_cubic,
get_smooth_cubic_bezier_handle_points,
get_smooth_quadratic_bezier_handle_points,
integer_interpolate, interpolate,
inverse_interpolate,
partial_quadratic_bezier_points,
proportions_along_bezier_curve_for_point,
quadratic_bezier_remap)
from manim.utils.bezier import (
bezier,
get_quadratic_approximation_of_cubic,
get_smooth_cubic_bezier_handle_points,
get_smooth_quadratic_bezier_handle_points,
integer_interpolate,
interpolate,
inverse_interpolate,
partial_quadratic_bezier_points,
proportions_along_bezier_curve_for_point,
quadratic_bezier_remap,
)
from manim.utils.color import *
from manim.utils.iterables import (listify, make_even, resize_array,
resize_with_interpolation)
from manim.utils.space_ops import (angle_between_vectors, cross2d,
earclip_triangulation, get_norm,
get_unit_normal, shoelace_direction,
z_to_vector)
from manim.utils.iterables import (
listify,
make_even,
resize_array,
resize_with_interpolation,
tuplify,
)
from manim.utils.space_ops import (
angle_between_vectors,
cross2d,
earclip_triangulation,
get_norm,
get_unit_normal,
shoelace_direction,
z_to_vector,
)
if TYPE_CHECKING:
from typing import Callable, Iterable, Optional, Sequence
@ -74,43 +91,42 @@ class OpenGLVMobject(OpenGLMobject):
def __init__(
self,
color: ParsableManimColor | None = None,
fill_color: ParsableManimColor | None = None,
fill_opacity: float = 0.0,
stroke_color: ParsableManimColor | None = None,
stroke_opacity: float = 1.0,
color: ParsableManimColor | list[ParsableManimColor] | None = None,
fill_color: ParsableManimColor | list[ParsableManimColor] | None = None,
fill_opacity: float | None = None,
stroke_color: ParsableManimColor | list[ParsableManimColor] | None = None,
stroke_opacity: float | None = None,
stroke_width: float = DEFAULT_STROKE_WIDTH,
draw_stroke_behind_fill: bool = False,
background_image_file: str | None = None,
long_lines: bool = False,
joint_type: LineJointType = LineJointType.AUTO,
flat_stroke: bool = False,
# Measured in pixel widths
anti_alias_width: float = 1.0,
**kwargs,
):
self.fill_color = fill_color or color or DEFAULT_FILL_COLOR
self.fill_opacity = fill_opacity
self.stroke_color = stroke_color or color or DEFAULT_STROKE_COLOR
self.stroke_opacity = stroke_opacity
if fill_color is None:
fill_color = color
if stroke_color is None:
stroke_color = color
self.fill_color: Iterable[ManimColor] = listify(ManimColor.parse(fill_color))
self.set_fill(opacity=fill_opacity)
self.stroke_color = listify(ManimColor.parse(stroke_color))
self.set_stroke(opacity=stroke_opacity)
self.stroke_width = stroke_width
self.draw_stroke_behind_fill = draw_stroke_behind_fill
self.background_image_file = background_image_file
self.long_lines = long_lines
self.joint_type = joint_type
self.flat_stroke = flat_stroke
self.anti_alias_width = anti_alias_width
# TODO: Remove this because the new shader doesn't need it
self.anti_alias_width = 1.0
self.needs_new_triangulation = True
self.triangulation = np.zeros(0, dtype="i4")
super().__init__(**kwargs)
self.refresh_unit_normal()
if fill_color is not None:
self.fill_color = ManimColor.parse(fill_color)
if stroke_color is not None:
self.stroke_color = ManimColor.parse(stroke_color)
# self.refresh_unit_normal()
def get_group_class(self):
return OpenGLVGroup
@ -177,19 +193,19 @@ class OpenGLVMobject(OpenGLMobject):
# Colors
def init_colors(self):
self.set_fill(
color=self.fill_color or self.color,
opacity=self.fill_opacity,
)
self.set_stroke(
color=self.stroke_color or self.color,
width=self.stroke_width,
opacity=self.stroke_opacity,
background=self.draw_stroke_behind_fill,
)
self.set_gloss(self.gloss)
self.set_flat_stroke(self.flat_stroke)
self.color = self.get_color()
# self.set_fill(
# color=self.fill_color or self.color,
# opacity=self.fill_opacity,
# )
# self.set_stroke(
# color=self.stroke_color or self.color,
# width=self.stroke_width,
# opacity=self.stroke_opacity,
# background=self.draw_stroke_behind_fill,
# )
# self.set_gloss(self.gloss)
# self.set_flat_stroke(self.flat_stroke)
# self.color = self.get_color()
return self
def set_rgba_array(
@ -209,7 +225,7 @@ class OpenGLVMobject(OpenGLMobject):
def set_fill(
self,
color: ParsableManimColor | None = None,
color: ParsableManimColor | Iterable[ParsableManimColor] | None = None,
opacity: float | None = None,
recurse: bool = True,
) -> Self:
@ -248,7 +264,12 @@ class OpenGLVMobject(OpenGLMobject):
--------
:meth:`~.OpenGLVMobject.set_style`
"""
self.set_rgba_array_by_color(color, opacity, "fill_rgba", recurse)
for mob in self.get_family(recurse):
if color is not None:
mob.fill_color = listify(ManimColor.parse(color))
if opacity is not None:
for c in mob.fill_color:
c.set_opacity(opacity)
return self
def set_stroke(
@ -259,18 +280,17 @@ class OpenGLVMobject(OpenGLMobject):
background=None,
recurse=True,
):
self.set_rgba_array_by_color(color, opacity, "stroke_rgba", recurse)
for mob in self.get_family(recurse):
if color is not None:
mob.stroke_color = listify(ManimColor.parse(color))
if opacity is not None:
for c in mob.stroke_color:
c.set_opacity(opacity)
if width is not None:
for mob in self.get_family(recurse):
if isinstance(width, np.ndarray):
arr = width.reshape((len(width), 1))
else:
arr = np.array([[w] for w in listify(width)], dtype=float)
mob.data["stroke_width"] = arr
if width is not None:
mob.stroke_width = listify(width)
if background is not None:
for mob in self.get_family(recurse):
if background is not None:
mob.draw_stroke_behind_fill = background
return self
@ -389,19 +409,19 @@ class OpenGLVMobject(OpenGLMobject):
# Todo im not quite sure why we are doing this
def get_fill_colors(self):
return [ManimColor.from_rgb(rgba[:3]) for rgba in self.fill_rgba]
return self.fill_color
def get_fill_opacities(self) -> np.ndarray:
return self.data["fill_rgba"][:, 3]
return (c.to_rgba()[3] for c in self.fill_color)
def get_stroke_colors(self):
return [ManimColor.from_rgb(rgba[:3]) for rgba in self.stroke_rgba]
return self.stroke_color
def get_stroke_opacities(self) -> np.ndarray:
return self.data["stroke_rgba"][:, 3]
return (c.to_rgba()[3] for c in self.stroke_color)
def get_stroke_widths(self) -> np.ndarray:
return self.data["stroke_width"][:, 0]
return self.stroke_width
# TODO, it's weird for these to return the first of various lists
# rather than the full information
@ -434,6 +454,7 @@ class OpenGLVMobject(OpenGLMobject):
return self.get_stroke_color()
def has_stroke(self) -> bool:
# TODO: This currently doesn't make sense needs fixing
return any(self.data["stroke_width"]) and any(self.data["stroke_rgba"][:, 3])
def has_fill(self) -> bool:

View file

@ -14,5 +14,4 @@ from manim.mobject.opengl.opengl_surface import *
from manim.mobject.opengl.opengl_three_dimensions import *
from manim.mobject.opengl.opengl_vectorized_mobject import *
from ..renderer.shader import *
from ..utils.opengl import *

View file

@ -1,80 +1,231 @@
import numpy as np
from renderer import Renderer, RendererData
import re
from functools import lru_cache
from pathlib import Path
from typing import TYPE_CHECKING
from manim.mobject.types.vectorized_mobject import VMobject
import moderngl as gl
import numpy as np
from typing_extensions import override
import manim.constants as const
import manim.utils.color.manim_colors as color
from manim._config import config, logger
from manim.camera.camera import OpenGLCameraFrame
from manim.renderer.opengl_shader_program import load_shader_program_by_folder
from manim.renderer.renderer import Renderer, RendererData
from manim.utils.space_ops import cross2d, earclip_triangulation, z_to_vector
if TYPE_CHECKING:
from manim.mobject.types.vectorized_mobject import VMobject
import manim.utils.color.core as c
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
class GLRenderData(RendererData):
def __init__(self) -> None:
super().__init__()
self.fill_rgbas = np.zeros((1,4))
self.stroke_rgbas = np.zeros((1,4))
self.normals = np.zeros((1,4))
self.mesh = np.zeros((0,3))
self.bounding_box = np.zeros((3,3))
self.fill_rgbas = np.zeros((1, 4))
self.stroke_rgbas = np.zeros((1, 4))
self.normals = np.zeros((1, 4))
self.orientation = None
self.mesh = np.zeros((0, 3))
self.bounding_box = np.zeros((3, 3))
def __repr__(self) -> str:
return f"""GLRenderData
fill:
{self.fill_rgbas}
stroke:
{self.stroke_rgbas}
normals:
{self.normals}
orientation:
{self.orientation}
mesh:
{self.mesh}
bounding_box:
{self.bounding_box}
"""
def get_triangulation(vmobject: OpenGLVMobject):
# Figure out how to triangulate the interior to know
# how to send the points as to the vertex shader.
# First triangles come directly from the points
points = vmobject.points
if len(points) <= 1:
vmobject.triangulation = np.zeros(0, dtype="i4")
vmobject.needs_new_triangulation = False
return vmobject.triangulation
normal_vector = vmobject.get_unit_normal()
indices = np.arange(len(points), dtype=int)
# Rotate points such that unit normal vector is OUT
if not np.isclose(normal_vector, const.OUT).all():
points = np.dot(points, z_to_vector(normal_vector))
atol = vmobject.tolerance_for_point_equality
end_of_loop = np.zeros(len(points) // 3, dtype=bool)
end_of_loop[:-1] = (np.abs(points[2:-3:3] - points[3::3]) > atol).any(1)
end_of_loop[-1] = True
v01s = points[1::3] - points[0::3]
v12s = points[2::3] - points[1::3]
curve_orientations = np.sign(cross2d(v01s, v12s))
orientation = np.transpose([curve_orientations.repeat(3)])
concave_parts = curve_orientations < 0
# These are the vertices to which we'll apply a polygon triangulation
inner_vert_indices = np.hstack(
[
indices[0::3],
indices[1::3][concave_parts],
indices[2::3][end_of_loop],
]
)
inner_vert_indices.sort()
rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2]
# Triangulate
inner_verts = points[inner_vert_indices]
inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)]
tri_indices = np.hstack([indices, inner_tri_indices])
return tri_indices, orientation
def prepare_array(values: np.ndarray, desired_length: int):
"""Interpolates a given list of colors to match the desired length
Parameters
----------
values : np.ndarray
a 2 dimensional numpy array where values are interpolated on the y axis
desired_length : int
the desired length for the array
Returns
-------
np.ndarray
the interpolated array of values
"""
fill_length = len(values)
if fill_length == 1:
return np.repeat(values, desired_length, axis=0)
xm = np.linspace(0, fill_length - 1, desired_length)
rgbas = []
for x in xm:
minimum = int(np.floor(x))
maximum = int(np.ceil(x))
alpha = x - minimum
if alpha == 0:
rgbas.append(values[minimum])
continue
val_a = values[minimum]
val_b = values[maximum]
rgbas.append(val_a * (1 - alpha) + val_b * alpha)
return np.array(rgbas)
def compute_bounding_box(mob):
all_points = np.vstack(
[
mob.points,
*(m.get_bounding_box() for m in mob.get_family()[1:] if m.has_points()),
],
)
if len(all_points) == 0:
return np.zeros((3, mob.dim))
else:
# Lower left and upper right corners
mins = all_points.min(0)
maxs = all_points.max(0)
mids = (mins + maxs) / 2
return np.array([mins, mids, maxs])
class OpenGLRenderer(Renderer):
pixel_array_dtype = np.uint8
def __init__(
self,
ctx: moderngl.Context | None = None,
background_image: str | None = None,
frame_config: dict = {},
pixel_width: int = config.pixel_width,
pixel_height: int = config.pixel_height,
fps: int = config.frame_rate,
# Note: frame height and width will be resized to match the pixel aspect rati
background_color=BLACK,
samples=4,
background_color: c.ManimColor = color.BLACK,
background_opacity: float = 1.0,
# Points in vectorized mobjects with norm greater
# than this value will be rescaled
max_allowable_norm: float = 1.0,
image_mode: str = "RGBA",
n_channels: int = 4,
pixel_array_dtype: type = np.uint8,
light_source_position: np.ndarray = np.array([-10, 10, 10]),
# Although vector graphics handle antialiasing fine
# without multisampling, for 3d scenes one might want
# to set samples to be greater than 0.
samples: int = 0,
background_image: str | None = None,
) -> None:
self.background_image = background_image
logger.debug("Initializing OpenGLRenderer")
self.pixel_width = pixel_width
self.pixel_height = pixel_height
self.fps = fps
self.max_allowable_norm = max_allowable_norm
self.image_mode = image_mode
self.n_channels = n_channels
self.pixel_array_dtype = pixel_array_dtype
self.light_source_position = light_source_position
self.samples = samples
self.background_color = (color.BLACK,)
self.background_image = background_image
self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max
self.background_color: list[float] = list(
color_to_rgba(background_color, background_opacity)
# Initializing Context
logger.debug("Initializing OpenGL context and framebuffers")
self.ctx = gl.create_context(standalone=True)
self.target_fbo = self.ctx.simple_framebuffer(
(self.pixel_width, self.pixel_height), samples=self.samples
)
self.output_fbo = self.ctx.framebuffer(
color_attachments=[
self.ctx.renderbuffer((self.pixel_width, self.pixel_height))
]
)
# self.init_frame(**frame_config)
# self.init_context(ctx)
# self.init_shaders()
# self.init_textures()
# self.init_light_source()
# self.refresh_perspective_uniforms()
# A cached map from mobjects to their associated list of render groups
# so that these render groups are not regenerated unnecessarily for static
# mobjects
self.mob_to_render_groups: dict = {}
def render_vmobject(self, mob: VMobject) -> None:
# Should be in OpenGL renderer
if not mob.renderer_data:
# Initalization
# Preparing vmobject shader
logger.debug("Initializing Shader Programs")
self.vmobject_fill_program = load_shader_program_by_folder(
self.ctx, "quadratic_bezier_fill"
)
self.vmobject_stroke_program = load_shader_program_by_folder(
self.ctx, "quadratic_bezier_stroke"
)
@override
def render_vmobject(self, mob: OpenGLVMobject, camera: OpenGLCameraFrame) -> None:
# Setting camera uniforms
if mob.renderer_data is None:
# Initialize
# TODO: Intialize all the data also for submobjects
logger.debug("Initializing GLRenderData")
mob.renderer_data = GLRenderData()
# Generate Mesh
mob.renderer_data.mesh, mob.renderer_data.orientation = get_triangulation(
mob
)
mesh_length = len(mob.renderer_data.mesh)
if mob.colors_changed:
mob.renderer_data.fill_rgbas = np.resize(mob.fill_color, (len(mob.renderer_data.mesh),4))
# Generate Fill Color
fill_color = np.array([c._internal_value for c in mob.fill_color])
stroke_color = np.array([c._internal_value for c in mob.stroke_color])
mob.renderer_data.fill_rgbas = prepare_array(fill_color, mesh_length)
mob.renderer_data.stroke_rgbas = prepare_array(stroke_color, mesh_length)
mob.renderer_data.normals = np.repeat(
[mob.get_unit_normal()], mesh_length, axis=0
)
mob.renderer_data.bounding_box = compute_bounding_box(mob)
if mob.points_changed:
if(mob.has_fill()):
mob.renderer_data.mesh = ... # Triangulation todo
# print(mob.renderer_data.mesh)
# print(mob.renderer_data.orientation)
# print(mob.points)
# if mob.colors_changed:
# mob.renderer_data.fill_rgbas = np.resize(mob.fill_color, (len(mob.renderer_data.mesh),4))
# if mob.points_changed:3357
# if(mob.has_fill()):
# mob.renderer_data.mesh = ... # Triangulation todo
# set shader
# use vbo
@ -83,7 +234,8 @@ class OpenGLRenderer(Renderer):
# set shader
# use vbo
# render stroke
self.fbo ...
# self.fbo ...
# def init_frame(self, **config) -> None:
# self.frame = OpenGLCameraFrame(**config)

View file

@ -0,0 +1,90 @@
# For caching
import re
from functools import lru_cache
from pathlib import Path
import moderngl as gl
from manim._config import logger
filename_to_code_map: dict = {}
def get_shader_dir():
return Path(__file__).parent / "shaders"
def find_file(file_name: Path, directories: list[Path]) -> Path:
# Check if what was passed in is already a valid path to a file
if file_name.exists():
return file_name
possible_paths = (directory / file_name for directory in directories)
for path in possible_paths:
logger.debug(f"Searching for {file_name} in {path}")
if path.exists():
return path
else:
logger.debug(f"shader_wrapper.py::find_file() : {path} does not exist.")
raise OSError(f"{file_name} not Found")
@lru_cache(maxsize=12)
def get_shader_code_from_file(filename: Path) -> str | None:
if filename in filename_to_code_map:
return filename_to_code_map[filename]
try:
filepath = find_file(
filename,
directories=[get_shader_dir(), Path("/")],
)
except OSError:
logger.warning(f"Could not find shader file {filename}")
return None
result = filepath.read_text()
# 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"^#include.*",
result,
flags=re.MULTILINE,
)
for line in insertions:
include_path = line.strip().replace("#include", "")
include_path = include_path.replace('"', "")
path = (filepath.parent / Path(include_path.strip())).resolve()
logger.debug(f"Trying to get code from: {path} to include in {filepath.name}")
inserted_code = get_shader_code_from_file(
path,
)
if inserted_code is None:
return None
result = result.replace(
line,
f"// Start include of: {include_path}\n\n{inserted_code}\n\n// End include of: {include_path}",
)
filename_to_code_map[filename] = result
return result
def load_shader_program_by_folder(ctx: gl.Context, folder_name: str):
vertex_code = get_shader_code_from_file(Path(folder_name + "/vert.glsl"))
geometry_code = get_shader_code_from_file(Path(folder_name + "/geom.glsl"))
fragment_code = get_shader_code_from_file(Path(folder_name + "/frag.glsl"))
if vertex_code is None or fragment_code is None:
logger.error(
f"Invalid program definition for {folder_name} vertex or fragment shader not present"
)
raise RuntimeError("Loading Shader Program Error")
if geometry_code is None:
return ctx.program(vertex_shader=vertex_code, fragment_shader=fragment_code)
elif geometry_code is not None:
return ctx.program(
vertex_shader=vertex_code,
geometry_shader=geometry_code,
fragment_shader=fragment_code,
)

View file

@ -2,31 +2,29 @@ from abc import ABC, abstractclassmethod, abstractstaticmethod
from typing import TYPE_CHECKING
import numpy as np
from typing_extensions import TypeAlias
from manim import config
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
from manim.mobject.types.image_mobject import ImageMobject
from manim.mobject.types.vectorized_mobject import VMobject
from manim.scene import Scene
if TYPE_CHECKING:
from typing import TypeAlias
Image : TypeAlias = np.ndarray
Image: TypeAlias = np.ndarray
class RendererData:
pass
class Renderer(ABC):
class Renderer(ABC):
def __init__(self):
self.fbo = np.zeros((config.height, config.width))
self.capabilities= {
self.capabilities = {
VMobject: self.render_vmobject,
ImageMobject: self.render_imobject,
}
def render(self, camera, renderables: [VMobject]) -> Image: # Image
def render(self, camera, renderables: [VMobject]) -> Image: # Image
for mob in renderables:
if type(mob) in self.capabilities:
mob.points
@ -36,15 +34,14 @@ class Renderer(ABC):
return self.fbo.get_pixels()
@abstractclassmethod
def render_vmobject(self, mob:VMobject):
def render_vmobject(self, mob: OpenGLVMobject) -> None:
raise NotImplementedError
def render_mesh(self, mob):
def render_mesh(self, mob) -> None:
raise NotImplementedError
def render_image(self, mob):
def render_image(self, mob) -> None:
raise NotImplementedError

View file

@ -1,445 +0,0 @@
from __future__ import annotations
import re
import textwrap
from pathlib import Path
import moderngl
import numpy as np
from .. import config
from ..utils import opengl
from ..utils.simple_functions import get_parameters
SHADER_FOLDER = Path(__file__).parent / "shaders"
shader_program_cache: dict = {}
file_path_to_code_map: dict = {}
__all__ = [
"Object3D",
"Mesh",
"Shader",
"FullScreenQuad",
]
def get_shader_code_from_file(file_path: Path) -> str:
# TODO: Is this code used ?
if file_path in file_path_to_code_map:
return file_path_to_code_map[file_path]
source = file_path.read_text()
include_lines = re.finditer(
r"^#include (?P<include_path>.*\.glsl)$",
source,
flags=re.MULTILINE,
)
for match in include_lines:
include_path = match.group("include_path")
included_code = get_shader_code_from_file(
file_path.parent / include_path,
)
source = source.replace(match.group(0), included_code)
file_path_to_code_map[file_path] = source
return source
def filter_attributes(unfiltered_attributes, attributes):
# Construct attributes for only those needed by the shader.
filtered_attributes_dtype = []
for i, dtype_name in enumerate(unfiltered_attributes.dtype.names):
if dtype_name in attributes:
filtered_attributes_dtype.append(
(
dtype_name,
unfiltered_attributes.dtype[i].subdtype[0].str,
unfiltered_attributes.dtype[i].shape,
),
)
filtered_attributes = np.zeros(
unfiltered_attributes[unfiltered_attributes.dtype.names[0]].shape[0],
dtype=filtered_attributes_dtype,
)
for dtype_name in unfiltered_attributes.dtype.names:
if dtype_name in attributes:
filtered_attributes[dtype_name] = unfiltered_attributes[dtype_name]
return filtered_attributes
class Object3D:
def __init__(self, *children):
self.model_matrix = np.eye(4)
self.normal_matrix = np.eye(4)
self.children = []
self.parent = None
self.add(*children)
self.init_updaters()
# TODO: Use path_func.
def interpolate(self, start, end, alpha, _):
self.model_matrix = (1 - alpha) * start.model_matrix + alpha * end.model_matrix
self.normal_matrix = (
1 - alpha
) * start.normal_matrix + alpha * end.normal_matrix
def single_copy(self):
copy = Object3D()
copy.model_matrix = self.model_matrix.copy()
copy.normal_matrix = self.normal_matrix.copy()
return copy
def copy(self):
node_to_copy = {}
bfs = [self]
while bfs:
node = bfs.pop(0)
bfs.extend(node.children)
node_copy = node.single_copy()
node_to_copy[node] = node_copy
# Add the copy to the copy of the parent.
if node.parent is not None and node is not self:
node_to_copy[node.parent].add(node_copy)
return node_to_copy[self]
def add(self, *children):
for child in children:
if child.parent is not None:
raise Exception(
"Attempt to add child that's already added to another Object3D",
)
self.remove(*children, current_children_only=False)
self.children.extend(children)
for child in children:
child.parent = self
def remove(self, *children, current_children_only=True):
if current_children_only:
for child in children:
if child.parent != self:
raise Exception(
"Attempt to remove child that isn't added to this Object3D",
)
self.children = list(filter(lambda child: child not in children, self.children))
for child in children:
child.parent = None
def get_position(self):
return self.model_matrix[:, 3][:3]
def set_position(self, position):
self.model_matrix[:, 3][:3] = position
return self
def get_meshes(self):
dfs = [self]
while dfs:
parent = dfs.pop()
if isinstance(parent, Mesh):
yield parent
dfs.extend(parent.children)
def get_family(self):
dfs = [self]
while dfs:
parent = dfs.pop()
yield parent
dfs.extend(parent.children)
def align_data_and_family(self, _):
pass
def hierarchical_model_matrix(self):
if self.parent is None:
return self.model_matrix
model_matrices = [self.model_matrix]
current_object = self
while current_object.parent is not None:
model_matrices.append(current_object.parent.model_matrix)
current_object = current_object.parent
return np.linalg.multi_dot(list(reversed(model_matrices)))
def hierarchical_normal_matrix(self):
if self.parent is None:
return self.normal_matrix[:3, :3]
normal_matrices = [self.normal_matrix]
current_object = self
while current_object.parent is not None:
normal_matrices.append(current_object.parent.model_matrix)
current_object = current_object.parent
return np.linalg.multi_dot(list(reversed(normal_matrices)))[:3, :3]
def init_updaters(self):
self.time_based_updaters = []
self.non_time_updaters = []
self.has_updaters = False
self.updating_suspended = False
def update(self, dt=0):
if not self.has_updaters or self.updating_suspended:
return self
for updater in self.time_based_updaters:
updater(self, dt)
for updater in self.non_time_updaters:
updater(self)
return self
def get_time_based_updaters(self):
return self.time_based_updaters
def has_time_based_updater(self):
return len(self.time_based_updaters) > 0
def get_updaters(self):
return self.time_based_updaters + self.non_time_updaters
def add_updater(self, update_function, index=None, call_updater=True):
if "dt" in get_parameters(update_function):
updater_list = self.time_based_updaters
else:
updater_list = self.non_time_updaters
if index is None:
updater_list.append(update_function)
else:
updater_list.insert(index, update_function)
self.refresh_has_updater_status()
if call_updater:
self.update()
return self
def remove_updater(self, update_function):
for updater_list in [self.time_based_updaters, self.non_time_updaters]:
while update_function in updater_list:
updater_list.remove(update_function)
self.refresh_has_updater_status()
return self
def clear_updaters(self):
self.time_based_updaters = []
self.non_time_updaters = []
self.refresh_has_updater_status()
return self
def match_updaters(self, mobject):
self.clear_updaters()
for updater in mobject.get_updaters():
self.add_updater(updater)
return self
def suspend_updating(self):
self.updating_suspended = True
return self
def resume_updating(self, call_updater=True):
self.updating_suspended = False
if call_updater:
self.update(dt=0)
return self
def refresh_has_updater_status(self):
self.has_updaters = len(self.get_updaters()) > 0
return self
class Mesh(Object3D):
def __init__(
self,
shader=None,
attributes=None,
geometry=None,
material=None,
indices=None,
use_depth_test=True,
primitive=moderngl.TRIANGLES,
):
super().__init__()
if shader is not None and attributes is not None:
self.shader = shader
self.attributes = attributes
self.indices = indices
elif geometry is not None and material is not None:
self.shader = material
self.attributes = geometry.attributes
self.indices = geometry.index
else:
raise Exception(
"Mesh requires either attributes and a Shader or a Geometry and a "
"Material",
)
self.use_depth_test = use_depth_test
self.primitive = primitive
self.skip_render = False
self.init_updaters()
def single_copy(self):
copy = Mesh(
attributes=self.attributes.copy(),
shader=self.shader,
indices=self.indices.copy() if self.indices is not None else None,
use_depth_test=self.use_depth_test,
primitive=self.primitive,
)
copy.skip_render = self.skip_render
copy.model_matrix = self.model_matrix.copy()
copy.normal_matrix = self.normal_matrix.copy()
# TODO: Copy updaters?
return copy
def set_uniforms(self, renderer):
self.shader.set_uniform(
"u_model_matrix",
opengl.matrix_to_shader_input(self.model_matrix),
)
self.shader.set_uniform("u_view_matrix", renderer.camera.formatted_view_matrix)
self.shader.set_uniform(
"u_projection_matrix",
renderer.camera.projection_matrix,
)
def render(self):
if self.skip_render:
return
if self.use_depth_test:
self.shader.context.enable(moderngl.DEPTH_TEST)
else:
self.shader.context.disable(moderngl.DEPTH_TEST)
from moderngl import Attribute
shader_attributes = []
for k, v in self.shader.shader_program._members.items():
if isinstance(v, Attribute):
shader_attributes.append(k)
shader_attributes = filter_attributes(self.attributes, shader_attributes)
vertex_buffer_object = self.shader.context.buffer(shader_attributes.tobytes())
if self.indices is None:
index_buffer_object = None
else:
vert_index_data = self.indices.astype("i4").tobytes()
if vert_index_data:
index_buffer_object = self.shader.context.buffer(vert_index_data)
else:
index_buffer_object = None
vertex_array_object = self.shader.context.simple_vertex_array(
self.shader.shader_program,
vertex_buffer_object,
*shader_attributes.dtype.names,
index_buffer=index_buffer_object,
)
vertex_array_object.render(self.primitive)
vertex_buffer_object.release()
vertex_array_object.release()
if index_buffer_object is not None:
index_buffer_object.release()
class Shader:
def __init__(
self,
context,
name=None,
source=None,
):
global shader_program_cache
self.context = context
self.name = name
# See if the program is cached.
if (
self.name in shader_program_cache
and shader_program_cache[self.name].ctx == self.context
):
self.shader_program = shader_program_cache[self.name]
elif source is not None:
# Generate the shader from inline code if it was passed.
self.shader_program = context.program(**source)
else:
# Search for a file containing the shader.
source_dict = {}
source_dict_key = {
"vert": "vertex_shader",
"frag": "fragment_shader",
"geom": "geometry_shader",
}
shader_folder = SHADER_FOLDER / name
for shader_file in shader_folder.iterdir():
shader_file_path = shader_folder / shader_file
shader_source = get_shader_code_from_file(shader_file_path)
source_dict[source_dict_key[shader_file_path.stem]] = shader_source
self.shader_program = context.program(**source_dict)
# Cache the shader.
if name is not None and name not in shader_program_cache:
shader_program_cache[self.name] = self.shader_program
def set_uniform(self, name, value):
try:
self.shader_program[name] = value
except KeyError:
pass
class FullScreenQuad(Mesh):
def __init__(
self,
context,
fragment_shader_source=None,
fragment_shader_name=None,
):
if fragment_shader_source is None and fragment_shader_name is None:
raise Exception("Must either pass shader name or shader source.")
if fragment_shader_name is not None:
# Use the name.
shader_file_path = SHADER_FOLDER / f"{fragment_shader_name}.frag"
fragment_shader_source = get_shader_code_from_file(shader_file_path)
elif fragment_shader_source is not None:
fragment_shader_source = textwrap.dedent(fragment_shader_source.lstrip())
shader = Shader(
context,
source={
"vertex_shader": """
#version 330
in vec4 in_vert;
uniform mat4 u_model_view_matrix;
uniform mat4 u_projection_matrix;
void main() {{
vec4 camera_space_vertex = u_model_view_matrix * in_vert;
vec4 clip_space_vertex = u_projection_matrix * camera_space_vertex;
gl_Position = clip_space_vertex;
}}
""",
"fragment_shader": fragment_shader_source,
},
)
attributes = np.zeros(6, dtype=[("in_vert", np.float32, (4,))])
attributes["in_vert"] = np.array(
[
[-config["frame_x_radius"], -config["frame_y_radius"], 0, 1],
[-config["frame_x_radius"], config["frame_y_radius"], 0, 1],
[config["frame_x_radius"], config["frame_y_radius"], 0, 1],
[-config["frame_x_radius"], -config["frame_y_radius"], 0, 1],
[config["frame_x_radius"], -config["frame_y_radius"], 0, 1],
[config["frame_x_radius"], config["frame_y_radius"], 0, 1],
],
)
shader.set_uniform("u_model_view_matrix", opengl.view_matrix())
shader.set_uniform(
"u_projection_matrix",
opengl.orthographic_projection_matrix(),
)
super().__init__(shader, attributes)
def render(self):
super().render()

View file

@ -1,287 +0,0 @@
from __future__ import annotations
import collections
import numpy as np
from ..utils import opengl
from ..utils.space_ops import cross2d, earclip_triangulation
from .shader import Shader
def build_matrix_lists(mob):
root_hierarchical_matrix = mob.hierarchical_model_matrix()
matrix_to_mobject_list = collections.defaultdict(list)
if mob.has_points():
matrix_to_mobject_list[tuple(root_hierarchical_matrix.ravel())].append(mob)
mobject_to_hierarchical_matrix = {mob: root_hierarchical_matrix}
dfs = [mob]
while dfs:
parent = dfs.pop()
for child in parent.submobjects:
child_hierarchical_matrix = (
mobject_to_hierarchical_matrix[parent] @ child.model_matrix
)
mobject_to_hierarchical_matrix[child] = child_hierarchical_matrix
if child.has_points():
matrix_to_mobject_list[tuple(child_hierarchical_matrix.ravel())].append(
child,
)
dfs.append(child)
return matrix_to_mobject_list
def render_opengl_vectorized_mobject_fill(renderer, mobject):
matrix_to_mobject_list = build_matrix_lists(mobject)
for matrix_tuple, mobject_list in matrix_to_mobject_list.items():
model_matrix = np.array(matrix_tuple).reshape((4, 4))
render_mobject_fills_with_matrix(renderer, model_matrix, mobject_list)
def render_mobject_fills_with_matrix(renderer, model_matrix, mobjects):
# Precompute the total number of vertices for which to reserve space.
# Note that triangulate_mobject() will cache its results.
total_size = 0
for submob in mobjects:
total_size += triangulate_mobject(submob).shape[0]
attributes = np.empty(
total_size,
dtype=[
("in_vert", np.float32, (3,)),
("in_color", np.float32, (4,)),
("texture_coords", np.float32, (2,)),
("texture_mode", np.int32),
],
)
write_offset = 0
for submob in mobjects:
if not submob.has_points():
continue
mobject_triangulation = triangulate_mobject(submob)
end_offset = write_offset + mobject_triangulation.shape[0]
attributes[write_offset:end_offset] = mobject_triangulation
attributes["in_color"][write_offset:end_offset] = np.repeat(
submob.fill_rgba,
mobject_triangulation.shape[0],
axis=0,
)
write_offset = end_offset
fill_shader = Shader(renderer.context, name="vectorized_mobject_fill")
fill_shader.set_uniform(
"u_model_view_matrix",
opengl.matrix_to_shader_input(
renderer.camera.unformatted_view_matrix @ model_matrix,
),
)
fill_shader.set_uniform(
"u_projection_matrix",
renderer.scene.camera.projection_matrix,
)
vbo = renderer.context.buffer(attributes.tobytes())
vao = renderer.context.simple_vertex_array(
fill_shader.shader_program,
vbo,
*attributes.dtype.names,
)
vao.render()
vao.release()
vbo.release()
def triangulate_mobject(mob):
if not mob.needs_new_triangulation:
return mob.triangulation
# Figure out how to triangulate the interior to know
# how to send the points as to the vertex shader.
# First triangles come directly from the points
# normal_vector = mob.get_unit_normal()
points = mob.points
b0s = points[0::3]
b1s = points[1::3]
b2s = points[2::3]
v01s = b1s - b0s
v12s = b2s - b1s
crosses = cross2d(v01s, v12s)
convexities = np.sign(crosses)
if mob.orientation == 1:
concave_parts = convexities > 0
convex_parts = convexities <= 0
else:
concave_parts = convexities < 0
convex_parts = convexities >= 0
# These are the vertices to which we'll apply a polygon triangulation
atol = mob.tolerance_for_point_equality
end_of_loop = np.zeros(len(b0s), dtype=bool)
end_of_loop[:-1] = (np.abs(b2s[:-1] - b0s[1:]) > atol).any(1)
end_of_loop[-1] = True
indices = np.arange(len(points), dtype=int)
inner_vert_indices = np.hstack(
[
indices[0::3],
indices[1::3][concave_parts],
indices[2::3][end_of_loop],
],
)
inner_vert_indices.sort()
rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2]
# Triangulate
inner_verts = points[inner_vert_indices]
inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)]
bezier_triangle_indices = np.reshape(indices, (-1, 3))
concave_triangle_indices = np.reshape(bezier_triangle_indices[concave_parts], (-1))
convex_triangle_indices = np.reshape(bezier_triangle_indices[convex_parts], (-1))
points = points[
np.hstack(
[
concave_triangle_indices,
convex_triangle_indices,
inner_tri_indices,
],
)
]
texture_coords = np.tile(
[
[0.0, 0.0],
[0.5, 0.0],
[1.0, 1.0],
],
(points.shape[0] // 3, 1),
)
texture_mode = np.hstack(
(
np.ones(concave_triangle_indices.shape[0]),
-1 * np.ones(convex_triangle_indices.shape[0]),
np.zeros(inner_tri_indices.shape[0]),
),
)
attributes = np.zeros(
points.shape[0],
dtype=[
("in_vert", np.float32, (3,)),
("in_color", np.float32, (4,)),
("texture_coords", np.float32, (2,)),
("texture_mode", np.int32),
],
)
attributes["in_vert"] = points
attributes["texture_coords"] = texture_coords
attributes["texture_mode"] = texture_mode
mob.triangulation = attributes
mob.needs_new_triangulation = False
return attributes
def render_opengl_vectorized_mobject_stroke(renderer, mobject):
matrix_to_mobject_list = build_matrix_lists(mobject)
for matrix_tuple, mobject_list in matrix_to_mobject_list.items():
model_matrix = np.array(matrix_tuple).reshape((4, 4))
render_mobject_strokes_with_matrix(renderer, model_matrix, mobject_list)
def render_mobject_strokes_with_matrix(renderer, model_matrix, mobjects):
# Precompute the total number of vertices for which to reserve space.
total_size = 0
for submob in mobjects:
total_size += submob.points.shape[0]
points = np.empty((total_size, 3))
colors = np.empty((total_size, 4))
widths = np.empty(total_size)
write_offset = 0
for submob in mobjects:
if not submob.has_points():
continue
end_offset = write_offset + submob.points.shape[0]
points[write_offset:end_offset] = submob.points
if submob.stroke_rgba.shape[0] == points[write_offset:end_offset].shape[0]:
colors[write_offset:end_offset] = submob.stroke_rgba
else:
colors[write_offset:end_offset] = np.repeat(
submob.stroke_rgba,
submob.points.shape[0],
axis=0,
)
widths[write_offset:end_offset] = np.repeat(
submob.stroke_width,
submob.points.shape[0],
)
write_offset = end_offset
stroke_data = np.zeros(
len(points),
dtype=[
# ("previous_curve", np.float32, (3, 3)),
("current_curve", np.float32, (3, 3)),
# ("next_curve", np.float32, (3, 3)),
("tile_coordinate", np.float32, (2,)),
("in_color", np.float32, (4,)),
("in_width", np.float32),
],
)
stroke_data["in_color"] = colors
stroke_data["in_width"] = widths
curves = np.reshape(points, (-1, 3, 3))
# stroke_data["previous_curve"] = np.repeat(np.roll(curves, 1, axis=0), 3, axis=0)
stroke_data["current_curve"] = np.repeat(curves, 3, axis=0)
# stroke_data["next_curve"] = np.repeat(np.roll(curves, -1, axis=0), 3, axis=0)
# Repeat each vertex in order to make a tile.
stroke_data = np.tile(stroke_data, 2)
stroke_data["tile_coordinate"] = np.vstack(
(
np.tile(
[
[0.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
],
(len(points) // 3, 1),
),
np.tile(
[
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
],
(len(points) // 3, 1),
),
),
)
shader = Shader(renderer.context, "vectorized_mobject_stroke")
shader.set_uniform(
"u_model_view_matrix",
opengl.matrix_to_shader_input(
renderer.camera.unformatted_view_matrix @ model_matrix,
),
)
shader.set_uniform("u_projection_matrix", renderer.scene.camera.projection_matrix)
shader.set_uniform("manim_unit_normal", tuple(-mobjects[0].unit_normal[0]))
vbo = renderer.context.buffer(stroke_data.tobytes())
vao = renderer.context.simple_vertex_array(
shader.shader_program, vbo, *stroke_data.dtype.names
)
renderer.frame_buffer_object.use()
vao.render()
vao.release()
vbo.release()

View file

@ -87,7 +87,7 @@ class ManimColor:
alpha: float = 1.0,
) -> None:
if value is None:
self._internal_value = np.array((0, 0, 0, alpha), dtype=ManimColorDType)
self._internal_value = np.array((1, 1, 1, alpha), dtype=ManimColorDType)
elif isinstance(value, ManimColor):
# logger.info(
# "ManimColor was passed another ManimColor. This is probably not what "
@ -509,6 +509,29 @@ class ManimColor:
self._internal_value * (1 - alpha) + other._internal_value * alpha
)
def set_opacity(self, opacity: float) -> ManimColor:
"""Sets the alpha value for the current ManimColor
Parameters
----------
opacity : float
Returns
-------
ManimColor
The color with the changed opacity value
Raises
------
ValueError
Raises an exception if the opacity value is not in range 0 to 1
"""
if opacity < 0 or opacity > 1:
raise ValueError(f"Alpha value is not in range 0-1 it is {opacity}")
self._internal_value[3] = opacity
return self
@classmethod
def from_rgb(
cls,

View file

@ -56,12 +56,9 @@ screeninfo = "^0.8"
Pygments = "^2.10.0"
"backports.cached-property" = { version = "^1.0.1", python = "<3.8" }
svgelements = "^1.8.0"
<<<<<<< HEAD
ipython = "^8.7.0"
pyopengl = "^3.1.6"
=======
typing-extensions = "^4.7.1"
>>>>>>> 50d663eb8b3b3ddb43fb20cce1943348abf9cc59
[tool.poetry.extras]
jupyterlab = ["jupyterlab", "notebook"]

View file

@ -0,0 +1,18 @@
import pytest
from manim import manim_colors as col
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
VMobject = OpenGLVMobject
def test_vmobject_init():
vm = VMobject(col.RED)
assert vm.fill_color == [col.RED]
assert vm.stroke_color == [col.RED]
vm = VMobject(col.GREEN, stroke_color=col.YELLOW)
assert vm.fill_color == [col.GREEN]
assert vm.stroke_color == [col.YELLOW]
vm = VMobject()
assert vm.fill_color == [col.WHITE]
assert vm.stroke_color == [col.WHITE]