class PbLightNode(): """Pybullet-compatible light node wrapper """ def __init__(self, render: NodePath): self._alight = AmbientLight('pb_alight') self._dlight = DirectionalLight('pb_dlight') self._anode = render.attach_new_node(self._alight) self._dnode = render.attach_new_node(self._dlight) self._render = render self._is_active = False self.set_active(True) def set_active(self, active: bool): if active and not self._is_active: self._render.set_light(self._anode) self._render.set_light(self._dnode) elif not active and self._is_active: self._render.clear_light(self._anode) self._render.clear_light(self._dnode) self._is_active = active def is_active(self): return self._is_active def update(self, pb_light): self._alight.set_color(Vec3(*pb_light.ambient_color)) self._dlight.set_color(Vec3(*pb_light.diffuse_color)) self._dlight.set_specular_color(Vec3(*pb_light.specular_color)) self._dlight.set_shadow_caster(pb_light.shadow_caster) self._dnode.set_pos(Vec3(*pb_light.position)) self._dnode.look_at(0, 0, 0)
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)
def __init__(self, cad_file=None, output_size=(512, 512), light_on=True, cast_shadow=True): # acquire lock since showbase cannot be created twice Panda3DRenderer.__lock.acquire() # set output size and init the base loadPrcFileData('', f'win-size {output_size[0]} {output_size[1]}') base = ShowBase(windowType='offscreen') # coordinate for normalized and centered object obj_node = base.render.attach_new_node('normalized_obj') # ambient alight = AmbientLight('alight') alight.set_color(VBase4(0.5, 0.5, 0.5, 1.0)) alnp = base.render.attachNewNode(alight) base.render.setLight(alnp) # directional light for ambient dlight1 = DirectionalLight('dlight1') dlight1.set_color(VBase4(0.235, 0.235, 0.235, 1.0)) dlnp1 = base.render.attach_new_node(dlight1) dlnp1.set_pos(-2, 3, 1) dlnp1.look_at(obj_node) base.render.set_light(dlnp1) # point light for ambient plight1 = PointLight('plight1') plight1.set_color(VBase4(1.75, 1.75, 1.75, 1.0)) plight1.setAttenuation((1, 1, 1)) plnp1 = base.render.attach_new_node(plight1) plnp1.set_pos(0, 0, 3) plnp1.look_at(obj_node) base.render.set_light(plnp1) plight2 = PointLight('plight2') plight2.set_color(VBase4(1.5, 1.5, 1.5, 1.0)) plight2.setAttenuation((1, 0, 1)) plnp2 = base.render.attach_new_node(plight2) plnp2.set_pos(0, -3, 0) plnp2.look_at(obj_node) base.render.set_light(plnp2) dlight2 = DirectionalLight('dlight2') dlight2.set_color(VBase4(0.325, 0.325, 0.325, 1.0)) dlnp2 = base.render.attach_new_node(dlight2) dlnp2.set_pos(-1, 1, -1.65) dlnp2.look_at(obj_node) base.render.set_light(dlnp2) dlight3 = DirectionalLight('dlight3') dlight3.set_color(VBase4(0.15, 0.15, 0.15, 1.0)) dlnp3 = base.render.attach_new_node(dlight3) dlnp3.set_pos(-2.5, 2.5, 2.0) dlnp3.look_at(obj_node) base.render.set_light(dlnp3) if cast_shadow: lens = PerspectiveLens() dlight3.set_lens(lens) dlight3.set_shadow_caster(True, 1024, 1024) dlight4 = DirectionalLight('dlight4') dlight4.set_color(VBase4(0.17, 0.17, 0.17, 1.0)) dlnp4 = base.render.attach_new_node(dlight4) dlnp4.set_pos(1.2, -2.0, 2.5) dlnp4.look_at(obj_node) base.render.set_light(dlnp4) if cast_shadow: lens = PerspectiveLens() dlight4.set_lens(lens) dlight4.set_shadow_caster(True, 1024, 1024) self.direct_node = direct_node = base.render.attach_new_node( 'direct_light') dlnp2.reparent_to(direct_node) dlnp3.reparent_to(direct_node) dlnp4.reparent_to(direct_node) # auto shader for shadow if cast_shadow: base.render.setShaderAuto() # no culling base.render.set_two_sided(True) # anti-alias base.render.setAntialias(AntialiasAttrib.MMultisample, 8) # init camera position self.coverage = 0.5 # the default clear color self.clear_color = (0.0, 0.0, 0.0, 0.0) # translate in rendered image self.obj_translate = (0, 0) # light rotation self.light_hpr = (0, 0, 0) # object rotation self.obj_hpr = (0, 0, 0) self.base = base self.obj = None self.obj_node = obj_node self.cast_shadow = cast_shadow self.camera = base.camera if cad_file is not None: self.set_obj(cad_file) if not light_on: base.render.set_light_off()
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