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)