ParametricSurface replaces the role of SurfaceMobject, and instead of tracking normals infinitesimal nudges are tracked

This commit is contained in:
Grant Sanderson 2020-06-05 11:12:52 -07:00
parent efe15cf2f0
commit cacfe67fe1
10 changed files with 155 additions and 138 deletions

View file

@ -174,6 +174,7 @@ class Camera(object):
moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA,
moderngl.ONE, moderngl.ONE
)
self.ctx.multisample = True # Testing
self.background_fbo = None
def init_light_source(self):

View file

@ -50,7 +50,7 @@ from manimlib.mobject.svg.text_mobject import *
from manimlib.mobject.three_dimensions import *
from manimlib.mobject.types.image_mobject import *
from manimlib.mobject.types.point_cloud_mobject import *
from manimlib.mobject.types.surface_mobject import *
from manimlib.mobject.types.surface import *
from manimlib.mobject.types.vectorized_mobject import *
from manimlib.mobject.mobject_update_utils import *
from manimlib.mobject.value_tracker import *

View file

@ -1,6 +1,6 @@
from manimlib.constants import *
from manimlib.mobject.geometry import Square
from manimlib.mobject.types.surface_mobject import ParametricSurface
from manimlib.mobject.types.surface import ParametricSurface
from manimlib.mobject.types.vectorized_mobject import VGroup
@ -8,7 +8,7 @@ from manimlib.mobject.types.vectorized_mobject import VGroup
class Sphere(ParametricSurface):
CONFIG = {
"resolution": (96, 48),
"resolution": (100, 50),
"radius": 1,
"u_range": (0, TAU),
"v_range": (0, PI),
@ -18,10 +18,19 @@ class Sphere(ParametricSurface):
return self.radius * np.array([
np.cos(u) * np.sin(v),
np.sin(u) * np.sin(v),
np.cos(v)
-np.cos(v)
])
class Clyinder(ParametricSurface):
CONFIG = {
}
def func(self, u, v):
pass
class Cube(VGroup):
CONFIG = {
"fill_opacity": 0.75,

View file

@ -7,96 +7,32 @@ from manimlib.constants import *
from manimlib.mobject.mobject import Mobject
from manimlib.utils.color import color_to_rgba
from manimlib.utils.images import get_full_raster_image_path
from manimlib.utils.space_ops import normalize_along_axis
class SurfaceMobject(Mobject):
class ParametricSurface(Mobject):
CONFIG = {
"u_range": (0, 1),
"v_range": (0, 1),
"resolution": (100, 100),
"color": GREY,
"opacity": 1,
"opacity": 1.0,
"gloss": 0.3,
# For du and dv steps. Much smaller and numerical error
# can crop up in the shaders.
"epsilon": 1e-5,
"render_primative": moderngl.TRIANGLES,
"vert_shader_file": "surface_vert.glsl",
"frag_shader_file": "surface_frag.glsl",
"shader_dtype": [
('point', np.float32, (3,)),
('normal', np.float32, (3,)),
('du_point', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('color', np.float32, (4,)),
('gloss', np.float32, (1,)),
]
}
def __init__(self, **kwargs):
super().__init__(**kwargs)
def init_points(self):
self.points = np.zeros((0, self.dim))
self.normals = np.zeros((0, self.dim))
def init_colors(self):
self.set_color(self.color, self.opacity)
def set_points(self, points, normals=None):
self.points = np.array(points)
if normals is None:
v01 = points[1:-1] - points[:-2]
v02 = points[2:] - points[:-2]
crosses = np.cross(v01, v02)
crosses[1::2] *= -1 # Because of reversed orientation of every other triangle in the strip
self.normals = np.vstack([
crosses,
crosses[-1:].repeat(2, 0) # Repeat last entry twice
])
else:
self.normals = np.array(normals)
def set_color(self, color, opacity):
# TODO, allow for multiple colors
rgba = color_to_rgba(color, opacity)
self.rgbas = np.array([rgba])
def set_gloss(self, gloss, family=True):
self.gloss = gloss
if family:
for sm in self.submobjects:
sm.set_gloss(gloss, family)
return self
def apply_function(self, function, **kwargs):
# Apply it to infinitesimal neighbors to preserve normals
pass
def rotate(self, axis, angle, **kwargs):
# Account for normals
pass
def stretch(self, factor, dim, **kwargs):
# Account for normals
pass
def get_shader_data(self):
data = self.get_blank_shader_data_array(len(self.points))
data["point"] = self.points
data["normal"] = self.normals
data["color"] = self.rgbas
data["gloss"] = self.gloss
return data
class ParametricSurface(SurfaceMobject):
CONFIG = {
"u_range": (0, 1),
"v_range": (0, 1),
"resolution": (100, 100),
"surface_piece_config": {},
"fill_color": BLUE_D,
"fill_opacity": 1.0,
"checkerboard_colors": [BLUE_D, BLUE_E],
"stroke_color": LIGHT_GREY,
"stroke_width": 0.5,
"should_make_jagged": False,
"pre_function_handle_to_anchor_scale_factor": 0.00001,
}
def __init__(self, function=None, **kwargs):
if function is None:
self.uv_func = self.func
@ -104,30 +40,28 @@ class ParametricSurface(SurfaceMobject):
self.uv_func = function
super().__init__(**kwargs)
def func(self, u, v):
raise Exception("Not implemented")
def init_points(self):
epsilon = 1e-6 # For differentials
nu, nv = self.resolution
u_range = np.linspace(*self.u_range, nu + 1)
v_range = np.linspace(*self.v_range, nv + 1)
# List of three grids, [Pure uv values, those nudged by du, those nudged by dv]
uv_grids = [
np.array([[[u + du, v + dv] for v in v_range] for u in u_range])
for (du, dv) in [(0, 0), (epsilon, 0), (0, epsilon)]
]
point_grid, points_nudged_du, points_nudged_dv = [
np.apply_along_axis(lambda p: self.uv_func(*p), 2, uv_grid)
for uv_grid in uv_grids
]
normal_grid = np.cross(
(points_nudged_du - point_grid) / epsilon,
(points_nudged_dv - point_grid) / epsilon,
)
self.set_points(
self.get_triangle_ready_array_from_grid(point_grid),
self.get_triangle_ready_array_from_grid(normal_grid),
)
# Get three lists:
# - Points generated by pure uv values
# - Those generated by values nudged by du
# - Those generated by values nudged by dv
point_lists = []
for (du, dv) in [(0, 0), (self.epsilon, 0), (0, self.epsilon)]:
uv_grid = np.array([[[u + du, v + dv] for v in v_range] for u in u_range])
point_grid = np.apply_along_axis(lambda p: self.uv_func(*p), 2, uv_grid)
point_lists.append(self.get_triangle_ready_array_from_grid(point_grid))
# Rather than tracking normal vectors, the points list will hold on to the
# infinitesimal nudged values alongside the original values. This way, one
# can perform all the manipulations they'd like to the surface, and normals
# are still easily recoverable.
self.points = np.vstack(point_lists)
def get_triangle_ready_array_from_grid(self, grid):
# Given a grid, say of points or normals, this returns an Nx3 array
@ -146,17 +80,66 @@ class ParametricSurface(SurfaceMobject):
arr[5::6] = grid[+1:, +1:].reshape((nu * nv, dim)) # Bottom right
return arr
def func(self, u, v):
raise Exception("Not implemented")
def init_colors(self):
self.set_color(self.color, self.opacity)
def get_surface_points_and_nudged_points(self):
k = len(self.points) // 3
return self.points[:k], self.points[k:2 * k], self.points[2 * k:]
def get_unit_normals(self):
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
normals = np.cross(
(du_points - s_points) / self.epsilon,
(dv_points - s_points) / self.epsilon,
)
return normalize_along_axis(normals, 1)
def set_color(self, color, opacity=1.0, family=True):
# TODO, allow for multiple colors
rgba = color_to_rgba(color, opacity)
self.rgbas = np.array([rgba])
if family:
for submob in self.submobjects:
submob.set_color(color, opacity)
def set_opacity(self, opacity, family=True):
self.rgbas[:, 3] = opacity
if family:
for sm in self.submobjects:
sm.set_opacity(opacity, family)
return self
def set_gloss(self, gloss, family=True):
self.gloss = gloss
if family:
for sm in self.submobjects:
sm.set_gloss(gloss, family)
return self
def get_shader_data(self):
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
data = self.get_blank_shader_data_array(len(s_points))
data["point"] = s_points
data["du_point"] = du_points
data["dv_point"] = dv_points
self.fill_in_shader_color_info(data)
return data
def fill_in_shader_color_info(self, data):
data["color"] = self.rgbas
data["gloss"] = self.gloss
return data
class TexturedSurfaceMobject(SurfaceMobject):
class TexturedSurface(ParametricSurface):
CONFIG = {
"vert_shader_file": "textured_surface_vert.glsl",
"frag_shader_file": "textured_surface_frag.glsl",
"shader_dtype": [
('point', np.float32, (3,)),
('normal', np.float32, (3,)),
('du_point', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('im_coords', np.float32, (2,)),
('opacity', np.float32, (1,)),
('gloss', np.float32, (1,)),
@ -165,25 +148,25 @@ class TexturedSurfaceMobject(SurfaceMobject):
def __init__(self, uv_surface, filename, **kwargs):
if not isinstance(uv_surface, ParametricSurface):
raise Exception("uv_surface must be of type Paramet")
raise Exception("uv_surface must be of type ParametricSurface")
path = get_full_raster_image_path(filename)
self.image = Image.open(path)
self.texture_path = path
self.uv_surface = uv_surface
super().__init__(**kwargs)
self.set_points(
uv_surface.points,
uv_surface.normals,
)
self.opacity = uv_surface.rgbas[:, 3]
self.gloss = uv_surface.gloss
def init_points(self):
self.points = self.uv_surface.points
# Init im_coords
nu, nv = uv_surface.resolution
nu, nv = self.uv_surface.resolution
u_range = np.linspace(0, 1, nu + 1)
v_range = np.linspace(0, 1, nv + 1) # Upsidedown?
v_range = np.linspace(1, 0, nv + 1) # Reverse y-direction
uv_grid = np.array([[[u, v] for v in v_range] for u in u_range])
self.im_coords = uv_surface.get_triangle_ready_array_from_grid(uv_grid)
self.im_coords = self.uv_surface.get_triangle_ready_array_from_grid(uv_grid)
def init_colors(self):
self.opacity = self.uv_surface.rgbas[:, 3]
self.gloss = self.uv_surface.gloss
def set_opacity(self, opacity, family=True):
self.opacity = opacity
@ -192,10 +175,7 @@ class TexturedSurfaceMobject(SurfaceMobject):
sm.set_opacity(opacity, family)
return self
def get_shader_data(self):
data = self.get_blank_shader_data_array(len(self.points))
data["point"] = self.points
data["normal"] = self.normals
def fill_in_shader_color_info(self, data):
data["im_coords"] = self.im_coords
data["opacity"] = self.opacity
data["gloss"] = self.gloss

View file

@ -1,8 +1,7 @@
vec4 add_light(vec4 raw_color, vec3 point, vec3 unit_normal, vec3 light_coords, float gloss){
if(gloss == 0.0) return raw_color;
// TODO, do we actually want this? For VMobjects its nice to just choose whichever unit normal
// is pointing towards the camera.
// TODO, do we actually want this? It effectively treats surfaces as two-sided
if(unit_normal.z < 0){
unit_normal *= -1;
}
@ -15,8 +14,9 @@ vec4 add_light(vec4 raw_color, vec3 point, vec3 unit_normal, vec3 light_coords,
float dot_prod = dot(normalize(light_reflection), normalize(to_camera));
float shine = gloss * exp(-3 * pow(1 - dot_prod, 2));
float dp2 = dot(normalize(to_light), unit_normal);
float mult = ((dp2 + 2.0) / 3.0);
return vec4(
((dp2 + 3.0) / 4.0) * mix(raw_color.rgb, vec3(1.0), shine),
mult * mix(raw_color.rgb, vec3(1.0), shine),
raw_color.a
);
}

View file

@ -0,0 +1,17 @@
// Only inlucde in an environment with to_screen_space defined
vec3 get_rotated_surface_unit_normal_vector(vec3 point, vec3 du_point, vec3 dv_point){
// normal = get_unit_normal(point, du_point, dv_point);
// return normalize((to_screen_space * vec4(normal, 0.0)).xyz);
vec3 cp = cross(
(du_point - point),
(dv_point - point)
);
if(length(cp) == 0){
// Instead choose a normal to just dv_point - point in the direction of point
vec3 v2 = dv_point - point;
cp = cross(cross(v2, point), v2);
}
// The zero is deliberate, as we only want to rotate and not shift
return normalize((to_screen_space * vec4(cp, 0.0)).xyz);
}

View file

@ -1,9 +1,7 @@
// Must be used in an environment with the following uniforms:
// uniform mat4 to_screen_space;
// uniform float focal_distance;
vec3 position_point_into_frame(vec3 point){
// Apply the pre-computed to_screen_space matrix.
vec4 new_point = to_screen_space * vec4(point, 1);
return new_point.xyz;
// Simply apply the pre-computed to_screen_space matrix.
return (to_screen_space * vec4(point, 1)).xyz;
}

View file

@ -1,8 +1,19 @@
#version 330
// in vec4 v_color;
// out vec4 frag_color;
uniform vec3 light_source_position;
in vec3 xyz_coords;
in vec3 v_normal;
in vec4 v_color;
in float v_gloss;
out vec4 frag_color;
#INSERT add_light.glsl
void main() {
frag_color = v_color;
frag_color = add_light(v_color, xyz_coords, normalize(v_normal), light_source_position, v_gloss);
}

View file

@ -6,26 +6,26 @@ uniform mat4 to_screen_space;
uniform float focal_distance;
uniform vec3 light_source_position;
// uniform sampler2D Texture;
in vec3 point;
in vec3 normal;
// in vec2 im_coords;
in vec3 du_point;
in vec3 dv_point;
in vec4 color;
in float gloss;
// out vec2 v_im_coords;
out vec3 xyz_coords;
out vec3 v_normal;
out vec4 v_color;
out float v_gloss;
// These lines will get replaced
#INSERT position_point_into_frame.glsl
#INSERT get_gl_Position.glsl
#INSERT add_light.glsl
#INSERT get_rotated_surface_unit_normal_vector.glsl
void main(){
vec3 xyz_coords = position_point_into_frame(point);
vec3 unit_normal = normalize(position_point_into_frame(normal));
// v_im_coords = im_coords;
v_color = add_light(color, xyz_coords, unit_normal, light_source_position, gloss);
xyz_coords = position_point_into_frame(point);
v_normal = get_rotated_surface_unit_normal_vector(point, du_point, dv_point);
v_color = color;
v_gloss = gloss;
gl_Position = get_gl_Position(xyz_coords);
}

View file

@ -9,7 +9,8 @@ uniform vec3 light_source_position;
// uniform sampler2D Texture;
in vec3 point;
in vec3 normal;
in vec3 du_point;
in vec3 dv_point;
in vec2 im_coords;
in float opacity;
in float gloss;
@ -20,14 +21,14 @@ out vec2 v_im_coords;
out float v_opacity;
out float v_gloss;
// These lines will get replaced
#INSERT position_point_into_frame.glsl
#INSERT get_gl_Position.glsl
#INSERT get_rotated_surface_unit_normal_vector.glsl
void main(){
xyz_coords = position_point_into_frame(point);
v_normal = position_point_into_frame(normal);
v_normal = get_rotated_surface_unit_normal_vector(point, du_point, dv_point);
v_im_coords = im_coords;
v_opacity = opacity;
v_gloss = gloss;