From 86f81a2ede230ebf33794454a42f9b6b122497fc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 5 Mar 2018 20:14:43 -0800 Subject: [PATCH] Add warning about light_source --- topics/light.py | 580 ++++++++++++++++++++++++------------------------ 1 file changed, 286 insertions(+), 294 deletions(-) diff --git a/topics/light.py b/topics/light.py index 2e239654..587d9c08 100644 --- a/topics/light.py +++ b/topics/light.py @@ -42,11 +42,293 @@ inverse_power_law = lambda maxint,scale,cutoff,exponent: \ inverse_quadratic = lambda maxint,scale,cutoff: inverse_power_law(maxint,scale,cutoff,2) +class SwitchOn(LaggedStart): + CONFIG = { + "lag_ratio": 0.2, + "run_time": SWITCH_ON_RUN_TIME + } -# Note: Overall, this class seems perfectly reasonable to me, the main -# thing to be wary of is that calling self.add(submob) puts that submob -# at the end of the submobjects list, and hence on top of everything else -# which is why the shadow might sometimes end up behind the spotlight + def __init__(self, light, **kwargs): + if (not isinstance(light,AmbientLight) and not isinstance(light,Spotlight)): + raise Exception("Only AmbientLights and Spotlights can be switched on") + LaggedStart.__init__( + self, FadeIn, light, **kwargs + ) + +class SwitchOff(LaggedStart): + CONFIG = { + "lag_ratio": 0.2, + "run_time": SWITCH_ON_RUN_TIME + } + + def __init__(self, light, **kwargs): + if (not isinstance(light,AmbientLight) and not isinstance(light,Spotlight)): + raise Exception("Only AmbientLights and Spotlights can be switched off") + light.submobjects = light.submobjects[::-1] + LaggedStart.__init__(self, + FadeOut, light, **kwargs) + light.submobjects = light.submobjects[::-1] + +class Lighthouse(SVGMobject): + CONFIG = { + "file_name" : "lighthouse", + "height" : LIGHTHOUSE_HEIGHT, + "fill_color" : WHITE, + "fill_opacity" : 1.0, + } + + def move_to(self,point): + self.next_to(point, DOWN, buff = 0) + +class AmbientLight(VMobject): + + # Parameters are: + # * a source point + # * an opacity function + # * a light color + # * a max opacity + # * a radius (larger than the opacity's dropoff length) + # * the number of subdivisions (levels, annuli) + + CONFIG = { + "source_point": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0), + "opacity_function" : lambda r : 1.0/(r+1.0)**2, + "color" : LIGHT_COLOR, + "max_opacity" : 1.0, + "num_levels" : NUM_LEVELS, + "radius" : 5.0 + } + + def generate_points(self): + # in theory, this method is only called once, right? + # so removing submobs shd not be necessary + # + # Note: Usually, yes, it is only called within Mobject.__init__, + # but there is no strong guarantee of that, and you may want certain + # update functions to regenerate points here and there. + for submob in self.submobjects: + self.remove(submob) + + self.add(self.source_point) + + # create annuli + self.radius = float(self.radius) + dr = self.radius / self.num_levels + for r in np.arange(0, self.radius, dr): + alpha = self.max_opacity * self.opacity_function(r) + annulus = Annulus( + inner_radius = r, + outer_radius = r + dr, + color = self.color, + fill_opacity = alpha + ) + annulus.move_to(self.get_source_point()) + self.add(annulus) + + + + def move_source_to(self,point): + #old_source_point = self.get_source_point() + #self.shift(point - old_source_point) + self.move_to(point) + + return self + + + + + + def get_source_point(self): + return self.source_point.get_location() + + + + + + + + def dimming(self,new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha / old_alpha + submob.set_fill(opacity = new_submob_alpha) + +class Spotlight(VMobject): + CONFIG = { + "source_point": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0), + "opacity_function" : lambda r : 1.0/(r/2+1.0)**2, + "color" : GREEN, # LIGHT_COLOR, + "max_opacity" : 1.0, + "num_levels" : 10, + "radius" : 10.0, + "screen" : None, + "camera_mob": None + } + + def projection_direction(self): + # Note: This seems reasonable, though for it to work you'd + # need to be sure that any 3d scene including a spotlight + # somewhere assigns that spotlights "camera" attribute + # to be the camera associated with that scene. + if self.camera_mob == None: + return OUT + else: + [phi, theta, r] = self.camera_mob.get_center() + v = np.array([np.sin(phi)*np.cos(theta), np.sin(phi)*np.sin(theta), np.cos(phi)]) + return v #/np.linalg.norm(v) + + def project(self,point): + v = self.projection_direction() + w = project_along_vector(point,v) + return w + + def get_source_point(self): + return self.source_point.get_location() + + def generate_points(self): + self.submobjects = [] + + self.add(self.source_point) + + if self.screen != None: + # look for the screen and create annular sectors + lower_angle, upper_angle = self.viewing_angles(self.screen) + self.radius = float(self.radius) + dr = self.radius / self.num_levels + lower_ray, upper_ray = self.viewing_rays(self.screen) + + for r in np.arange(0, self.radius, dr): + new_sector = self.new_sector(r,dr,lower_angle,upper_angle) + self.add(new_sector) + + def new_sector(self,r,dr,lower_angle,upper_angle): + alpha = self.max_opacity * self.opacity_function(r) + annular_sector = AnnularSector( + inner_radius = r, + outer_radius = r + dr, + color = self.color, + fill_opacity = alpha, + start_angle = lower_angle, + angle = upper_angle - lower_angle + ) + # rotate (not project) it into the viewing plane + rotation_matrix = z_to_vector(self.projection_direction()) + annular_sector.apply_matrix(rotation_matrix) + # now rotate it inside that plane + rotated_RIGHT = np.dot(RIGHT, rotation_matrix.T) + projected_RIGHT = self.project(RIGHT) + omega = angle_between_vectors(rotated_RIGHT,projected_RIGHT) + annular_sector.rotate(omega, axis = self.projection_direction()) + annular_sector.move_arc_center_to(self.get_source_point()) + + return annular_sector + + def viewing_angle_of_point(self,point): + # as measured from the positive x-axis + v1 = self.project(RIGHT) + v2 = self.project(np.array(point) - self.get_source_point()) + absolute_angle = angle_between_vectors(v1, v2) + # determine the angle's sign depending on their plane's + # choice of orientation. That choice is set by the camera + # position, i. e. projection direction + + if np.dot(self.projection_direction(),np.cross(v1, v2)) > 0: + return absolute_angle + else: + return -absolute_angle + + def viewing_angles(self,screen): + + screen_points = screen.get_anchors() + projected_screen_points = map(self.project,screen_points) + + viewing_angles = np.array(map(self.viewing_angle_of_point, + projected_screen_points)) + + lower_angle = upper_angle = 0 + if len(viewing_angles) != 0: + lower_angle = np.min(viewing_angles) + upper_angle = np.max(viewing_angles) + + if upper_angle - lower_angle > TAU/2: + lower_angle, upper_angle = upper_angle, lower_angle + TAU + return lower_angle, upper_angle + + def viewing_rays(self,screen): + + lower_angle, upper_angle = self.viewing_angles(screen) + projected_RIGHT = self.project(RIGHT)/np.linalg.norm(self.project(RIGHT)) + lower_ray = rotate_vector(projected_RIGHT,lower_angle, axis = self.projection_direction()) + upper_ray = rotate_vector(projected_RIGHT,upper_angle, axis = self.projection_direction()) + + return lower_ray, upper_ray + + def opening_angle(self): + l,u = self.viewing_angles(self.screen) + return u - l + + def start_angle(self): + l,u = self.viewing_angles(self.screen) + return l + + def stop_angle(self): + l,u = self.viewing_angles(self.screen) + return u + + def move_source_to(self,point): + self.source_point.set_location(np.array(point)) + #self.source_point.move_to(np.array(point)) + #self.move_to(point) + self.update_sectors() + return self + + def update_sectors(self): + if self.screen == None: + return + for submob in self.submobjects: + if type(submob) == AnnularSector: + lower_angle, upper_angle = self.viewing_angles(self.screen) + #dr = submob.outer_radius - submob.inner_radius + dr = self.radius / self.num_levels + new_submob = self.new_sector( + submob.inner_radius, dr, lower_angle, upper_angle + ) + # submob.points = new_submob.points + # submob.set_fill(opacity = 10 * self.opacity_function(submob.outer_radius)) + Transform(submob, new_submob).update(1) + + def dimming(self,new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + # Note: Maybe it'd be best to have a Shadow class so that the + # type can be checked directly? + if type(submob) != AnnularSector: + # it's the shadow, don't dim it + continue + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha/old_alpha + submob.set_fill(opacity = new_submob_alpha) + + def change_opacity_function(self,new_f): + self.opacity_function = new_f + dr = self.radius/self.num_levels + + sectors = [] + for submob in self.submobjects: + if type(submob) == AnnularSector: + sectors.append(submob) + + for (r,submob) in zip(np.arange(0,self.radius,dr),sectors): + if type(submob) != AnnularSector: + # it's the shadow, don't dim it + continue + alpha = self.opacity_function(r) + submob.set_fill(opacity = alpha) + +# Warning: This class is likely quite buggy. class LightSource(VMobject): # combines: # a lighthouse @@ -308,296 +590,6 @@ class LightSource(VMobject): # shift it closer to the camera so it is in front of the spotlight self.shadow.mark_paths_closed = True - -class SwitchOn(LaggedStart): - CONFIG = { - "lag_ratio": 0.2, - "run_time": SWITCH_ON_RUN_TIME - } - - def __init__(self, light, **kwargs): - if (not isinstance(light,AmbientLight) and not isinstance(light,Spotlight)): - raise Exception("Only AmbientLights and Spotlights can be switched on") - LaggedStart.__init__( - self, FadeIn, light, **kwargs - ) - -class SwitchOff(LaggedStart): - CONFIG = { - "lag_ratio": 0.2, - "run_time": SWITCH_ON_RUN_TIME - } - - def __init__(self, light, **kwargs): - if (not isinstance(light,AmbientLight) and not isinstance(light,Spotlight)): - raise Exception("Only AmbientLights and Spotlights can be switched off") - light.submobjects = light.submobjects[::-1] - LaggedStart.__init__(self, - FadeOut, light, **kwargs) - light.submobjects = light.submobjects[::-1] - -class Lighthouse(SVGMobject): - CONFIG = { - "file_name" : "lighthouse", - "height" : LIGHTHOUSE_HEIGHT, - "fill_color" : WHITE, - "fill_opacity" : 1.0, - } - - def move_to(self,point): - self.next_to(point, DOWN, buff = 0) - -class AmbientLight(VMobject): - - # Parameters are: - # * a source point - # * an opacity function - # * a light color - # * a max opacity - # * a radius (larger than the opacity's dropoff length) - # * the number of subdivisions (levels, annuli) - - CONFIG = { - "source_point": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0), - "opacity_function" : lambda r : 1.0/(r+1.0)**2, - "color" : LIGHT_COLOR, - "max_opacity" : 1.0, - "num_levels" : NUM_LEVELS, - "radius" : 5.0 - } - - def generate_points(self): - # in theory, this method is only called once, right? - # so removing submobs shd not be necessary - # - # Note: Usually, yes, it is only called within Mobject.__init__, - # but there is no strong guarantee of that, and you may want certain - # update functions to regenerate points here and there. - for submob in self.submobjects: - self.remove(submob) - - self.add(self.source_point) - - # create annuli - self.radius = float(self.radius) - dr = self.radius / self.num_levels - for r in np.arange(0, self.radius, dr): - alpha = self.max_opacity * self.opacity_function(r) - annulus = Annulus( - inner_radius = r, - outer_radius = r + dr, - color = self.color, - fill_opacity = alpha - ) - annulus.move_to(self.get_source_point()) - self.add(annulus) - - - - def move_source_to(self,point): - #old_source_point = self.get_source_point() - #self.shift(point - old_source_point) - self.move_to(point) - - return self - - - - - - def get_source_point(self): - return self.source_point.get_location() - - - - - - - - def dimming(self,new_alpha): - old_alpha = self.max_opacity - self.max_opacity = new_alpha - for submob in self.submobjects: - old_submob_alpha = submob.fill_opacity - new_submob_alpha = old_submob_alpha * new_alpha / old_alpha - submob.set_fill(opacity = new_submob_alpha) - - -class Spotlight(VMobject): - CONFIG = { - "source_point": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0), - "opacity_function" : lambda r : 1.0/(r/2+1.0)**2, - "color" : GREEN, # LIGHT_COLOR, - "max_opacity" : 1.0, - "num_levels" : 10, - "radius" : 10.0, - "screen" : None, - "camera_mob": None - } - - def projection_direction(self): - # Note: This seems reasonable, though for it to work you'd - # need to be sure that any 3d scene including a spotlight - # somewhere assigns that spotlights "camera" attribute - # to be the camera associated with that scene. - if self.camera_mob == None: - return OUT - else: - [phi, theta, r] = self.camera_mob.get_center() - v = np.array([np.sin(phi)*np.cos(theta), np.sin(phi)*np.sin(theta), np.cos(phi)]) - return v #/np.linalg.norm(v) - - def project(self,point): - v = self.projection_direction() - w = project_along_vector(point,v) - return w - - def get_source_point(self): - return self.source_point.get_location() - - def generate_points(self): - self.submobjects = [] - - self.add(self.source_point) - - if self.screen != None: - # look for the screen and create annular sectors - lower_angle, upper_angle = self.viewing_angles(self.screen) - self.radius = float(self.radius) - dr = self.radius / self.num_levels - lower_ray, upper_ray = self.viewing_rays(self.screen) - - for r in np.arange(0, self.radius, dr): - new_sector = self.new_sector(r,dr,lower_angle,upper_angle) - self.add(new_sector) - - def new_sector(self,r,dr,lower_angle,upper_angle): - alpha = self.max_opacity * self.opacity_function(r) - annular_sector = AnnularSector( - inner_radius = r, - outer_radius = r + dr, - color = self.color, - fill_opacity = alpha, - start_angle = lower_angle, - angle = upper_angle - lower_angle - ) - # rotate (not project) it into the viewing plane - rotation_matrix = z_to_vector(self.projection_direction()) - annular_sector.apply_matrix(rotation_matrix) - # now rotate it inside that plane - rotated_RIGHT = np.dot(RIGHT, rotation_matrix.T) - projected_RIGHT = self.project(RIGHT) - omega = angle_between_vectors(rotated_RIGHT,projected_RIGHT) - annular_sector.rotate(omega, axis = self.projection_direction()) - annular_sector.move_arc_center_to(self.get_source_point()) - - return annular_sector - - def viewing_angle_of_point(self,point): - # as measured from the positive x-axis - v1 = self.project(RIGHT) - v2 = self.project(np.array(point) - self.get_source_point()) - absolute_angle = angle_between_vectors(v1, v2) - # determine the angle's sign depending on their plane's - # choice of orientation. That choice is set by the camera - # position, i. e. projection direction - - if np.dot(self.projection_direction(),np.cross(v1, v2)) > 0: - return absolute_angle - else: - return -absolute_angle - - def viewing_angles(self,screen): - - screen_points = screen.get_anchors() - projected_screen_points = map(self.project,screen_points) - - viewing_angles = np.array(map(self.viewing_angle_of_point, - projected_screen_points)) - - lower_angle = upper_angle = 0 - if len(viewing_angles) != 0: - lower_angle = np.min(viewing_angles) - upper_angle = np.max(viewing_angles) - - if upper_angle - lower_angle > TAU/2: - lower_angle, upper_angle = upper_angle, lower_angle + TAU - return lower_angle, upper_angle - - def viewing_rays(self,screen): - - lower_angle, upper_angle = self.viewing_angles(screen) - projected_RIGHT = self.project(RIGHT)/np.linalg.norm(self.project(RIGHT)) - lower_ray = rotate_vector(projected_RIGHT,lower_angle, axis = self.projection_direction()) - upper_ray = rotate_vector(projected_RIGHT,upper_angle, axis = self.projection_direction()) - - return lower_ray, upper_ray - - def opening_angle(self): - l,u = self.viewing_angles(self.screen) - return u - l - - def start_angle(self): - l,u = self.viewing_angles(self.screen) - return l - - def stop_angle(self): - l,u = self.viewing_angles(self.screen) - return u - - def move_source_to(self,point): - self.source_point.set_location(np.array(point)) - #self.source_point.move_to(np.array(point)) - #self.move_to(point) - self.update_sectors() - return self - - def update_sectors(self): - if self.screen == None: - return - for submob in self.submobjects: - if type(submob) == AnnularSector: - lower_angle, upper_angle = self.viewing_angles(self.screen) - #dr = submob.outer_radius - submob.inner_radius - dr = self.radius / self.num_levels - new_submob = self.new_sector( - submob.inner_radius, dr, lower_angle, upper_angle - ) - # submob.points = new_submob.points - # submob.set_fill(opacity = 10 * self.opacity_function(submob.outer_radius)) - Transform(submob, new_submob).update(1) - - def dimming(self,new_alpha): - old_alpha = self.max_opacity - self.max_opacity = new_alpha - for submob in self.submobjects: - # Note: Maybe it'd be best to have a Shadow class so that the - # type can be checked directly? - if type(submob) != AnnularSector: - # it's the shadow, don't dim it - continue - old_submob_alpha = submob.fill_opacity - new_submob_alpha = old_submob_alpha * new_alpha/old_alpha - submob.set_fill(opacity = new_submob_alpha) - - def change_opacity_function(self,new_f): - self.opacity_function = new_f - dr = self.radius/self.num_levels - - sectors = [] - for submob in self.submobjects: - if type(submob) == AnnularSector: - sectors.append(submob) - - for (r,submob) in zip(np.arange(0,self.radius,dr),sectors): - if type(submob) != AnnularSector: - # it's the shadow, don't dim it - continue - alpha = self.opacity_function(r) - submob.set_fill(opacity = alpha) - - - class ScreenTracker(ContinualAnimation): def __init__(self, light_source, **kwargs): self.light_source = light_source