mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
wip: copy data functions to opengl_renderer, preparing uniforms and shaders
This commit is contained in:
parent
aa18dc44d1
commit
9904627b19
13 changed files with 483 additions and 885 deletions
14
example_scenes/test_new.py
Normal file
14
example_scenes/test_new.py
Normal 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)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 *
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
90
manim/renderer/opengl_shader_program.py
Normal file
90
manim/renderer/opengl_shader_program.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
18
tests/experimental/test_vmobject_init.py
Normal file
18
tests/experimental/test_vmobject_init.py
Normal 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]
|
||||
Loading…
Add table
Add a link
Reference in a new issue