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)
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()
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)
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()
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. """ def __init__(self, pipeline): DebugObject.__init__(self, "GlobalIllumnination") self.pipeline = pipeline # Fetch the scene data self.targetCamera = Globals.base.cam self.targetSpace = Globals.base.render # Store grid size in world space units # This is the half voxel grid size self.voxelGridSizeWS = Vec3(60) # When you change this resolution, you have to change it in Shader/GI/ConvertGrid.fragment aswell self.voxelGridResolution = LVecBase3i(256) self.targetLight = None self.helperLight = None self.ptaGridPos = PTALVecBase3f.emptyArray(1) self.gridPos = Vec3(0) # Create ptas self.ptaLightUVStart = PTALVecBase2f.emptyArray(1) self.ptaLightMVP = PTAMat4.emptyArray(1) self.ptaVoxelGridStart = PTALVecBase3f.emptyArray(1) self.ptaVoxelGridEnd = PTALVecBase3f.emptyArray(1) self.ptaLightDirection = PTALVecBase3f.emptyArray(1) self.targetSpace.setShaderInput("giLightUVStart", self.ptaLightUVStart) self.targetSpace.setShaderInput("giLightMVP", self.ptaLightMVP) self.targetSpace.setShaderInput("giVoxelGridStart", self.ptaVoxelGridStart) self.targetSpace.setShaderInput("giVoxelGridEnd", self.ptaVoxelGridEnd) self.targetSpace.setShaderInput("giLightDirection", self.ptaLightDirection) def setTargetLight(self, light): """ Sets the sun light which is the main source of GI. Only that light casts gi. """ if light.getLightType() != LightType.Directional: self.error("setTargetLight expects a directional light!") return self.targetLight = light self._createHelperLight() def _createHelperLight(self): """ Creates the helper light. We can't use the main directional light because it uses PSSM, so we need an extra shadow map that covers the whole voxel grid. """ self.helperLight = GIHelperLight() self.helperLight.setPos(Vec3(50,50,100)) self.helperLight.setShadowMapResolution(512) self.helperLight.setFilmSize(math.sqrt( (self.voxelGridSizeWS.x**2) * 2) * 2 ) self.helperLight.setCastsShadows(True) self.pipeline.addLight(self.helperLight) self.targetSpace.setShaderInput("giLightUVSize", float(self.helperLight.shadowResolution) / self.pipeline.settings.shadowAtlasSize) self._updateGridPos() def setup(self): """ Setups everything for the GI to work """ # Create the voxelize pass which is used to voxelize the scene from # several directions self.voxelizePass = VoxelizePass() self.voxelizePass.setVoxelGridResolution(self.voxelGridResolution) self.voxelizePass.setVoxelGridSize(self.voxelGridSizeWS) self.voxelizePass.initVoxelStorage() self.pipeline.getRenderPassManager().registerPass(self.voxelizePass) # Create 3D Texture which is a copy of the voxel generation grid but # stable, as the generation grid is updated part by part and that would # lead to flickering self.voxelStableTex = Texture("VoxelsStable") self.voxelStableTex.setup3dTexture(self.voxelGridResolution.x, self.voxelGridResolution.y, self.voxelGridResolution.z, Texture.TFloat, Texture.FRgba8) # Set appropriate filter types: # The stable texture has mipmaps, which are generated during the process. # This is required for cone tracing. self.voxelStableTex.setMagfilter(SamplerState.FTLinear) self.voxelStableTex.setMinfilter(SamplerState.FTLinearMipmapLinear) self.voxelStableTex.setWrapU(SamplerState.WMBorderColor) self.voxelStableTex.setWrapV(SamplerState.WMBorderColor) self.voxelStableTex.setWrapW(SamplerState.WMBorderColor) self.voxelStableTex.setBorderColor(Vec4(0,0,0,0)) MemoryMonitor.addTexture("Voxel Grid Texture", self.voxelStableTex) # Setups the render target to convert the voxel grid self.convertBuffer = RenderTarget("VoxelConvertBuffer") self.convertBuffer.setSize(self.voxelGridResolution.x, self.voxelGridResolution.y) self.convertBuffer.setColorWrite(False) # self.convertBuffer.addColorTexture() self.convertBuffer.prepareOffscreenBuffer() self.convertBuffer.setShaderInput("src", self.voxelizePass.getVoxelTex()) self.convertBuffer.setShaderInput("dest", self.voxelStableTex) self.convertBuffer.setActive(False) # Store the frame index, we need that to decide which step we are currently # doing self.frameIndex = 0 # Create the various render targets to generate the mipmaps of the stable voxel grid self.mipmapTargets = [] computeSize = LVecBase3i(self.voxelGridResolution) currentMipmap = 0 while computeSize.z > 1: computeSize /= 2 target = RenderTarget("GIMiplevel" + str(currentMipmap)) target.setSize(computeSize.x, computeSize.y) target.setColorWrite(False) # target.addColorTexture() target.prepareOffscreenBuffer() target.setActive(False) target.setShaderInput("sourceMipmap", currentMipmap) target.setShaderInput("source", self.voxelStableTex) target.setShaderInput("dest", self.voxelStableTex, False, True, -1, currentMipmap + 1) self.mipmapTargets.append(target) currentMipmap += 1 # Create the final gi pass self.finalPass = GlobalIlluminationPass() self.pipeline.getRenderPassManager().registerPass(self.finalPass) self.pipeline.getRenderPassManager().registerDynamicVariable("giVoxelGridData", self.bindTo) def _createConvertShader(self): """ Loads the shader for converting the voxel grid """ shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/ConvertGrid.fragment") self.convertBuffer.setShader(shader) def _createGenerateMipmapsShader(self): """ Loads the shader for generating the voxel grid mipmaps """ computeSizeZ = self.voxelGridResolution.z for child in self.mipmapTargets: computeSizeZ /= 2 shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex", "Shader/GI/GenerateMipmaps/" + str(computeSizeZ) + ".fragment") child.setShader(shader) def reloadShader(self): """ Reloads all shaders and updates the voxelization camera state aswell """ self._createGenerateMipmapsShader() self._createConvertShader() def _updateGridPos(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 = 32.0 stepSizeX = float(self.voxelGridSizeWS.x * 2.0) / float(self.voxelGridResolution.x) * snap stepSizeY = float(self.voxelGridSizeWS.y * 2.0) / float(self.voxelGridResolution.y) * snap stepSizeZ = float(self.voxelGridSizeWS.z * 2.0) / float(self.voxelGridResolution.z) * snap self.gridPos = self.targetCamera.getPos(self.targetSpace) self.gridPos.x -= self.gridPos.x % stepSizeX self.gridPos.y -= self.gridPos.y % stepSizeY self.gridPos.z -= self.gridPos.z % stepSizeZ def update(self): """ Processes the gi, this method is called every frame """ # With no light, there is no gi if self.targetLight is None: self.error("The GI cannot work without a directional target light! Set one " "with renderPipeline.setGILightSource(directionalLight) first!") return # Fetch current light direction direction = self.targetLight.getDirection() if self.frameIndex == 0: # Step 1: Voxelize scene from the x-Axis for child in self.mipmapTargets: child.setActive(False) self.convertBuffer.setActive(False) # Clear the old data in generation texture self.voxelizePass.clearGrid() self.voxelizePass.voxelizeSceneFromDirection(self.gridPos, "x") # Set required inputs self.ptaLightUVStart[0] = self.helperLight.shadowSources[0].getAtlasPos() self.ptaLightMVP[0] = self.helperLight.shadowSources[0].mvp self.ptaVoxelGridStart[0] = self.gridPos - self.voxelGridSizeWS self.ptaVoxelGridEnd[0] = self.gridPos + self.voxelGridSizeWS self.ptaLightDirection[0] = direction elif self.frameIndex == 1: # Step 2: Voxelize scene from the y-Axis self.voxelizePass.voxelizeSceneFromDirection(self.gridPos, "y") elif self.frameIndex == 2: # Step 3: Voxelize the scene from the z-Axis self.voxelizePass.voxelizeSceneFromDirection(self.gridPos, "z") # Update helper light, so that it is at the right position when Step 1 # starts again self.helperLight.setPos(self.gridPos) self.helperLight.setDirection(direction) elif self.frameIndex == 3: # Step 4: Extract voxel grid and generate mipmaps self.voxelizePass.setActive(False) self.convertBuffer.setActive(True) for child in self.mipmapTargets: child.setActive(True) # We are done now, update the inputs self.ptaGridPos[0] = Vec3(self.gridPos) self._updateGridPos() # Increase frame index self.frameIndex += 1 self.frameIndex = self.frameIndex % 4 def bindTo(self, node, prefix): """ Binds all required shader inputs to a target to compute / display the global illumination """ normFactor = Vec3(1.0, float(self.voxelGridResolution.y) / float(self.voxelGridResolution.x) * self.voxelGridSizeWS.y / self.voxelGridSizeWS.x, float(self.voxelGridResolution.z) / float(self.voxelGridResolution.x) * self.voxelGridSizeWS.z / self.voxelGridSizeWS.x) node.setShaderInput(prefix + ".gridPos", self.ptaGridPos) node.setShaderInput(prefix + ".gridHalfSize", self.voxelGridSizeWS) node.setShaderInput(prefix + ".gridResolution", self.voxelGridResolution) node.setShaderInput(prefix + ".voxels", self.voxelStableTex) node.setShaderInput(prefix + ".voxelNormFactor", normFactor) node.setShaderInput(prefix + ".geometry", self.voxelStableTex)
def setup(self): """ Setups everything for the GI to work """ # Create the voxelize pass which is used to voxelize the scene from # several directions self.voxelizePass = VoxelizePass() self.voxelizePass.setVoxelGridResolution(self.voxelGridResolution) self.voxelizePass.setVoxelGridSize(self.voxelGridSizeWS) self.voxelizePass.initVoxelStorage() self.pipeline.getRenderPassManager().registerPass(self.voxelizePass) # Create 3D Texture which is a copy of the voxel generation grid but # stable, as the generation grid is updated part by part and that would # lead to flickering self.voxelStableTex = Texture("VoxelsStable") self.voxelStableTex.setup3dTexture(self.voxelGridResolution.x, self.voxelGridResolution.y, self.voxelGridResolution.z, Texture.TFloat, Texture.FRgba8) # Set appropriate filter types: # The stable texture has mipmaps, which are generated during the process. # This is required for cone tracing. self.voxelStableTex.setMagfilter(SamplerState.FTLinear) self.voxelStableTex.setMinfilter(SamplerState.FTLinearMipmapLinear) self.voxelStableTex.setWrapU(SamplerState.WMBorderColor) self.voxelStableTex.setWrapV(SamplerState.WMBorderColor) self.voxelStableTex.setWrapW(SamplerState.WMBorderColor) self.voxelStableTex.setBorderColor(Vec4(0,0,0,0)) MemoryMonitor.addTexture("Voxel Grid Texture", self.voxelStableTex) # Setups the render target to convert the voxel grid self.convertBuffer = RenderTarget("VoxelConvertBuffer") self.convertBuffer.setSize(self.voxelGridResolution.x, self.voxelGridResolution.y) self.convertBuffer.setColorWrite(False) # self.convertBuffer.addColorTexture() self.convertBuffer.prepareOffscreenBuffer() self.convertBuffer.setShaderInput("src", self.voxelizePass.getVoxelTex()) self.convertBuffer.setShaderInput("dest", self.voxelStableTex) self.convertBuffer.setActive(False) # Store the frame index, we need that to decide which step we are currently # doing self.frameIndex = 0 # Create the various render targets to generate the mipmaps of the stable voxel grid self.mipmapTargets = [] computeSize = LVecBase3i(self.voxelGridResolution) currentMipmap = 0 while computeSize.z > 1: computeSize /= 2 target = RenderTarget("GIMiplevel" + str(currentMipmap)) target.setSize(computeSize.x, computeSize.y) target.setColorWrite(False) # target.addColorTexture() target.prepareOffscreenBuffer() target.setActive(False) target.setShaderInput("sourceMipmap", currentMipmap) target.setShaderInput("source", self.voxelStableTex) target.setShaderInput("dest", self.voxelStableTex, False, True, -1, currentMipmap + 1) self.mipmapTargets.append(target) currentMipmap += 1 # Create the final gi pass self.finalPass = GlobalIlluminationPass() self.pipeline.getRenderPassManager().registerPass(self.finalPass) self.pipeline.getRenderPassManager().registerDynamicVariable("giVoxelGridData", self.bindTo)