class DirectionalLight(Light): def __init__(self, shadows=True, **kwargs): super().__init__() self._light = PandaDirectionalLight('directional_light') render.setLight(self.attachNewNode(self._light)) self.shadow_map_resolution = Vec2(1024, 1024) for key, value in kwargs.items(): setattr(self, key, value) invoke(setattr, self, 'shadows', shadows, delay=.1) @property def shadows(self): return self._shadows @shadows.setter def shadows(self, value): self._shadows = value if value: self._light.set_shadow_caster(True, int(self.shadow_map_resolution[0]), int(self.shadow_map_resolution[1])) bmin, bmax = scene.get_tight_bounds(self) lens = self._light.get_lens() lens.set_near_far(bmin.z * 2, bmax.z * 2) lens.set_film_offset((bmin.xy + bmax.xy) * .5) lens.set_film_size((bmax.xy - bmin.xy)) else: self._light.set_shadow_caster(False)
class Sky: """Encapsulates global illumination and fog.""" ambient: AmbientLight ambient_np: NodePath sun: DirectionalLight sun_np: NodePath fog: Fog def __init__(self, parent: NodePath, camera: Camera): self._camera = camera self._parent = parent self.hours = 0.0 self._init_sun() self._init_ambient() self._init_fog() def update(self): """Update lights.""" self.sun_np.set_pos(self._camera.focus, 0.0, 0.0, 0.0) film = p3d.camera.get_z() + self._camera.rotator.get_p() ** 2.0 * 0.01 self.sun.get_lens().set_film_size(film, film) self.sun.get_lens().set_near_far(-film, film) def set_time(self, hours: float): """Update lighting according to time in range 0.0 <= t < 24.0.""" hours %= 24 self.hours = hours self.sun_np.set_p(360.0 * (hours + 6.0) / 24.0) color = calculate_sky_color(hours) self.sun.set_color((5.0 * color[0], 5.0 * color[1], 5.0 * color[2], color[3])) self.fog.set_color(color) p3d.win.set_clear_color(color) def _init_sun(self): """Initialize sun light.""" self.sun = DirectionalLight('sun') # self.sun.set_shadow_caster(True, 2048, 2048) self.sun.get_lens().set_film_size(1024, 1024) self.sun.get_lens().set_near_far(-1024, 1024) self.sun.set_camera_mask(0x00010000) self.sun.set_color((1.0, 1.0, 1.0, 1.0)) # self.sun.show_frustum() self.sun_np = self._parent.attach_new_node(self.sun) self.sun_np.set_hpr(270.0, 0.0, 0.0) self.sun_np.set_pos(self._camera.focus, 0.0, 0.0, 512.0) self._parent.set_light(self.sun_np) def _init_ambient(self): """Initialize ambient light.""" self.ambient = AmbientLight('ambient') self.ambient.set_color((0.4, 0.4, 0.6, 1.0)) self.ambient_np = self._parent.attach_new_node(self.ambient) self._parent.set_light(self.ambient_np) def _init_fog(self): """Initialize distance fog.""" self.fog = Fog('fog') self.fog.set_linear_range(2000.0, 7000.0) if not ConfigVariableBool('use-shaders', False): self._parent.set_fog(self.fog)
class CastawayBase(ShowBase): """ The Showbase instance for the castaway example. Handles window and scene management """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Position the camera. Set a saner far distance. self.camera.set_pos(-35, -6, 6) self.camera.set_hpr(-74, -19, 91) self.camLens.set_far(250) self.disable_mouse() # Display instructions add_title("Panda3D Tutorial: Castaway Island") add_instructions(0.95, "[ESC]: Quit") add_instructions(0.90, '[TAB]: Toggle Buffer Viewer') add_instructions(0.85, '[F12]: Save Screenshot') add_instructions(0.80, '[F11]: Toggle Frame Rate Meter') add_instructions(0.75, '[F]: Toggle Sun Frustum') add_instructions(0.70, '[O]: Toggle OOBE mode') add_instructions(0.65, '[1]: Enable static adjustment mode') add_instructions(0.60, '[2]: Enable dynamic adjustment mode') add_instructions(0.55, '[3]: disable automatic adjustment') # Prepare scene graph self.scene_root = self.render.attach_new_node('castaway_scene') scene_shader = Shader.load(Shader.SL_GLSL, "resources/scene.vert", "resources/scene.frag") self.render.set_shader(scene_shader) self.render.set_shader_input('camera', self.camera) self.render.set_antialias(AntialiasAttrib.MAuto) # Load the island asset self.island = self.loader.load_model('resources/island') self.island.reparent_to(self.scene_root) self.island.set_p(90) self.island.flatten_strong() # Create water and fog instances self.load_water() self.load_fog() # Setup lighting self.load_lights() self.show_frustum = False self.taskMgr.add(self._adjust_lighting_bounds_task, sort=45) self.adjustment_mode = 0 # Setup key bindings for debuging self.accept('tab', self.bufferViewer.toggleEnable) self.accept('f12', self.screenshot) self.accept('o', self.oobe) self.accept('f11', self.toggle_frame_rate_meter) self.accept('1', self.set_adjust_mode, [0]) self.accept('2', self.set_adjust_mode, [1]) self.accept('3', self.set_adjust_mode, [2]) self.accept('f', self.toggle_frustum) self.accept('esc', sys.exit) def set_adjust_mode(self, mode): """ Sets the currently enabled adjustment mode """ if mode < 0 or mode > 2: mode = 0 print('Setting adjustment mode: %s' % mode) self.adjustment_mode = mode def toggle_frustum(self): """ Toggles the sun lights frustum viewer """ if self.show_frustum: self.sun_light.hide_frustum() else: self.sun_light.show_frustum() self.show_frustum = not self.show_frustum def toggle_frame_rate_meter(self): """ Toggles the frame rate meter's state """ self.set_frame_rate_meter(self.frameRateMeter == None) def load_water(self): """ Loads the islands psuedo infinite water plane """ # Create a material for the PBR shaders water_material = Material() water_material.set_base_color(VBase4(0, 0.7, 0.9, 1)) water_card_maker = CardMaker('water_card') water_card_maker.set_frame(-200, 200, -150, 150) self.water_path = self.render.attach_new_node( water_card_maker.generate()) self.water_path.set_material(water_material, 1) self.water_path.set_scale(500) def load_fog(self): """ Loads the fog seen in the distance from the island """ self.world_fog = Fog('world_fog') self.world_fog.set_color( Vec3(SKY_COLOR.get_x(), SKY_COLOR.get_y(), SKY_COLOR.get_z())) self.world_fog.set_linear_range(0, 320) self.world_fog.set_linear_fallback(45, 160, 320) self.world_fog_path = self.render.attach_new_node(self.world_fog) self.render.set_fog(self.world_fog) def load_lights(self): """ Loads the scene lighting objects """ # Create a sun source self.sun_light = DirectionalLight('sun_light') self.sun_light.set_color_temperature(SUN_TEMPERATURE) self.sun_light.color = self.sun_light.color * 4 self.sun_light_path = self.render.attach_new_node(self.sun_light) self.sun_light_path.set_pos(10, -10, -10) self.sun_light_path.look_at(0, 0, 0) self.sun_light_path.hprInterval( 10.0, (self.sun_light_path.get_h(), self.sun_light_path.get_p() - 360, self.sun_light_path.get_r()), bakeInStart=True).loop() self.render.set_light(self.sun_light_path) self.sun_light.get_lens().set_near_far(1, 30) self.sun_light.get_lens().set_film_size(20, 40) self.sun_light.set_shadow_caster(True, 4096, 4096) # Create a sky light self.sky_light = AmbientLight('sky_light') self.sky_light.set_color(VBase4(SKY_COLOR * 0.04, 1)) self.sky_light_path = self.render.attach_new_node(self.sky_light) self.render.set_light(self.sky_light_path) self.set_background_color(SKY_COLOR) def adjust_colors(self, color): """ Adjusts the scene's current time of day """ self.set_background_color(color) self.sky_light.set_color(color) self.world_fog.set_color(color) def adjust_lighting_static(self): """ This method tightly fits the light frustum around the entire scene. Because it is computationally expensive for complex scenes, it is intended to be used for non-rotating light sources, and called once at loading time. """ bmin, bmax = self.scene_root.get_tight_bounds(self.sun_light_path) lens = self.sun_light.get_lens() lens.set_film_offset((bmin.xz + bmax.xz) * 0.5) lens.set_film_size(bmax.xz - bmin.xz) lens.set_near_far(bmin.y, bmax.y) def adjust_lighting_dynamic(self): """ This method is much faster, but not nearly as tightly fitting. May (or may not) work better with "bounds-type best" in Config.prc. It will automatically try to reduce the shadow frustum size in order not to shadow objects that are out of view. Additionally, it will disable the shadow camera if the scene bounds are completely out of view of the shadow camera. """ # Get Panda's precomputed scene bounds. scene_bounds = self.scene_root.get_bounds() scene_bounds.xform(self.scene_root.get_mat(self.sun_light_path)) # Also transform the bounding volume of the camera frustum to light space. lens_bounds = self.camLens.make_bounds() lens_bounds.xform(self.camera.get_mat(self.sun_light_path)) # Does the lens bounds contain the scene bounds? intersection = lens_bounds.contains(scene_bounds) if not intersection: # No; deactivate the shadow camera. self.sun_light.set_active(False) return self.sun_light.set_active(True) bmin = scene_bounds.get_min() bmax = scene_bounds.get_max() if intersection & BoundingVolume.IF_all: # Completely contains the world volume; no adjustment necessary. pass else: # Adjust all dimensions to tighten around the view frustum bounds, # except for the near distance, because objects that are out of view # in that direction may still cast shadows. lmin = lens_bounds.get_min() lmax = lens_bounds.get_max() bmin[0] = min(max(bmin[0], lmin[0]), lmax[0]) bmin[1] = min(bmin[1], lmax[1]) bmin[2] = min(max(bmin[2], lmin[2]), lmax[2]) lens = self.sun_light.get_lens() lens.set_film_offset((bmin.xz + bmax.xz) * 0.5) lens.set_film_size(bmax.xz - bmin.xz) lens.set_near_far(bmin.y, bmax.y) def _adjust_lighting_bounds_task(self, task): """ Calls the adjust_lighting_bounds function between the ivalLoop(20) and the igLoop(50) """ if self.adjustment_mode == 0: self.adjust_lighting_static() elif self.adjustment_mode == 1: self.adjust_lighting_dynamic() return task.cont