def _createDebugTexts(self): """ Creates a debug overlay to show GI status """ self.debugText = None self.buildingText = None if self.pipeline.settings.displayDebugStats: self.debugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.88), rightAligned=True, color=Vec3(1, 1, 0), size=0.03) self.buildingText = FastText(pos=Vec2(-0.3, 0), rightAligned=False, color=Vec3(1, 1, 0), size=0.03) self.buildingText.setText("PREPARING GI, PLEASE BE PATIENT ....")
def _createDebugTexts(self): """ Creates a debug overlay if specified in the pipeline settings """ self.lightsVisibleDebugText = None self.lightsUpdatedDebugText = None if self.pipeline.settings.displayDebugStats: self.lightsVisibleDebugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.84), rightAligned=True, color=Vec3(1, 1, 0), size=0.03) self.lightsUpdatedDebugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.8), rightAligned=True, color=Vec3(1, 1, 0), size=0.03)
class GlobalIllumination(DebugObject): """ This class handles the global illumination processing. To process the global illumination, the scene is first rasterized from 3 directions, and a 3D voxel grid is created. After that, the mipmaps of the voxel grid are generated. The final shader then performs voxel cone tracing to compute an ambient, diffuse and specular term. The gi is split over several frames to reduce the computation cost. Currently there are 5 different steps, split over 4 frames: Frame 1: - Rasterize the scene from the x-axis Frame 2: - Rasterize the scene from the y-axis Frame 3: - Rasterize the scene from the z-axis Frame 4: - Copy the generated temporary voxel grid into a stable voxel grid - Generate the mipmaps for that stable voxel grid using a gaussian filter In the final pass the stable voxel grid is sampled. The voxel tracing selects the mipmap depending on the cone size. This enables small scale details as well as blurry reflections and low-frequency ao / diffuse. For performance reasons, the final pass is executed at half window resolution and then bilateral upscaled. """ QualityLevels = ["Low", "Medium", "High", "Ultra"] def __init__(self, pipeline): DebugObject.__init__(self, "GlobalIllumnination") self.pipeline = pipeline self.qualityLevel = self.pipeline.settings.giQualityLevel if self.qualityLevel not in self.QualityLevels: self.fatal("Unsupported gi quality level:" + self.qualityLevel) self.qualityLevelIndex = self.QualityLevels.index(self.qualityLevel) # Grid size in world space units self.voxelGridSize = self.pipeline.settings.giVoxelGridSize # Grid resolution in pixels self.voxelGridResolution = [32, 64, 128, 192][self.qualityLevelIndex] # Has to be a multiple of 2 self.distributionSteps = [16, 30, 60, 90][self.qualityLevelIndex] self.slideCount = int(self.voxelGridResolution / 8) self.slideVertCount = self.voxelGridResolution / self.slideCount self.bounds = BoundingBox() self.renderCount = 0 # Create the task manager self.taskManager = DistributedTaskManager() self.gridPosLive = PTALVecBase3f.emptyArray(1) self.gridPosTemp = PTALVecBase3f.emptyArray(1) # Store ready state self.readyStateFlag = PTAFloat.emptyArray(1) self.readyStateFlag[0] = 0 self.frameIndex = 0 self.steps = [] def _createDebugTexts(self): """ Creates a debug overlay to show GI status """ self.debugText = None self.buildingText = None if self.pipeline.settings.displayDebugStats: self.debugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.88), rightAligned=True, color=Vec3(1, 1, 0), size=0.03) self.buildingText = FastText(pos=Vec2(-0.3, 0), rightAligned=False, color=Vec3(1, 1, 0), size=0.03) self.buildingText.setText("PREPARING GI, PLEASE BE PATIENT ....") def stepVoxelize(self, idx): # If we are at the beginning of the frame, compute the new grid position if idx == 0: self.gridPosTemp[0] = self._computeGridPos() # Clear voxel grid at the beginning # for tex in self.generationTextures: # tex.clearImage() self.clearGridTarget.setActive(True) if self.debugText is not None: self.debugText.setText("GI Grid Center: " + ", ".join( str(round(i, 2)) for i in self.gridPosTemp[0]) + " / GI Frame " + str(self.renderCount)) self.renderCount += 1 if self.renderCount == 3: self.readyStateFlag[0] = 1.0 if self.buildingText: self.buildingText.remove() self.buildingText = None self.voxelizePass.voxelizeSceneFromDirection(self.gridPosTemp[0], "xyz"[idx]) def stepDistribute(self, idx): if idx == 0: skyBegin = 142.0 skyInGrid = (skyBegin - self.gridPosTemp[0].z) / (2.0 * self.voxelGridSize) skyInGrid = int(skyInGrid * self.voxelGridResolution) self.convertGridTarget.setShaderInput("skyStartZ", skyInGrid) self.convertGridTarget.setActive(True) self.distributeTarget.setActive(True) swap = idx % 2 == 0 sources = self.pingDataTextures if swap else self.pongDataTextures dests = self.pongDataTextures if swap else self.pingDataTextures if idx == self.distributionSteps - 1: self.publishGrid() dests = self.dataTextures for i, direction in enumerate(self.directions): self.distributeTarget.setShaderInput("src" + direction, sources[i]) self.distributeTarget.setShaderInput("dst" + direction, dests[i]) # Only do the last blur-step on high+ quality, leads to artifacts otherwise # due to the low grid resolution if self.qualityLevel in ["High", "Ultra"]: self.distributeTarget.setShaderInput( "isLastStep", idx >= self.distributionSteps - 1) self.distributeTarget.setShaderInput("writeSolidness", idx >= self.distributionSteps - 1) def publishGrid(self): """ This function gets called when the grid is ready to be used, and updates the live grid data """ self.gridPosLive[0] = self.gridPosTemp[0] self.bounds.setMinMax(self.gridPosLive[0] - Vec3(self.voxelGridSize), self.gridPosLive[0] + Vec3(self.voxelGridSize)) def getBounds(self): """ Returns the bounds of the gi grid """ return self.bounds def update(self): """ Processes the gi, this method is called every frame """ # Disable all buffers here before starting the rendering self.disableTargets() # for target in self.mipmapTargets: # target.setActive(False) self.taskManager.process() def disableTargets(self): """ Disables all active targets """ self.voxelizePass.setActive(False) self.convertGridTarget.setActive(False) self.clearGridTarget.setActive(False) self.distributeTarget.setActive(False) def setup(self): """ Setups everything for the GI to work """ assert (self.distributionSteps % 2 == 0) self._createDebugTexts() self.pipeline.getRenderPassManager().registerDefine( "USE_GLOBAL_ILLUMINATION", 1) self.pipeline.getRenderPassManager().registerDefine( "GI_SLIDE_COUNT", self.slideCount) self.pipeline.getRenderPassManager().registerDefine( "GI_QUALITY_LEVEL", self.qualityLevelIndex) # make the grid resolution a constant self.pipeline.getRenderPassManager().registerDefine( "GI_GRID_RESOLUTION", self.voxelGridResolution) self.taskManager.addTask(3, self.stepVoxelize) self.taskManager.addTask(self.distributionSteps, self.stepDistribute) # Create the voxelize pass which is used to voxelize the scene from # several directions self.voxelizePass = VoxelizePass(self.pipeline) self.voxelizePass.setVoxelGridResolution(self.voxelGridResolution) self.voxelizePass.setVoxelGridSize(self.voxelGridSize) self.voxelizePass.setGridResolutionMultiplier(1) self.pipeline.getRenderPassManager().registerPass(self.voxelizePass) self.generationTextures = [] # Create the buffers used to create the voxel grid for color in "rgb": tex = Texture("VoxelGeneration-" + color) tex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TInt, Texture.FR32) tex.setClearColor(Vec4(0)) self.generationTextures.append(tex) Globals.render.setShaderInput("voxelGenDest" + color.upper(), tex) MemoryMonitor.addTexture("VoxelGenerationTex-" + color.upper(), tex) self.bindTo(Globals.render, "giData") self.convertGridTarget = RenderTarget("ConvertGIGrid") self.convertGridTarget.setSize( self.voxelGridResolution * self.slideCount, self.voxelGridResolution * self.slideVertCount) if self.pipeline.settings.useDebugAttachments: self.convertGridTarget.addColorTexture() self.convertGridTarget.prepareOffscreenBuffer() # Set a near-filter to the texture if self.pipeline.settings.useDebugAttachments: self.convertGridTarget.getColorTexture().setMinfilter( Texture.FTNearest) self.convertGridTarget.getColorTexture().setMagfilter( Texture.FTNearest) self.clearGridTarget = RenderTarget("ClearGIGrid") self.clearGridTarget.setSize( self.voxelGridResolution * self.slideCount, self.voxelGridResolution * self.slideVertCount) if self.pipeline.settings.useDebugAttachments: self.clearGridTarget.addColorTexture() self.clearGridTarget.prepareOffscreenBuffer() for idx, color in enumerate("rgb"): self.convertGridTarget.setShaderInput( "voxelGenSrc" + color.upper(), self.generationTextures[idx]) self.clearGridTarget.setShaderInput("voxelGenTex" + color.upper(), self.generationTextures[idx]) # Create the data textures self.dataTextures = [] self.directions = ["PosX", "NegX", "PosY", "NegY", "PosZ", "NegZ"] for i, direction in enumerate(self.directions): tex = Texture("GIDataTex" + direction) tex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR11G11B10) MemoryMonitor.addTexture("VoxelDataTex-" + direction, tex) self.dataTextures.append(tex) self.pipeline.getRenderPassManager().registerStaticVariable( "giVoxelData" + direction, tex) # Create ping / pong textures self.pingDataTextures = [] self.pongDataTextures = [] for i, direction in enumerate(self.directions): texPing = Texture("GIPingDataTex" + direction) texPing.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR11G11B10) MemoryMonitor.addTexture("VoxelPingDataTex-" + direction, texPing) self.pingDataTextures.append(texPing) texPong = Texture("GIPongDataTex" + direction) texPong.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR11G11B10) MemoryMonitor.addTexture("VoxelPongDataTex-" + direction, texPong) self.pongDataTextures.append(texPong) self.convertGridTarget.setShaderInput("voxelDataDest" + direction, self.pingDataTextures[i]) # self.clearGridTarget.setShaderInput("voxelDataDest" + str(i), self.pongDataTextures[i]) # Set texture wrap modes for tex in self.pingDataTextures + self.pongDataTextures + self.dataTextures + self.generationTextures: tex.setMinfilter(Texture.FTLinear) tex.setMagfilter(Texture.FTLinear) tex.setWrapU(Texture.WMBorderColor) tex.setWrapV(Texture.WMBorderColor) tex.setWrapW(Texture.WMBorderColor) tex.setAnisotropicDegree(0) tex.setBorderColor(Vec4(0)) for tex in self.dataTextures: tex.setMinfilter(Texture.FTLinear) tex.setMagfilter(Texture.FTLinear) self.distributeTarget = RenderTarget("DistributeVoxels") self.distributeTarget.setSize( self.voxelGridResolution * self.slideCount, self.voxelGridResolution * self.slideVertCount) if self.pipeline.settings.useDebugAttachments: self.distributeTarget.addColorTexture() self.distributeTarget.prepareOffscreenBuffer() # Set a near-filter to the texture if self.pipeline.settings.useDebugAttachments: self.distributeTarget.getColorTexture().setMinfilter( Texture.FTNearest) self.distributeTarget.getColorTexture().setMagfilter( Texture.FTNearest) self.distributeTarget.setShaderInput("isLastStep", False) # Create solidness texture self.voxelSolidTex = Texture("GIDataSolidTex") self.voxelSolidTex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR16) self.convertGridTarget.setShaderInput("voxelSolidDest", self.voxelSolidTex) self.distributeTarget.setShaderInput("voxelSolidTex", self.voxelSolidTex) MemoryMonitor.addTexture("VoxelSolidTex", self.voxelSolidTex) self.voxelSolidStableTex = Texture("GIDataSolidStableTex") self.voxelSolidStableTex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR16) self.distributeTarget.setShaderInput("voxelSolidWriteTex", self.voxelSolidStableTex) self.pipeline.getRenderPassManager().registerStaticVariable( "giVoxelSolidTex", self.voxelSolidStableTex) # Create the final gi pass self.finalPass = GlobalIlluminationPass() self.pipeline.getRenderPassManager().registerPass(self.finalPass) self.pipeline.getRenderPassManager().registerDynamicVariable( "giData", self.bindTo) self.pipeline.getRenderPassManager().registerStaticVariable( "giReadyState", self.readyStateFlag) # Visualize voxels if False: self.voxelCube = loader.loadModel("Box") self.voxelCube.reparentTo(render) # self.voxelCube.setTwoSided(True) self.voxelCube.node().setFinal(True) self.voxelCube.node().setBounds(OmniBoundingVolume()) self.voxelCube.setInstanceCount(self.voxelGridResolution**3) # self.voxelCube.hide() self.bindTo(self.voxelCube, "giData") for i in xrange(5): self.voxelCube.setShaderInput("giDataTex" + str(i), self.pingDataTextures[i]) self.disableTargets() def _createConvertShader(self): """ Loads the shader for converting the voxel grid """ shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/ConvertGrid.fragment") self.convertGridTarget.setShader(shader) def _createClearShader(self): """ Loads the shader for converting the voxel grid """ shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/ClearGrid.fragment") self.clearGridTarget.setShader(shader) def _createGenerateMipmapsShader(self): """ Loads the shader for generating the voxel grid mipmaps """ computeSize = self.voxelGridResolution for child in self.mipmapTargets: computeSize /= 2 shader = Shader.load( Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/GenerateMipmaps/" + str(computeSize) + ".fragment") child.setShader(shader) def _createDistributionShader(self): """ Creates the photon distribution shader """ shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/Distribute.fragment") self.distributeTarget.setShader(shader) def _createBlurShader(self): """ Creates the photon distribution shader """ shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/BlurPhotonGrid.fragment") self.blurBuffer.setShader(shader) def reloadShader(self): """ Reloads all shaders and updates the voxelization camera state aswell """ self.debug("Reloading shaders") self._createConvertShader() self._createClearShader() self._createDistributionShader() # self._createGenerateMipmapsShader() # self._createPhotonBoxShader() # self._createBlurShader() if hasattr(self, "voxelCube"): self.pipeline.setEffect(self.voxelCube, "Effects/DisplayVoxels.effect", { "normalMapping": False, "castShadows": False, "castGI": False }) def _createPhotonBoxShader(self): """ Loads the shader to visualize the photons """ shader = Shader.load(Shader.SLGLSL, "Shader/DefaultShaders/Photon/vertex.glsl", "Shader/DefaultShaders/Photon/fragment.glsl") # self.photonBox.setShader(shader, 100) def _computeGridPos(self): """ Computes the new center of the voxel grid. The center pos is also snapped, to avoid flickering. """ # It is important that the grid is snapped, otherwise it will flicker # while the camera moves. When using a snap of 32, everything until # the log2(32) = 5th mipmap is stable. snap = 1.0 stepSizeX = float(self.voxelGridSize * 2.0) / float( self.voxelGridResolution) * snap stepSizeY = float(self.voxelGridSize * 2.0) / float( self.voxelGridResolution) * snap stepSizeZ = float(self.voxelGridSize * 2.0) / float( self.voxelGridResolution) * snap gridPos = Globals.base.camera.getPos(Globals.base.render) gridPos.x -= gridPos.x % stepSizeX gridPos.y -= gridPos.y % stepSizeY gridPos.z -= gridPos.z % stepSizeZ return gridPos def bindTo(self, node, prefix): """ Binds all required shader inputs to a target to compute / display the global illumination """ node.setShaderInput(prefix + ".positionGeneration", self.gridPosTemp) node.setShaderInput(prefix + ".position", self.gridPosLive) node.setShaderInput(prefix + ".size", self.voxelGridSize) node.setShaderInput(prefix + ".resolution", self.voxelGridResolution)
class GlobalIllumination(DebugObject): """ This class handles the global illumination processing. To process the global illumination, the scene is first rasterized from 3 directions, and a 3D voxel grid is created. After that, the mipmaps of the voxel grid are generated. The final shader then performs voxel cone tracing to compute an ambient, diffuse and specular term. The gi is split over several frames to reduce the computation cost. Currently there are 5 different steps, split over 4 frames: Frame 1: - Rasterize the scene from the x-axis Frame 2: - Rasterize the scene from the y-axis Frame 3: - Rasterize the scene from the z-axis Frame 4: - Copy the generated temporary voxel grid into a stable voxel grid - Generate the mipmaps for that stable voxel grid using a gaussian filter In the final pass the stable voxel grid is sampled. The voxel tracing selects the mipmap depending on the cone size. This enables small scale details as well as blurry reflections and low-frequency ao / diffuse. For performance reasons, the final pass is executed at half window resolution and then bilateral upscaled. """ QualityLevels = ["Low", "Medium", "High", "Ultra"] def __init__(self, pipeline): DebugObject.__init__(self, "GlobalIllumnination") self.pipeline = pipeline self.qualityLevel = self.pipeline.settings.giQualityLevel if self.qualityLevel not in self.QualityLevels: self.fatal("Unsupported gi quality level:" + self.qualityLevel) self.qualityLevelIndex = self.QualityLevels.index(self.qualityLevel) # Grid size in world space units self.voxelGridSize = self.pipeline.settings.giVoxelGridSize # Grid resolution in pixels self.voxelGridResolution = [32, 64, 128, 192][self.qualityLevelIndex] # Has to be a multiple of 2 self.distributionSteps = [16, 30, 60, 90][self.qualityLevelIndex] self.slideCount = int(self.voxelGridResolution / 8) self.slideVertCount = self.voxelGridResolution / self.slideCount self.bounds = BoundingBox() self.renderCount = 0 # Create the task manager self.taskManager = DistributedTaskManager() self.gridPosLive = PTALVecBase3f.emptyArray(1) self.gridPosTemp = PTALVecBase3f.emptyArray(1) # Store ready state self.readyStateFlag = PTAFloat.emptyArray(1) self.readyStateFlag[0] = 0 self.frameIndex = 0 self.steps = [] def _createDebugTexts(self): """ Creates a debug overlay to show GI status """ self.debugText = None self.buildingText = None if self.pipeline.settings.displayDebugStats: self.debugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.88), rightAligned=True, color=Vec3(1, 1, 0), size=0.03) self.buildingText = FastText(pos=Vec2(-0.3, 0), rightAligned=False, color=Vec3(1, 1, 0), size=0.03) self.buildingText.setText("PREPARING GI, PLEASE BE PATIENT ....") def stepVoxelize(self, idx): # If we are at the beginning of the frame, compute the new grid position if idx == 0: self.gridPosTemp[0] = self._computeGridPos() # Clear voxel grid at the beginning # for tex in self.generationTextures: # tex.clearImage() self.clearGridTarget.setActive(True) if self.debugText is not None: self.debugText.setText("GI Grid Center: " + ", ".join(str(round(i, 2)) for i in self.gridPosTemp[0]) + " / GI Frame " + str(self.renderCount) ) self.renderCount += 1 if self.renderCount == 3: self.readyStateFlag[0] = 1.0 if self.buildingText: self.buildingText.remove() self.buildingText = None self.voxelizePass.voxelizeSceneFromDirection(self.gridPosTemp[0], "xyz"[idx]) def stepDistribute(self, idx): if idx == 0: skyBegin = 142.0 skyInGrid = (skyBegin - self.gridPosTemp[0].z) / (2.0 * self.voxelGridSize) skyInGrid = int(skyInGrid * self.voxelGridResolution) self.convertGridTarget.setShaderInput("skyStartZ", skyInGrid) self.convertGridTarget.setActive(True) self.distributeTarget.setActive(True) swap = idx % 2 == 0 sources = self.pingDataTextures if swap else self.pongDataTextures dests = self.pongDataTextures if swap else self.pingDataTextures if idx == self.distributionSteps - 1: self.publishGrid() dests = self.dataTextures for i, direction in enumerate(self.directions): self.distributeTarget.setShaderInput("src" + direction, sources[i]) self.distributeTarget.setShaderInput("dst" + direction, dests[i]) # Only do the last blur-step on high+ quality, leads to artifacts otherwise # due to the low grid resolution if self.qualityLevel in ["High", "Ultra"]: self.distributeTarget.setShaderInput("isLastStep", idx >= self.distributionSteps-1) self.distributeTarget.setShaderInput("writeSolidness", idx >= self.distributionSteps-1) def publishGrid(self): """ This function gets called when the grid is ready to be used, and updates the live grid data """ self.gridPosLive[0] = self.gridPosTemp[0] self.bounds.setMinMax(self.gridPosLive[0]-Vec3(self.voxelGridSize), self.gridPosLive[0]+Vec3(self.voxelGridSize)) def getBounds(self): """ Returns the bounds of the gi grid """ return self.bounds def update(self): """ Processes the gi, this method is called every frame """ # Disable all buffers here before starting the rendering self.disableTargets() # for target in self.mipmapTargets: # target.setActive(False) self.taskManager.process() def disableTargets(self): """ Disables all active targets """ self.voxelizePass.setActive(False) self.convertGridTarget.setActive(False) self.clearGridTarget.setActive(False) self.distributeTarget.setActive(False) def setup(self): """ Setups everything for the GI to work """ assert(self.distributionSteps % 2 == 0) self._createDebugTexts() self.pipeline.getRenderPassManager().registerDefine("USE_GLOBAL_ILLUMINATION", 1) self.pipeline.getRenderPassManager().registerDefine("GI_SLIDE_COUNT", self.slideCount) self.pipeline.getRenderPassManager().registerDefine("GI_QUALITY_LEVEL", self.qualityLevelIndex) # make the grid resolution a constant self.pipeline.getRenderPassManager().registerDefine("GI_GRID_RESOLUTION", self.voxelGridResolution) self.taskManager.addTask(3, self.stepVoxelize) self.taskManager.addTask(self.distributionSteps, self.stepDistribute) # Create the voxelize pass which is used to voxelize the scene from # several directions self.voxelizePass = VoxelizePass(self.pipeline) self.voxelizePass.setVoxelGridResolution(self.voxelGridResolution) self.voxelizePass.setVoxelGridSize(self.voxelGridSize) self.voxelizePass.setGridResolutionMultiplier(1) self.pipeline.getRenderPassManager().registerPass(self.voxelizePass) self.generationTextures = [] # Create the buffers used to create the voxel grid for color in "rgb": tex = Texture("VoxelGeneration-" + color) tex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TInt, Texture.FR32) tex.setClearColor(Vec4(0)) self.generationTextures.append(tex) Globals.render.setShaderInput("voxelGenDest" + color.upper(), tex) MemoryMonitor.addTexture("VoxelGenerationTex-" + color.upper(), tex) self.bindTo(Globals.render, "giData") self.convertGridTarget = RenderTarget("ConvertGIGrid") self.convertGridTarget.setSize(self.voxelGridResolution * self.slideCount, self.voxelGridResolution * self.slideVertCount) if self.pipeline.settings.useDebugAttachments: self.convertGridTarget.addColorTexture() self.convertGridTarget.prepareOffscreenBuffer() # Set a near-filter to the texture if self.pipeline.settings.useDebugAttachments: self.convertGridTarget.getColorTexture().setMinfilter(Texture.FTNearest) self.convertGridTarget.getColorTexture().setMagfilter(Texture.FTNearest) self.clearGridTarget = RenderTarget("ClearGIGrid") self.clearGridTarget.setSize(self.voxelGridResolution * self.slideCount, self.voxelGridResolution * self.slideVertCount) if self.pipeline.settings.useDebugAttachments: self.clearGridTarget.addColorTexture() self.clearGridTarget.prepareOffscreenBuffer() for idx, color in enumerate("rgb"): self.convertGridTarget.setShaderInput("voxelGenSrc" + color.upper(), self.generationTextures[idx]) self.clearGridTarget.setShaderInput("voxelGenTex" + color.upper(), self.generationTextures[idx]) # Create the data textures self.dataTextures = [] self.directions = ["PosX", "NegX", "PosY", "NegY", "PosZ", "NegZ"] for i, direction in enumerate(self.directions): tex = Texture("GIDataTex" + direction) tex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR11G11B10) MemoryMonitor.addTexture("VoxelDataTex-" + direction, tex) self.dataTextures.append(tex) self.pipeline.getRenderPassManager().registerStaticVariable("giVoxelData" + direction, tex) # Create ping / pong textures self.pingDataTextures = [] self.pongDataTextures = [] for i, direction in enumerate(self.directions): texPing = Texture("GIPingDataTex" + direction) texPing.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR11G11B10) MemoryMonitor.addTexture("VoxelPingDataTex-" + direction, texPing) self.pingDataTextures.append(texPing) texPong = Texture("GIPongDataTex" + direction) texPong.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR11G11B10) MemoryMonitor.addTexture("VoxelPongDataTex-" + direction, texPong) self.pongDataTextures.append(texPong) self.convertGridTarget.setShaderInput("voxelDataDest"+direction, self.pingDataTextures[i]) # self.clearGridTarget.setShaderInput("voxelDataDest" + str(i), self.pongDataTextures[i]) # Set texture wrap modes for tex in self.pingDataTextures + self.pongDataTextures + self.dataTextures + self.generationTextures: tex.setMinfilter(Texture.FTLinear) tex.setMagfilter(Texture.FTLinear) tex.setWrapU(Texture.WMBorderColor) tex.setWrapV(Texture.WMBorderColor) tex.setWrapW(Texture.WMBorderColor) tex.setAnisotropicDegree(0) tex.setBorderColor(Vec4(0)) for tex in self.dataTextures: tex.setMinfilter(Texture.FTLinear) tex.setMagfilter(Texture.FTLinear) self.distributeTarget = RenderTarget("DistributeVoxels") self.distributeTarget.setSize(self.voxelGridResolution * self.slideCount, self.voxelGridResolution * self.slideVertCount) if self.pipeline.settings.useDebugAttachments: self.distributeTarget.addColorTexture() self.distributeTarget.prepareOffscreenBuffer() # Set a near-filter to the texture if self.pipeline.settings.useDebugAttachments: self.distributeTarget.getColorTexture().setMinfilter(Texture.FTNearest) self.distributeTarget.getColorTexture().setMagfilter(Texture.FTNearest) self.distributeTarget.setShaderInput("isLastStep", False) # Create solidness texture self.voxelSolidTex = Texture("GIDataSolidTex") self.voxelSolidTex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR16) self.convertGridTarget.setShaderInput("voxelSolidDest", self.voxelSolidTex) self.distributeTarget.setShaderInput("voxelSolidTex", self.voxelSolidTex) MemoryMonitor.addTexture("VoxelSolidTex", self.voxelSolidTex) self.voxelSolidStableTex = Texture("GIDataSolidStableTex") self.voxelSolidStableTex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR16) self.distributeTarget.setShaderInput("voxelSolidWriteTex", self.voxelSolidStableTex) self.pipeline.getRenderPassManager().registerStaticVariable("giVoxelSolidTex", self.voxelSolidStableTex) # Create the final gi pass self.finalPass = GlobalIlluminationPass() self.pipeline.getRenderPassManager().registerPass(self.finalPass) self.pipeline.getRenderPassManager().registerDynamicVariable("giData", self.bindTo) self.pipeline.getRenderPassManager().registerStaticVariable("giReadyState", self.readyStateFlag) # Visualize voxels if False: self.voxelCube = loader.loadModel("Box") self.voxelCube.reparentTo(render) # self.voxelCube.setTwoSided(True) self.voxelCube.node().setFinal(True) self.voxelCube.node().setBounds(OmniBoundingVolume()) self.voxelCube.setInstanceCount(self.voxelGridResolution**3) # self.voxelCube.hide() self.bindTo(self.voxelCube, "giData") for i in xrange(5): self.voxelCube.setShaderInput("giDataTex" + str(i), self.pingDataTextures[i]) self.disableTargets() def _createConvertShader(self): """ Loads the shader for converting the voxel grid """ shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/ConvertGrid.fragment") self.convertGridTarget.setShader(shader) def _createClearShader(self): """ Loads the shader for converting the voxel grid """ shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/ClearGrid.fragment") self.clearGridTarget.setShader(shader) def _createGenerateMipmapsShader(self): """ Loads the shader for generating the voxel grid mipmaps """ computeSize = self.voxelGridResolution for child in self.mipmapTargets: computeSize /= 2 shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/GenerateMipmaps/" + str(computeSize) + ".fragment") child.setShader(shader) def _createDistributionShader(self): """ Creates the photon distribution shader """ shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/Distribute.fragment") self.distributeTarget.setShader(shader) def _createBlurShader(self): """ Creates the photon distribution shader """ shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/BlurPhotonGrid.fragment") self.blurBuffer.setShader(shader) def reloadShader(self): """ Reloads all shaders and updates the voxelization camera state aswell """ self.debug("Reloading shaders") self._createConvertShader() self._createClearShader() self._createDistributionShader() # self._createGenerateMipmapsShader() # self._createPhotonBoxShader() # self._createBlurShader() if hasattr(self, "voxelCube"): self.pipeline.setEffect(self.voxelCube, "Effects/DisplayVoxels.effect", { "normalMapping": False, "castShadows": False, "castGI": False }) def _createPhotonBoxShader(self): """ Loads the shader to visualize the photons """ shader = Shader.load(Shader.SLGLSL, "Shader/DefaultShaders/Photon/vertex.glsl", "Shader/DefaultShaders/Photon/fragment.glsl") # self.photonBox.setShader(shader, 100) def _computeGridPos(self): """ Computes the new center of the voxel grid. The center pos is also snapped, to avoid flickering. """ # It is important that the grid is snapped, otherwise it will flicker # while the camera moves. When using a snap of 32, everything until # the log2(32) = 5th mipmap is stable. snap = 1.0 stepSizeX = float(self.voxelGridSize * 2.0) / float(self.voxelGridResolution) * snap stepSizeY = float(self.voxelGridSize * 2.0) / float(self.voxelGridResolution) * snap stepSizeZ = float(self.voxelGridSize * 2.0) / float(self.voxelGridResolution) * snap gridPos = Globals.base.camera.getPos(Globals.base.render) gridPos.x -= gridPos.x % stepSizeX gridPos.y -= gridPos.y % stepSizeY gridPos.z -= gridPos.z % stepSizeZ return gridPos def bindTo(self, node, prefix): """ Binds all required shader inputs to a target to compute / display the global illumination """ node.setShaderInput(prefix + ".positionGeneration", self.gridPosTemp) node.setShaderInput(prefix + ".position", self.gridPosLive) node.setShaderInput(prefix + ".size", self.voxelGridSize) node.setShaderInput(prefix + ".resolution", self.voxelGridResolution)
class LightManager(DebugObject): """ This class is internally used by the RenderingPipeline to handle Lights and their Shadows. It stores a list of lights, and updates the required ShadowSources per frame. There are two main update methods: updateLights processes each light and does a basic frustum check. If the light is in the frustum, its ID is passed to the light precompute container (set with setLightingCuller). Also, each shadowSource of the light is checked, and if it reports to be invalid, it's queued to the list of queued shadow updates. updateShadows processes the queued shadow updates and setups everything to render the shadow depth textures to the shadow atlas. Lights can be added with addLight. Notice you cannot change the shadow resolution or wether the light casts shadows after you called addLight. This is because it might already have a position in the atlas, and so the atlas would have to delete it's map, which is not supported (yet). This shouldn't be an issue, as you usually always know before if a light will cast shadows or not. """ def __init__(self, pipeline): """ Creates a new LightManager. It expects a RenderPipeline as parameter. """ DebugObject.__init__(self, "LightManager") self.lightSlots = [None] * LightLimits.maxTotalLights self.shadowSourceSlots = [None] * LightLimits.maxShadowMaps self.queuedShadowUpdates = [] self.renderedLights = {} self.pipeline = pipeline # Create arrays to store lights & shadow sources self.allLightsArray = ShaderStructArray(Light, LightLimits.maxTotalLights) self.updateCallbacks = [] self.cullBounds = None self.numTiles = None self.lightingComputator = None self.shadowScene = Globals.render # Create atlas self.shadowAtlas = ShadowAtlas() self.shadowAtlas.setSize(self.pipeline.settings.shadowAtlasSize) self.shadowAtlas.create() self.maxShadowUpdatesPerFrame = self.pipeline.settings.maxShadowUpdatesPerFrame self.numShadowUpdatesPTA = PTAInt.emptyArray(1) self.updateShadowsArray = ShaderStructArray( ShadowSource, self.maxShadowUpdatesPerFrame) self.allShadowsArray = ShaderStructArray(ShadowSource, LightLimits.maxShadowMaps) self._initLightCulling() # Create shadow compute buffer self._createShadowPass() self._createUnshadowedLightsPass() self._createShadowedLightsPass() self._createApplyLightsPass() self._createExposurePass() if self.pipeline.settings.enableScattering: self._createScatteringPass() # Create the initial shadow state self.shadowScene.setTag("ShadowPassShader", "Default") # Register variables & arrays self.pipeline.getRenderPassManager().registerDynamicVariable( "shadowUpdateSources", self._bindUpdateSources) self.pipeline.getRenderPassManager().registerDynamicVariable( "allLights", self._bindAllLights) self.pipeline.getRenderPassManager().registerDynamicVariable( "allShadowSources", self._bindAllSources) self.pipeline.getRenderPassManager().registerStaticVariable( "numShadowUpdates", self.numShadowUpdatesPTA) self._loadIESProfiles() self._addShaderDefines() self._createDebugTexts() def _bindUpdateSources(self, renderPass, name): """ Internal method to bind the shadow update source to a target """ self.updateShadowsArray.bindTo(renderPass, name) def _bindAllLights(self, renderPass, name): """ Internal method to bind the global lights array to a target """ self.allLightsArray.bindTo(renderPass, name) def _bindAllSources(self, renderPass, name): """ Internal method to bind the global shadow sources to a target """ self.allShadowsArray.bindTo(renderPass, name) def _createShadowPass(self): """ Creates the shadow pass, where the shadow atlas is generated into """ self.shadowPass = ShadowScenePass() self.shadowPass.setMaxRegions(self.maxShadowUpdatesPerFrame) self.shadowPass.setSize(self.shadowAtlas.getSize()) self.pipeline.getRenderPassManager().registerPass(self.shadowPass) def _createUnshadowedLightsPass(self): """ Creates the pass which renders all unshadowed lights """ self.unshadowedLightsPass = UnshadowedLightsPass() self.pipeline.getRenderPassManager().registerPass( self.unshadowedLightsPass) def _createApplyLightsPass(self): """ Creates the pass which applies all lights """ self.applyLightsPass = ApplyLightsPass(self.pipeline) self.applyLightsPass.setTileCount(self.numTiles) self.pipeline.getRenderPassManager().registerPass(self.applyLightsPass) def _createShadowedLightsPass(self): """ Creates the pass which renders all unshadowed lights """ self.shadowedLightsPass = ShadowedLightsPass() self.pipeline.getRenderPassManager().registerPass( self.shadowedLightsPass) def _createExposurePass(self): """ Creates the pass which applies the exposure and color correction """ self.exposurePass = ExposurePass() self.pipeline.getRenderPassManager().registerPass(self.exposurePass) def _createScatteringPass(self): """ Creates the scattering pass """ self.scatteringPass = ScatteringPass() self.pipeline.getRenderPassManager().registerPass(self.scatteringPass) self.scatteringCubemapPass = ScatteringCubemapPass(self.pipeline) self.pipeline.getRenderPassManager().registerPass( self.scatteringCubemapPass) def _loadIESProfiles(self): """ Loads the ies profiles from Data/IESProfiles. """ self.iesLoader = IESLoader() self.iesLoader.loadIESProfiles("Data/IESProfiles/") self.pipeline.getRenderPassManager().registerStaticVariable( "IESProfilesTex", self.iesLoader.getIESProfileStorageTex()) def _initLightCulling(self): """ Creates the pass which gets a list of lights and computes which light affects which tile """ # Fetch patch size self.patchSize = LVecBase2i(self.pipeline.settings.computePatchSizeX, self.pipeline.settings.computePatchSizeY) # size has to be a multiple of the compute unit size # but still has to cover the whole screen sizeX = int(math.ceil(float(Globals.resolution.x) / self.patchSize.x)) sizeY = int(math.ceil(float(Globals.resolution.y) / self.patchSize.y)) self.lightCullingPass = LightCullingPass(self.pipeline) self.lightCullingPass.setSize(sizeX, sizeY) self.lightCullingPass.setPatchSize(self.patchSize.x, self.patchSize.y) self.pipeline.getRenderPassManager().registerPass( self.lightCullingPass) self.pipeline.getRenderPassManager().registerStaticVariable( "lightingTileCount", LVecBase2i(sizeX, sizeY)) self.debug("Batch size =", sizeX, "x", sizeY, "Actual Buffer size=", int(sizeX * self.patchSize.x), "x", int(sizeY * self.patchSize.y)) self.numTiles = LVecBase2i(sizeX, sizeY) # Create the buffer which stores the rendered lights self._makeRenderedLightsBuffer() def _makeRenderedLightsBuffer(self): """ Creates the buffer which stores the indices of all rendered lights """ bufferSize = 16 bufferSize += LightLimits.maxLights["PointLight"] bufferSize += LightLimits.maxLights["PointLightShadow"] bufferSize += LightLimits.maxLights["DirectionalLight"] bufferSize += LightLimits.maxLights["DirectionalLightShadow"] bufferSize += LightLimits.maxLights["SpotLight"] bufferSize += LightLimits.maxLights["SpotLightShadow"] self.renderedLightsBuffer = Texture("RenderedLightsBuffer") self.renderedLightsBuffer.setupBufferTexture(bufferSize, Texture.TInt, Texture.FR32i, GeomEnums.UHDynamic) self.pipeline.getRenderPassManager().registerStaticVariable( "renderedLightsBuffer", self.renderedLightsBuffer) MemoryMonitor.addTexture("Rendered Lights Buffer", self.renderedLightsBuffer) def _addShaderDefines(self): """ Adds settings like the maximum light count to the list of defines which are available in the shader later """ define = lambda name, val: self.pipeline.getRenderPassManager( ).registerDefine(name, val) settings = self.pipeline.settings define("MAX_VISIBLE_LIGHTS", LightLimits.maxTotalLights) define("MAX_POINT_LIGHTS", LightLimits.maxLights["PointLight"]) define("MAX_SHADOWED_POINT_LIGHTS", LightLimits.maxLights["PointLightShadow"]) define("MAX_DIRECTIONAL_LIGHTS", LightLimits.maxLights["DirectionalLight"]) define("MAX_SHADOWED_DIRECTIONAL_LIGHTS", LightLimits.maxLights["DirectionalLightShadow"]) define("MAX_SPOT_LIGHTS", LightLimits.maxLights["SpotLight"]) define("MAX_SHADOWED_SPOT_LIGHTS", LightLimits.maxLights["SpotLightShadow"]) define("MAX_TILE_POINT_LIGHTS", LightLimits.maxPerTileLights["PointLight"]) define("MAX_TILE_SHADOWED_POINT_LIGHTS", LightLimits.maxPerTileLights["PointLightShadow"]) define("MAX_TILE_DIRECTIONAL_LIGHTS", LightLimits.maxPerTileLights["DirectionalLight"]) define("MAX_TILE_SHADOWED_DIRECTIONAL_LIGHTS", LightLimits.maxPerTileLights["DirectionalLightShadow"]) define("MAX_TILE_SPOT_LIGHTS", LightLimits.maxPerTileLights["SpotLight"]) define("MAX_TILE_SHADOWED_SPOT_LIGHTS", LightLimits.maxPerTileLights["SpotLightShadow"]) define("SHADOW_MAX_TOTAL_MAPS", LightLimits.maxShadowMaps) define("LIGHTING_COMPUTE_PATCH_SIZE_X", settings.computePatchSizeX) define("LIGHTING_COMPUTE_PATCH_SIZE_Y", settings.computePatchSizeY) if settings.renderShadows: define("USE_SHADOWS", 1) define("SHADOW_MAP_ATLAS_SIZE", settings.shadowAtlasSize) define("SHADOW_MAX_UPDATES_PER_FRAME", settings.maxShadowUpdatesPerFrame) define("SHADOW_GEOMETRY_MAX_VERTICES", settings.maxShadowUpdatesPerFrame * 3) define("CUBEMAP_ANTIALIASING_FACTOR", settings.cubemapAntialiasingFactor) define("SHADOW_NUM_PCF_SAMPLES", settings.numPCFSamples) define("PCSS_SAMPLE_RADIUS", settings.pcssSampleRadius) if settings.usePCSS: define("USE_PCSS", 1) if settings.useDiffuseAntialiasing: define("USE_DIFFUSE_ANTIALIASING", 1) define("SHADOW_NUM_PCSS_SEARCH_SAMPLES", settings.numPCSSSearchSamples) define("SHADOW_NUM_PCSS_FILTER_SAMPLES", settings.numPCSSFilterSamples) define("SHADOW_PSSM_BORDER_PERCENTAGE", settings.shadowCascadeBorderPercentage) if settings.useHardwarePCF: define("USE_HARDWARE_PCF", 1) if settings.enableAlphaTestedShadows: define("USE_ALPHA_TESTED_SHADOWS", 1) def processCallbacks(self): """ Processes all updates from the previous frame """ for update in self.updateCallbacks: update.onUpdated() self.updateCallbacks = [] def _createDebugTexts(self): """ Creates a debug overlay if specified in the pipeline settings """ self.lightsVisibleDebugText = None self.lightsUpdatedDebugText = None if self.pipeline.settings.displayDebugStats: self.lightsVisibleDebugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.84), rightAligned=True, color=Vec3(1, 1, 0), size=0.03) self.lightsUpdatedDebugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.8), rightAligned=True, color=Vec3(1, 1, 0), size=0.03) def _queueShadowUpdate(self, sourceIndex): """ Internal method to add a shadowSource to the list of queued updates. Returns the position of the source in queue """ if sourceIndex not in self.queuedShadowUpdates: self.queuedShadowUpdates.append(sourceIndex) return len(self.queuedShadowUpdates) - 1 return self.queuedShadowUpdates.index(sourceIndex) def _allocateLightSlot(self, light): """ Tries to find a free light slot. Returns False if no slot is free, if a slot is free it gets allocated and linked to the light """ for index, val in enumerate(self.lightSlots): if val == None: light.setIndex(index) self.lightSlots[index] = light return True return False def _findShadowSourceSlot(self): """ Tries to find a free shadow source. Returns False if no slot is free """ for index, val in enumerate(self.shadowSourceSlots): if val == None: return index return -1 def addLight(self, light): """ Adds a light to the list of rendered lights. NOTICE: You have to set relevant properties like Whether the light casts shadows or the shadowmap resolution before calling this! Otherwise it won't work (and maybe crash? I didn't test, just DON'T DO IT!) """ if light.attached: self.warn("Light is already attached!") return light.attached = True # self.lights.append(light) if not self._allocateLightSlot(light): self.error("Cannot allocate light slot, out of slots.") return False if light.hasShadows() and not self.pipeline.settings.renderShadows: self.warn( "Attached shadow light but shadowing is disabled in pipeline.ini" ) light.setCastsShadows(False) sources = light.getShadowSources() # Check each shadow source for index, source in enumerate(sources): # Check for correct resolution tileSize = self.shadowAtlas.getTileSize() if source.resolution < tileSize or source.resolution % tileSize != 0: self.warn( "The ShadowSource resolution has to be a multiple of the tile size (" + str(tileSize) + ")!") self.warn("Adjusting resolution to", tileSize) source.resolution = tileSize if source.resolution > self.shadowAtlas.getSize(): self.warn( "The ShadowSource resolution cannot be bigger than the atlas size (" + str(self.shadowAtlas.getSize()) + ")") self.warn("Adjusting resolution to", tileSize) source.resolution = tileSize # Frind slot for source sourceSlotIndex = self._findShadowSourceSlot() if sourceSlotIndex < 0: self.error("Cannot store more shadow sources!") return False self.shadowSourceSlots[sourceSlotIndex] = source source.setSourceIndex(sourceSlotIndex) light.setSourceIndex(index, sourceSlotIndex) # Store light in the shader struct array self.allLightsArray[light.getIndex()] = light light.queueUpdate() light.queueShadowUpdate() def removeLight(self, light): """ Removes a light from the rendered lights """ index = light.getIndex() if light.hasShadows(): sources = light.getShadowSources() for source in sources: self.shadowSourceSlots[source.getSourceIndex()] = None self.shadowAtlas.deallocateTiles(source.getUID()) # remove the source from the current updates if source.getSourceIndex() in self.queuedShadowUpdates: self.queuedShadowUpdates.remove(source.getSourceIndex()) source.cleanup() light.cleanup() del light self.lightSlots[index] = None def setCullBounds(self, bounds): """ Sets the current camera bounds used for light culling """ self.cullBounds = bounds def _writeRenderedLightsToBuffer(self): """ Stores the list of rendered lights in the buffer to access it in the shader later """ pstats_WriteBuffers.start() image = memoryview(self.renderedLightsBuffer.modifyRamImage()) bufferEntrySize = 4 # Write counters offset = 0 image[offset:offset + bufferEntrySize * 6] = struct.pack( 'i' * 6, len(self.renderedLights["PointLight"]), len(self.renderedLights["PointLightShadow"]), len(self.renderedLights["DirectionalLight"]), len(self.renderedLights["DirectionalLightShadow"]), len(self.renderedLights["SpotLight"]), len(self.renderedLights["SpotLightShadow"])) offset = 16 * bufferEntrySize # Write light lists for lightType in [ "PointLight", "PointLightShadow", "DirectionalLight", "DirectionalLightShadow", "SpotLight", "SpotLightShadow" ]: entryCount = len(self.renderedLights[lightType]) if entryCount > LightLimits.maxLights[lightType]: self.error("Out of lights bounds for", lightType) if entryCount > 0: # We can write all lights at once, thats pretty cool! image[offset:offset + entryCount * bufferEntrySize] = struct.pack( 'i' * entryCount, *self.renderedLights[lightType]) offset += LightLimits.maxLights[lightType] * bufferEntrySize pstats_WriteBuffers.stop() def update(self): """ Main update function """ self.updateLights() self.updateShadows() self.processCallbacks() def updateLights(self): """ This is one of the two per-frame-tasks. See class description to see what it does """ # Clear dictionary to store the lights rendered this frame self.renderedLights = {} # self.queuedShadowUpdates = [] for lightType in LightLimits.maxLights: self.renderedLights[lightType] = [] pstats_ProcessLights.start() # Fetch gi grid bounds giGridBounds = None if self.pipeline.settings.enableGlobalIllumination: giGridBounds = self.pipeline.globalIllum.getBounds() # Process each light for index, light in enumerate(self.lightSlots): if light == None: continue # When shadow maps should be always updated if self.pipeline.settings.alwaysUpdateAllShadows: light.queueShadowUpdate() # Update light if required pstats_PerLightUpdates.start() if light.needsUpdate(): light.performUpdate() pstats_PerLightUpdates.stop() # Perform culling pstats_CullLights.start() lightBounds = light.getBounds() if not self.cullBounds.contains(lightBounds): # In case the light is not in the camera frustum, check if the light is # in the gi frustum if giGridBounds: if not giGridBounds.contains(lightBounds): continue else: continue pstats_CullLights.stop() delaySpawn = False # Queue shadow updates if necessary pstats_QueueShadowUpdate.start() if light.hasShadows() and light.needsShadowUpdate(): neededUpdates = light.performShadowUpdate() for update in neededUpdates: updatePosition = self._queueShadowUpdate( update.getSourceIndex()) willUpdateNextFrame = updatePosition < self.maxShadowUpdatesPerFrame # If the source did not get rendered so far, and wont get rendered # in the next frame, delay the rendering of this light if not willUpdateNextFrame and not update.hasAtlasPos(): delaySpawn = True pstats_QueueShadowUpdate.stop() # When the light is not ready yet, wait for the next frame if delaySpawn: # self.debug("Delaying light spawn") continue # Check if the ies profile has been assigned yet if light.getLightType() == LightType.Spot: if light.getIESProfileIndex() < 0 and light.getIESProfileName( ) is not None: name = light.getIESProfileName() index = self.iesLoader.getIESProfileIndexByName(name) if index < 0: self.error("Unkown ies profile:", name) light.setIESProfileIndex(0) else: light.setIESProfileIndex(index) # Add light to the correct list now pstats_AppendRenderedLight.start() lightTypeName = light.getTypeName() if light.hasShadows(): lightTypeName += "Shadow" self.renderedLights[lightTypeName].append(index) pstats_AppendRenderedLight.stop() pstats_ProcessLights.stop() self._writeRenderedLightsToBuffer() # Generate debug text if self.lightsVisibleDebugText is not None: renderedPL = str(len(self.renderedLights["PointLight"])) renderedPL_S = str(len(self.renderedLights["PointLightShadow"])) renderedDL = str(len(self.renderedLights["DirectionalLight"])) renderedDL_S = str( len(self.renderedLights["DirectionalLightShadow"])) renderedSL = str(len(self.renderedLights["SpotLight"])) renderedSL_S = str(len(self.renderedLights["SpotLight"])) self.lightsVisibleDebugText.setText("Point: " + renderedPL + "/" + renderedPL_S + ", Directional: " + renderedDL + "/" + renderedDL_S + ", Spot: " + renderedSL + "/" + renderedSL_S) def updateShadows(self): """ This is one of the two per-frame-tasks. See class description to see what it does """ # Process shadows queuedUpdateLen = len(self.queuedShadowUpdates) # Compute shadow updates numUpdates = 0 lastRenderedSourcesStr = "[ " # When there are no updates, disable the buffer if len(self.queuedShadowUpdates) < 1: self.shadowPass.setActiveRegionCount(0) self.numShadowUpdatesPTA[0] = 0 else: # Check each update in the queue for index, updateID in enumerate(self.queuedShadowUpdates): # We only process a limited number of shadow maps if numUpdates >= self.maxShadowUpdatesPerFrame: break update = self.shadowSourceSlots[updateID] updateSize = update.getResolution() # assign position in atlas if not done yet if not update.hasAtlasPos(): storePos = self.shadowAtlas.reserveTiles( updateSize, updateSize, update.getUID()) if not storePos: # No space found, try to reduce resolution self.warn( "Could not find space for the shadow map of size", updateSize) self.warn("The size will be reduced to", self.shadowAtlas.getTileSize()) updateSize = self.shadowAtlas.getTileSize() update.setResolution(updateSize) storePos = self.shadowAtlas.reserveTiles( updateSize, updateSize, update.getUID()) if not storePos: self.fatal( "Still could not find a shadow atlas position, " "the shadow atlas is completely full. " "Either we reduce the resolution of existing shadow maps, " "increase the shadow atlas resolution, " "or crash the app. Guess what I decided to do :-P" ) update.assignAtlasPos(*storePos) update.update() # Store update in array self.allShadowsArray[updateID] = update self.updateShadowsArray[index] = update # Compute viewport & set depth clearer texScale = float(update.getResolution()) / float( self.shadowAtlas.getSize()) atlasPos = update.getAtlasPos() left, right = atlasPos.x, (atlasPos.x + texScale) bottom, top = atlasPos.y, (atlasPos.y + texScale) self.shadowPass.setRegionDimensions(numUpdates, left, right, bottom, top) regionCam = self.shadowPass.getRegionCamera(numUpdates) regionCam.setPos(update.cameraNode.getPos()) regionCam.setHpr(update.cameraNode.getHpr()) regionCam.node().setLens(update.getLens()) numUpdates += 1 # Finally, we can tell the update it's valid now. update.setValid() # In the next frame the update is processed, so call it later self.updateCallbacks.append(update) # Only add the uid to the output if the max updates # aren't too much. Otherwise we spam the screen if self.maxShadowUpdatesPerFrame <= 8: lastRenderedSourcesStr += str(update.getUID()) + " " # Remove all updates which got processed from the list self.queuedShadowUpdates = self.queuedShadowUpdates[numUpdates:] self.numShadowUpdatesPTA[0] = numUpdates self.shadowPass.setActiveRegionCount(numUpdates) lastRenderedSourcesStr += "]" # Generate debug text if self.lightsUpdatedDebugText is not None: self.lightsUpdatedDebugText.setText( 'Updates: ' + str(numUpdates) + "/" + str(queuedUpdateLen) + ", Last: " + lastRenderedSourcesStr + ", Free Tiles: " + str(self.shadowAtlas.getFreeTileCount()) + "/" + str(self.shadowAtlas.getTotalTileCount()))
class LightManager(DebugObject): """ This class is internally used by the RenderingPipeline to handle Lights and their Shadows. It stores a list of lights, and updates the required ShadowSources per frame. There are two main update methods: updateLights processes each light and does a basic frustum check. If the light is in the frustum, its ID is passed to the light precompute container (set with setLightingCuller). Also, each shadowSource of the light is checked, and if it reports to be invalid, it's queued to the list of queued shadow updates. updateShadows processes the queued shadow updates and setups everything to render the shadow depth textures to the shadow atlas. Lights can be added with addLight. Notice you cannot change the shadow resolution or wether the light casts shadows after you called addLight. This is because it might already have a position in the atlas, and so the atlas would have to delete it's map, which is not supported (yet). This shouldn't be an issue, as you usually always know before if a light will cast shadows or not. """ def __init__(self, pipeline): """ Creates a new LightManager. It expects a RenderPipeline as parameter. """ DebugObject.__init__(self, "LightManager") self.lightSlots = [None] * LightLimits.maxTotalLights self.shadowSourceSlots = [None] * LightLimits.maxShadowMaps self.queuedShadowUpdates = [] self.renderedLights = {} self.pipeline = pipeline # Create arrays to store lights & shadow sources self.allLightsArray = ShaderStructArray(Light, LightLimits.maxTotalLights) self.updateCallbacks = [] self.cullBounds = None self.numTiles = None self.lightingComputator = None self.shadowScene = Globals.render # Create atlas self.shadowAtlas = ShadowAtlas() self.shadowAtlas.setSize(self.pipeline.settings.shadowAtlasSize) self.shadowAtlas.create() self.maxShadowUpdatesPerFrame = self.pipeline.settings.maxShadowUpdatesPerFrame self.numShadowUpdatesPTA = PTAInt.emptyArray(1) self.updateShadowsArray = ShaderStructArray( ShadowSource, self.maxShadowUpdatesPerFrame) self.allShadowsArray = ShaderStructArray( ShadowSource, LightLimits.maxShadowMaps) self._initLightCulling() # Create shadow compute buffer self._createShadowPass() self._createUnshadowedLightsPass() self._createShadowedLightsPass() self._createApplyLightsPass() self._createExposurePass() if self.pipeline.settings.enableScattering: self._createScatteringPass() # Create the initial shadow state self.shadowScene.setTag("ShadowPassShader", "Default") # Register variables & arrays self.pipeline.getRenderPassManager().registerDynamicVariable("shadowUpdateSources", self._bindUpdateSources) self.pipeline.getRenderPassManager().registerDynamicVariable("allLights", self._bindAllLights) self.pipeline.getRenderPassManager().registerDynamicVariable("allShadowSources", self._bindAllSources) self.pipeline.getRenderPassManager().registerStaticVariable("numShadowUpdates", self.numShadowUpdatesPTA) self._loadIESProfiles() self._addShaderDefines() self._createDebugTexts() def _bindUpdateSources(self, renderPass, name): """ Internal method to bind the shadow update source to a target """ self.updateShadowsArray.bindTo(renderPass, name) def _bindAllLights(self, renderPass, name): """ Internal method to bind the global lights array to a target """ self.allLightsArray.bindTo(renderPass, name) def _bindAllSources(self, renderPass, name): """ Internal method to bind the global shadow sources to a target """ self.allShadowsArray.bindTo(renderPass, name) def _createShadowPass(self): """ Creates the shadow pass, where the shadow atlas is generated into """ self.shadowPass = ShadowScenePass() self.shadowPass.setMaxRegions(self.maxShadowUpdatesPerFrame) self.shadowPass.setSize(self.shadowAtlas.getSize()) self.pipeline.getRenderPassManager().registerPass(self.shadowPass) def _createUnshadowedLightsPass(self): """ Creates the pass which renders all unshadowed lights """ self.unshadowedLightsPass = UnshadowedLightsPass() self.pipeline.getRenderPassManager().registerPass(self.unshadowedLightsPass) def _createApplyLightsPass(self): """ Creates the pass which applies all lights """ self.applyLightsPass = ApplyLightsPass(self.pipeline) self.applyLightsPass.setTileCount(self.numTiles) self.pipeline.getRenderPassManager().registerPass(self.applyLightsPass) def _createShadowedLightsPass(self): """ Creates the pass which renders all unshadowed lights """ self.shadowedLightsPass = ShadowedLightsPass() self.pipeline.getRenderPassManager().registerPass(self.shadowedLightsPass) def _createExposurePass(self): """ Creates the pass which applies the exposure and color correction """ self.exposurePass = ExposurePass() self.pipeline.getRenderPassManager().registerPass(self.exposurePass) def _createScatteringPass(self): """ Creates the scattering pass """ self.scatteringPass = ScatteringPass() self.pipeline.getRenderPassManager().registerPass(self.scatteringPass) self.scatteringCubemapPass = ScatteringCubemapPass(self.pipeline) self.pipeline.getRenderPassManager().registerPass(self.scatteringCubemapPass) def _loadIESProfiles(self): """ Loads the ies profiles from Data/IESProfiles. """ self.iesLoader = IESLoader() self.iesLoader.loadIESProfiles("Data/IESProfiles/") self.pipeline.getRenderPassManager().registerStaticVariable("IESProfilesTex", self.iesLoader.getIESProfileStorageTex()) def _initLightCulling(self): """ Creates the pass which gets a list of lights and computes which light affects which tile """ # Fetch patch size self.patchSize = LVecBase2i( self.pipeline.settings.computePatchSizeX, self.pipeline.settings.computePatchSizeY) # size has to be a multiple of the compute unit size # but still has to cover the whole screen sizeX = int(math.ceil(float(Globals.resolution.x) / self.patchSize.x)) sizeY = int(math.ceil(float(Globals.resolution.y) / self.patchSize.y)) self.lightCullingPass = LightCullingPass(self.pipeline) self.lightCullingPass.setSize(sizeX, sizeY) self.lightCullingPass.setPatchSize(self.patchSize.x, self.patchSize.y) self.pipeline.getRenderPassManager().registerPass(self.lightCullingPass) self.pipeline.getRenderPassManager().registerStaticVariable("lightingTileCount", LVecBase2i(sizeX, sizeY)) self.debug("Batch size =", sizeX, "x", sizeY, "Actual Buffer size=", int(sizeX * self.patchSize.x), "x", int(sizeY * self.patchSize.y)) self.numTiles = LVecBase2i(sizeX, sizeY) # Create the buffer which stores the rendered lights self._makeRenderedLightsBuffer() def _makeRenderedLightsBuffer(self): """ Creates the buffer which stores the indices of all rendered lights """ bufferSize = 16 bufferSize += LightLimits.maxLights["PointLight"] bufferSize += LightLimits.maxLights["PointLightShadow"] bufferSize += LightLimits.maxLights["DirectionalLight"] bufferSize += LightLimits.maxLights["DirectionalLightShadow"] bufferSize += LightLimits.maxLights["SpotLight"] bufferSize += LightLimits.maxLights["SpotLightShadow"] self.renderedLightsBuffer = Texture("RenderedLightsBuffer") self.renderedLightsBuffer.setupBufferTexture(bufferSize, Texture.TInt, Texture.FR32i, GeomEnums.UHDynamic) self.pipeline.getRenderPassManager().registerStaticVariable( "renderedLightsBuffer", self.renderedLightsBuffer) MemoryMonitor.addTexture("Rendered Lights Buffer", self.renderedLightsBuffer) def _addShaderDefines(self): """ Adds settings like the maximum light count to the list of defines which are available in the shader later """ define = lambda name, val: self.pipeline.getRenderPassManager().registerDefine(name, val) settings = self.pipeline.settings define("MAX_VISIBLE_LIGHTS", LightLimits.maxTotalLights) define("MAX_POINT_LIGHTS", LightLimits.maxLights["PointLight"]) define("MAX_SHADOWED_POINT_LIGHTS", LightLimits.maxLights["PointLightShadow"]) define("MAX_DIRECTIONAL_LIGHTS", LightLimits.maxLights["DirectionalLight"]) define("MAX_SHADOWED_DIRECTIONAL_LIGHTS", LightLimits.maxLights["DirectionalLightShadow"]) define("MAX_SPOT_LIGHTS", LightLimits.maxLights["SpotLight"]) define("MAX_SHADOWED_SPOT_LIGHTS", LightLimits.maxLights["SpotLightShadow"]) define("MAX_TILE_POINT_LIGHTS", LightLimits.maxPerTileLights["PointLight"]) define("MAX_TILE_SHADOWED_POINT_LIGHTS", LightLimits.maxPerTileLights["PointLightShadow"]) define("MAX_TILE_DIRECTIONAL_LIGHTS", LightLimits.maxPerTileLights["DirectionalLight"]) define("MAX_TILE_SHADOWED_DIRECTIONAL_LIGHTS", LightLimits.maxPerTileLights["DirectionalLightShadow"]) define("MAX_TILE_SPOT_LIGHTS", LightLimits.maxPerTileLights["SpotLight"]) define("MAX_TILE_SHADOWED_SPOT_LIGHTS", LightLimits.maxPerTileLights["SpotLightShadow"]) define("SHADOW_MAX_TOTAL_MAPS", LightLimits.maxShadowMaps) define("LIGHTING_COMPUTE_PATCH_SIZE_X", settings.computePatchSizeX) define("LIGHTING_COMPUTE_PATCH_SIZE_Y", settings.computePatchSizeY) if settings.renderShadows: define("USE_SHADOWS", 1) define("SHADOW_MAP_ATLAS_SIZE", settings.shadowAtlasSize) define("SHADOW_MAX_UPDATES_PER_FRAME", settings.maxShadowUpdatesPerFrame) define("SHADOW_GEOMETRY_MAX_VERTICES", settings.maxShadowUpdatesPerFrame * 3) define("CUBEMAP_ANTIALIASING_FACTOR", settings.cubemapAntialiasingFactor) define("SHADOW_NUM_PCF_SAMPLES", settings.numPCFSamples) define("PCSS_SAMPLE_RADIUS", settings.pcssSampleRadius) if settings.usePCSS: define("USE_PCSS", 1) if settings.useDiffuseAntialiasing: define("USE_DIFFUSE_ANTIALIASING", 1) define("SHADOW_NUM_PCSS_SEARCH_SAMPLES", settings.numPCSSSearchSamples) define("SHADOW_NUM_PCSS_FILTER_SAMPLES", settings.numPCSSFilterSamples) define("SHADOW_PSSM_BORDER_PERCENTAGE", settings.shadowCascadeBorderPercentage) if settings.useHardwarePCF: define("USE_HARDWARE_PCF", 1) if settings.enableAlphaTestedShadows: define("USE_ALPHA_TESTED_SHADOWS", 1) def processCallbacks(self): """ Processes all updates from the previous frame """ for update in self.updateCallbacks: update.onUpdated() self.updateCallbacks = [] def _createDebugTexts(self): """ Creates a debug overlay if specified in the pipeline settings """ self.lightsVisibleDebugText = None self.lightsUpdatedDebugText = None if self.pipeline.settings.displayDebugStats: self.lightsVisibleDebugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.84), rightAligned=True, color=Vec3(1, 1, 0), size=0.03) self.lightsUpdatedDebugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.8), rightAligned=True, color=Vec3(1, 1, 0), size=0.03) def _queueShadowUpdate(self, sourceIndex): """ Internal method to add a shadowSource to the list of queued updates. Returns the position of the source in queue """ if sourceIndex not in self.queuedShadowUpdates: self.queuedShadowUpdates.append(sourceIndex) return len(self.queuedShadowUpdates) - 1 return self.queuedShadowUpdates.index(sourceIndex) def _allocateLightSlot(self, light): """ Tries to find a free light slot. Returns False if no slot is free, if a slot is free it gets allocated and linked to the light """ for index, val in enumerate(self.lightSlots): if val == None: light.setIndex(index) self.lightSlots[index] = light return True return False def _findShadowSourceSlot(self): """ Tries to find a free shadow source. Returns False if no slot is free """ for index, val in enumerate(self.shadowSourceSlots): if val == None: return index return -1 def addLight(self, light): """ Adds a light to the list of rendered lights. NOTICE: You have to set relevant properties like Whether the light casts shadows or the shadowmap resolution before calling this! Otherwise it won't work (and maybe crash? I didn't test, just DON'T DO IT!) """ if light.attached: self.warn("Light is already attached!") return light.attached = True # self.lights.append(light) if not self._allocateLightSlot(light): self.error("Cannot allocate light slot, out of slots.") return False if light.hasShadows() and not self.pipeline.settings.renderShadows: self.warn("Attached shadow light but shadowing is disabled in pipeline.ini") light.setCastsShadows(False) sources = light.getShadowSources() # Check each shadow source for index, source in enumerate(sources): # Check for correct resolution tileSize = self.shadowAtlas.getTileSize() if source.resolution < tileSize or source.resolution % tileSize != 0: self.warn( "The ShadowSource resolution has to be a multiple of the tile size (" + str(tileSize) + ")!") self.warn("Adjusting resolution to", tileSize) source.resolution = tileSize if source.resolution > self.shadowAtlas.getSize(): self.warn( "The ShadowSource resolution cannot be bigger than the atlas size (" + str(self.shadowAtlas.getSize()) + ")") self.warn("Adjusting resolution to", tileSize) source.resolution = tileSize # Frind slot for source sourceSlotIndex = self._findShadowSourceSlot() if sourceSlotIndex < 0: self.error("Cannot store more shadow sources!") return False self.shadowSourceSlots[sourceSlotIndex] = source source.setSourceIndex(sourceSlotIndex) light.setSourceIndex(index, sourceSlotIndex) # Store light in the shader struct array self.allLightsArray[light.getIndex()] = light light.queueUpdate() light.queueShadowUpdate() def removeLight(self, light): """ Removes a light from the rendered lights """ index = light.getIndex() if light.hasShadows(): sources = light.getShadowSources() for source in sources: self.shadowSourceSlots[source.getSourceIndex()] = None self.shadowAtlas.deallocateTiles(source.getUID()) # remove the source from the current updates if source.getSourceIndex() in self.queuedShadowUpdates: self.queuedShadowUpdates.remove(source.getSourceIndex()) source.cleanup() light.cleanup() del light self.lightSlots[index] = None def setCullBounds(self, bounds): """ Sets the current camera bounds used for light culling """ self.cullBounds = bounds def _writeRenderedLightsToBuffer(self): """ Stores the list of rendered lights in the buffer to access it in the shader later """ pstats_WriteBuffers.start() image = memoryview(self.renderedLightsBuffer.modifyRamImage()) bufferEntrySize = 4 # Write counters offset = 0 image[offset:offset + bufferEntrySize * 6] = struct.pack('i' * 6, len(self.renderedLights["PointLight"]), len(self.renderedLights["PointLightShadow"]), len(self.renderedLights["DirectionalLight"]), len(self.renderedLights["DirectionalLightShadow"]), len(self.renderedLights["SpotLight"]), len(self.renderedLights["SpotLightShadow"])) offset = 16 * bufferEntrySize # Write light lists for lightType in ["PointLight", "PointLightShadow", "DirectionalLight", "DirectionalLightShadow", "SpotLight", "SpotLightShadow"]: entryCount = len(self.renderedLights[lightType]) if entryCount > LightLimits.maxLights[lightType]: self.error("Out of lights bounds for", lightType) if entryCount > 0: # We can write all lights at once, thats pretty cool! image[offset:offset + entryCount * bufferEntrySize] = struct.pack('i' * entryCount, *self.renderedLights[lightType]) offset += LightLimits.maxLights[lightType] * bufferEntrySize pstats_WriteBuffers.stop() def update(self): """ Main update function """ self.updateLights() self.updateShadows() self.processCallbacks() def updateLights(self): """ This is one of the two per-frame-tasks. See class description to see what it does """ # Clear dictionary to store the lights rendered this frame self.renderedLights = {} # self.queuedShadowUpdates = [] for lightType in LightLimits.maxLights: self.renderedLights[lightType] = [] pstats_ProcessLights.start() # Fetch gi grid bounds giGridBounds = None if self.pipeline.settings.enableGlobalIllumination: giGridBounds = self.pipeline.globalIllum.getBounds() # Process each light for index, light in enumerate(self.lightSlots): if light == None: continue # When shadow maps should be always updated if self.pipeline.settings.alwaysUpdateAllShadows: light.queueShadowUpdate() # Update light if required pstats_PerLightUpdates.start() if light.needsUpdate(): light.performUpdate() pstats_PerLightUpdates.stop() # Perform culling pstats_CullLights.start() lightBounds = light.getBounds() if not self.cullBounds.contains(lightBounds): # In case the light is not in the camera frustum, check if the light is # in the gi frustum if giGridBounds: if not giGridBounds.contains(lightBounds): continue else: continue pstats_CullLights.stop() delaySpawn = False # Queue shadow updates if necessary pstats_QueueShadowUpdate.start() if light.hasShadows() and light.needsShadowUpdate(): neededUpdates = light.performShadowUpdate() for update in neededUpdates: updatePosition = self._queueShadowUpdate(update.getSourceIndex()) willUpdateNextFrame = updatePosition < self.maxShadowUpdatesPerFrame # If the source did not get rendered so far, and wont get rendered # in the next frame, delay the rendering of this light if not willUpdateNextFrame and not update.hasAtlasPos(): delaySpawn = True pstats_QueueShadowUpdate.stop() # When the light is not ready yet, wait for the next frame if delaySpawn: # self.debug("Delaying light spawn") continue # Check if the ies profile has been assigned yet if light.getLightType() == LightType.Spot: if light.getIESProfileIndex() < 0 and light.getIESProfileName() is not None: name = light.getIESProfileName() index = self.iesLoader.getIESProfileIndexByName(name) if index < 0: self.error("Unkown ies profile:",name) light.setIESProfileIndex(0) else: light.setIESProfileIndex(index) # Add light to the correct list now pstats_AppendRenderedLight.start() lightTypeName = light.getTypeName() if light.hasShadows(): lightTypeName += "Shadow" self.renderedLights[lightTypeName].append(index) pstats_AppendRenderedLight.stop() pstats_ProcessLights.stop() self._writeRenderedLightsToBuffer() # Generate debug text if self.lightsVisibleDebugText is not None: renderedPL = str(len(self.renderedLights["PointLight"])) renderedPL_S = str(len(self.renderedLights["PointLightShadow"])) renderedDL = str(len(self.renderedLights["DirectionalLight"])) renderedDL_S = str(len(self.renderedLights["DirectionalLightShadow"])) renderedSL = str(len(self.renderedLights["SpotLight"])) renderedSL_S = str(len(self.renderedLights["SpotLight"])) self.lightsVisibleDebugText.setText( "Point: " + renderedPL + "/" + renderedPL_S + ", Directional: " + renderedDL + "/"+ renderedDL_S + ", Spot: " + renderedSL+ "/" + renderedSL_S) def updateShadows(self): """ This is one of the two per-frame-tasks. See class description to see what it does """ # Process shadows queuedUpdateLen = len(self.queuedShadowUpdates) # Compute shadow updates numUpdates = 0 lastRenderedSourcesStr = "[ " # When there are no updates, disable the buffer if len(self.queuedShadowUpdates) < 1: self.shadowPass.setActiveRegionCount(0) self.numShadowUpdatesPTA[0] = 0 else: # Check each update in the queue for index, updateID in enumerate(self.queuedShadowUpdates): # We only process a limited number of shadow maps if numUpdates >= self.maxShadowUpdatesPerFrame: break update = self.shadowSourceSlots[updateID] updateSize = update.getResolution() # assign position in atlas if not done yet if not update.hasAtlasPos(): storePos = self.shadowAtlas.reserveTiles( updateSize, updateSize, update.getUID()) if not storePos: # No space found, try to reduce resolution self.warn( "Could not find space for the shadow map of size", updateSize) self.warn( "The size will be reduced to", self.shadowAtlas.getTileSize()) updateSize = self.shadowAtlas.getTileSize() update.setResolution(updateSize) storePos = self.shadowAtlas.reserveTiles( updateSize, updateSize, update.getUID()) if not storePos: self.fatal( "Still could not find a shadow atlas position, " "the shadow atlas is completely full. " "Either we reduce the resolution of existing shadow maps, " "increase the shadow atlas resolution, " "or crash the app. Guess what I decided to do :-P") update.assignAtlasPos(*storePos) update.update() # Store update in array self.allShadowsArray[updateID] = update self.updateShadowsArray[index] = update # Compute viewport & set depth clearer texScale = float(update.getResolution()) / float(self.shadowAtlas.getSize()) atlasPos = update.getAtlasPos() left, right = atlasPos.x, (atlasPos.x + texScale) bottom, top = atlasPos.y, (atlasPos.y + texScale) self.shadowPass.setRegionDimensions(numUpdates, left, right, bottom, top) regionCam = self.shadowPass.getRegionCamera(numUpdates) regionCam.setPos(update.cameraNode.getPos()) regionCam.setHpr(update.cameraNode.getHpr()) regionCam.node().setLens(update.getLens()) numUpdates += 1 # Finally, we can tell the update it's valid now. update.setValid() # In the next frame the update is processed, so call it later self.updateCallbacks.append(update) # Only add the uid to the output if the max updates # aren't too much. Otherwise we spam the screen if self.maxShadowUpdatesPerFrame <= 8: lastRenderedSourcesStr += str(update.getUID()) + " " # Remove all updates which got processed from the list self.queuedShadowUpdates = self.queuedShadowUpdates[numUpdates:] self.numShadowUpdatesPTA[0] = numUpdates self.shadowPass.setActiveRegionCount(numUpdates) lastRenderedSourcesStr += "]" # Generate debug text if self.lightsUpdatedDebugText is not None: self.lightsUpdatedDebugText.setText( 'Updates: ' + str(numUpdates) + "/" + str(queuedUpdateLen) + ", Last: " + lastRenderedSourcesStr + ", Free Tiles: " + str(self.shadowAtlas.getFreeTileCount()) + "/" + str(self.shadowAtlas.getTotalTileCount()))