def observe(self):
        obj_node = self.car_env._car_local_node
        cam_node = self.camera_node
        bounds = BoundingBox(*obj_node.getTightBounds())
        corners_XYZ = np.array(list(itertools.product(*zip(bounds.getMin(), bounds.getMax()))))
        obj_to_cam_T = obj_node.getParent().getMat(cam_node)
        corners_XYZ = np.array([obj_to_cam_T.xform(Point3(*corner_XYZ)) for corner_XYZ in corners_XYZ])[:, :3]

        obs = super(Bbox3dSimpleQuadPanda3dEnv, self).observe()
        obs['points'] = corners_XYZ
        return obs
Example #2
0
def NormalizedSquarePatchAABB(radius,
                              x0,
                              y0,
                              x1,
                              y1,
                              offset=None,
                              x_inverted=False,
                              y_inverted=False,
                              xy_swap=False):
    a = NormalizedSquarePatchPoint(radius, 0, 0, x0, y0, x1, y1, offset,
                                   x_inverted, y_inverted, xy_swap)
    b = NormalizedSquarePatchPoint(radius, 1, 0, x0, y0, x1, y1, offset,
                                   x_inverted, y_inverted, xy_swap)
    c = NormalizedSquarePatchPoint(radius, 1, 1, x0, y0, x1, y1, offset,
                                   x_inverted, y_inverted, xy_swap)
    d = NormalizedSquarePatchPoint(radius, 0, 1, x0, y0, x1, y1, offset,
                                   x_inverted, y_inverted, xy_swap)
    m = NormalizedSquarePatchPoint(radius, 0.5, 0.5, x0, y0, x1, y1, offset,
                                   x_inverted, y_inverted, xy_swap)
    x_min = min(a[0], b[0], c[0], d[0], m[0])
    y_min = min(a[1], b[1], c[1], d[1], m[1])
    z_min = min(a[2], b[2], c[2], d[2], m[2])
    x_max = max(a[0], b[0], c[0], d[0], m[0])
    y_max = max(a[1], b[1], c[1], d[1], m[1])
    z_max = max(a[2], b[2], c[2], d[2], m[2])
    return BoundingBox(LPoint3(x_min, y_min, z_min),
                       LPoint3(x_max, y_max, z_max))
Example #3
0
    def recalcBoundingBox(self):
        if not self.np:
            return

        # Don't have the picker box or selection visualization contribute to the
        # calculation of the bounding box.
        if self.collNp:
            self.collNp.stash()
        self.hideBoundingBox()

        # Calculate a bounding box relative to ourself
        mins, maxs = self.getBounds(self.np)

        invalid, mins, maxs = self.fixBounds(mins, maxs)
        if invalid:
            mins = Point3(-8)
            maxs = Point3(8)

        self.boundingBox = BoundingBox(mins, maxs)
        self.boundsBox.setMinMax(mins, maxs)
        if self.selected:
            self.showBoundingBox()

        if self.collNp:
            self.collNp.unstash()
            self.collNp.node().clearSolids()
            self.collNp.node().addSolid(CollisionBox(mins, maxs))
            self.collNp.hide(~VIEWPORT_3D_MASK)

        self.send('mapObjectBoundsChanged', [self])
Example #4
0
    def __init__(self, id):
        MapObjectInit.start()

        MapWritable.__init__(self, base.document)
        self.temporary = False
        self.id = id
        self.selected = False
        self.classname = ""
        self.parent = None
        self.children = {}
        self.boundingBox = BoundingBox(Vec3(-0.5, -0.5, -0.5), Vec3(0.5, 0.5, 0.5))
        self.boundsBox = Box()
        self.boundsBox.addView(GeomView.Lines, VIEWPORT_3D_MASK, state = BoundsBox3DState)
        self.boundsBox.addView(GeomView.Lines, VIEWPORT_2D_MASK, state = BoundsBox2DState)
        self.boundsBox.generateGeometry()
        self.collNp = None

        self.group = None

        self.properties = {}

        # All MapObjects have transform
        self.addProperty(OriginProperty(self))
        self.addProperty(AnglesProperty(self))
        self.addProperty(ScaleProperty(self))
        self.addProperty(ShearProperty(self))

        self.np = NodePath(ModelNode(self.ObjectName + ".%i" % self.id))
        self.np.setPythonTag("mapobject", self)
        self.applyCollideMask()
        # Test bounding volume at this node and but nothing below it.
        self.np.node().setFinal(True)

        MapObjectInit.stop()
Example #5
0
def is_in_view(cam_node, obj_node, scale_size=None, crop_size=None):
    """
    Returns the intersection flag between the camera's frustrum and the
    object's tight bounding box.

    https://www.panda3d.org/forums/viewtopic.php?t=11704

    Intersection flags are defined in here:
    https://github.com/panda3d/panda3d/blob/master/panda/src/mathutil/boundingVolume.h
    """
    lens_bounds = make_bounds(cam_node.node().getLens(),
                              scale_size=scale_size,
                              crop_size=crop_size)
    bounds = BoundingBox(*obj_node.getTightBounds())
    bounds.xform(obj_node.getParent().getMat(cam_node))
    return lens_bounds.contains(bounds)
Example #6
0
def halfSphereAABB(height, positive, offset):
    if positive:
        min_points = LPoint3(-1, 0 - offset, -1)
        max_points = LPoint3(1, 1 - offset, 1)
    else:
        min_points = LPoint3(-1, offset - 1, -1)
        max_points = LPoint3(1, offset, 1)
    return BoundingBox(min_points, max_points)
Example #7
0
def UVPatchAABB(radius, x0, y0, x1, y1, offset):
    a = UVPatchPoint(radius, 0, 0, x0, y0, x1, y1, offset)
    b = UVPatchPoint(radius, 1, 0, x0, y0, x1, y1, offset)
    c = UVPatchPoint(radius, 1, 1, x0, y0, x1, y1, offset)
    d = UVPatchPoint(radius, 0, 1, x0, y0, x1, y1, offset)
    m = UVPatchPoint(radius, 0.5, 0.5, x0, y0, x1, y1, offset)
    x_min = min(a[0], b[0], c[0], d[0], m[0])
    y_min = min(a[1], b[1], c[1], d[1], m[1])
    z_min = min(a[2], b[2], c[2], d[2], m[2])
    x_max = max(a[0], b[0], c[0], d[0], m[0])
    y_max = max(a[1], b[1], c[1], d[1], m[1])
    z_max = max(a[2], b[2], c[2], d[2], m[2])
    return BoundingBox(LPoint3(x_min, y_min, z_min),
                       LPoint3(x_max, y_max, z_max))
    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 = []
Example #9
0
def test_nodepath_user_data():
    from panda3d.core import NodePath, BoundingBox

    # Randomly chose this for testing.
    data = BoundingBox()

    path = NodePath("node")
    assert not path.has_user_data()

    path.set_user_data(data)
    assert path.has_user_data()
    assert path.get_user_data() == data

    path.clear_user_data()
    assert not path.has_user_data()
Example #10
0
    def calcBoundingBox(self):
        self.minX = None
        self.minY = None
        self.minZ = None
        self.maxX = None
        self.maxY = None
        self.maxZ = None
        mins = Point3(99999999)
        maxs = Point3(-99999999)
        for point in self.points:
            if point.x < mins.x:
                mins.x = point.x
            if point.y < mins.y:
                mins.y = point.y
            if point.z < mins.z:
                mins.z = point.z

            if point.x > maxs.x:
                maxs.x = point.x
            if point.y > maxs.y:
                maxs.y = point.y
            if point.z > maxs.z:
                maxs.z = point.z

            if self.minX is None or point.x < self.minX.x:
                self.minX = point
            if self.minY is None or point.y < self.minY.y:
                self.minY = point
            if self.minZ is None or point.z < self.minZ.z:
                self.minZ = point
            if self.maxX is None or point.x > self.maxX.x:
                self.maxX = point
            if self.maxY is None or point.y > self.maxY.y:
                self.maxY = point
            if self.maxZ is None or point.z > self.maxZ.z:
                self.maxZ = point

        self.mins = mins
        self.maxs = maxs

        self.boundingBox = BoundingBox(self.mins, self.maxs)
        self.extents = [
            self.minX, self.minY, self.minZ, self.maxX, self.maxY, self.maxZ
        ]
Example #11
0
    def __init__(self, point_cloud_size):
        format = GeomVertexFormat.getV3c4t2()
        vdata = GeomVertexData('point', format, Geom.UHDynamic)
        self._pos_writer = GeomVertexWriter(vdata, 'vertex')
        self._color_writer = GeomVertexWriter(vdata, 'color')
        self._tex_writer = GeomVertexWriter(vdata, 'texcoord')
        self._point_cloud_size = point_cloud_size
        self._prev_point_cloud_size = 0

        assert point_cloud_size > 0
        vdata.setNumRows(point_cloud_size * 6)
        self._tex_writer.set_row(0)
        set_texture(self._tex_writer, point_cloud_size)
        pnts = GeomTriangles(Geom.UHStatic)
        pnts.addConsecutiveVertices(0, 3 * 2 * point_cloud_size)
        pnts.closePrimitive()
        points_geom = Geom(vdata)
        points_geom.addPrimitive(pnts)
        snode = GeomNode('points')
        snode.addGeom(points_geom)
        dir_name = osp.dirname(__file__)
        # print(osp.join(dir_name, 'pnts_vs.glsl'))
        vs_shader = osp.join(dir_name, 'pnts_vs.glsl')
        fs_shader = osp.join(dir_name, 'pnts_fs.glsl')
        myShader = Shader.load(
            Shader.SL_GLSL,
            vertex=Filename.fromOsSpecific(vs_shader).getFullpath(),
            fragment=Filename.fromOsSpecific(fs_shader).getFullpath())

        assert myShader is not None
        self.points_node = base.render.attachNewNode(snode)
        self.points_node.setPos(0., 0., 0.)
        self.points_node.set_shader(myShader)
        self.points_node.set_shader_input(
            "view_size", (base.win.getXSize(), base.win.getYSize()))
        self.points_node.node().setBounds(
            BoundingBox((-1000., -1000., -1000.), (1000., 1000., 1000.)))
        self.points_node.setTransparency(TransparencyAttrib.MAlpha)
    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 = []
Example #13
0
    def __init__(self, world, x1, y1, x2, y2, z):
        self.world = world
        logging.info(('setting up water plane at z=' + str(z)))

        # Water surface
        maker = CardMaker('water')
        maker.setFrame(x1, x2, y1, y2)

        self.waterNP = render.attachNewNode(maker.generate())
        self.waterNP.setHpr(0, -90, 0)
        self.waterNP.setPos(0, 0, z)
        self.waterNP.setTransparency(TransparencyAttrib.MAlpha)
        self.waterNP.setShader(loader.loadShader('shaders/water.sha'))
        self.waterNP.setShaderInput('wateranim',
                                    Vec4(0.03, -0.015, 64.0,
                                         0))  # vx, vy, scale, skip
        # offset, strength, refraction factor (0=perfect mirror, 1=total refraction), refractivity
        self.waterNP.setShaderInput('waterdistort', Vec4(0.4, 4.0, 0.25, 0.45))
        self.waterNP.setShaderInput('time', 0)

        # Reflection plane
        self.waterPlane = Plane(Vec3(0, 0, z + 1), Point3(0, 0, z))
        planeNode = PlaneNode('waterPlane')
        planeNode.setPlane(self.waterPlane)

        # Buffer and reflection camera
        buffer = base.win.makeTextureBuffer('waterBuffer', 512, 512)
        buffer.setClearColor(Vec4(0, 0, 0, 1))

        cfa = CullFaceAttrib.makeReverse()
        rs = RenderState.make(cfa)

        self.watercamNP = base.makeCamera(buffer)
        self.watercamNP.reparentTo(render)

        #sa = ShaderAttrib.make()
        #sa = sa.setShader(loader.loadShader('shaders/splut3Clipped.sha') )

        cam = self.watercamNP.node()
        cam.getLens().setFov(base.camLens.getFov())
        cam.getLens().setNear(1)
        cam.getLens().setFar(5000)
        cam.setInitialState(rs)
        cam.setTagStateKey('Clipped')
        #cam.setTagState('True', RenderState.make(sa))

        # ---- water textures ---------------------------------------------

        # reflection texture, created in realtime by the 'water camera'
        tex0 = buffer.getTexture()
        tex0.setWrapU(Texture.WMClamp)
        tex0.setWrapV(Texture.WMClamp)
        ts0 = TextureStage('reflection')
        self.waterNP.setTexture(ts0, tex0)

        # distortion texture
        tex1 = loader.loadTexture('textures/water.png')
        ts1 = TextureStage('distortion')
        self.waterNP.setTexture(ts1, tex1)

        # ---- Fog --- broken
        min = Point3(x1, y1, -999.0)
        max = Point3(x2, y2, z)
        boundry = BoundingBox(min, max)
        self.waterFog = Fog('waterFog')
        self.waterFog.setBounds(boundry)
        colour = (0.2, 0.5, 0.8)
        self.waterFog.setColor(*colour)
        self.waterFog.setExpDensity(0.05)
        render.attachNewNode(self.waterFog)
        #render.setFog(world.waterFog)
        taskMgr.add(self.update, "waterTask")
class GlobalIllumination(DebugObject):
    """ This class handles the global illumination processing. To process the
    global illumination, the scene is first rasterized from 3 directions, and 
    a 3D voxel grid is created. After that, the mipmaps of the voxel grid are
    generated. The final shader then performs voxel cone tracing to compute 
    an ambient, diffuse and specular term.

    The gi is split over several frames to reduce the computation cost. Currently
    there are 5 different steps, split over 4 frames:

    Frame 1: 
        - Rasterize the scene from the x-axis

    Frame 2:
        - Rasterize the scene from the y-axis

    Frame 3: 
        - Rasterize the scene from the z-axis

    Frame 4:
        - Copy the generated temporary voxel grid into a stable voxel grid
        - Generate the mipmaps for that stable voxel grid using a gaussian filter

    In the final pass the stable voxel grid is sampled. The voxel tracing selects
    the mipmap depending on the cone size. This enables small scale details as well
    as blurry reflections and low-frequency ao / diffuse. For performance reasons,
    the final pass is executed at half window resolution and then bilateral upscaled.
    """

    QualityLevels = ["Low", "Medium", "High", "Ultra"]

    def __init__(self, pipeline):
        DebugObject.__init__(self, "GlobalIllumnination")
        self.pipeline = pipeline

        self.qualityLevel = self.pipeline.settings.giQualityLevel

        if self.qualityLevel not in self.QualityLevels:
            self.fatal("Unsupported gi quality level:" + self.qualityLevel)

        self.qualityLevelIndex = self.QualityLevels.index(self.qualityLevel)

        # Grid size in world space units
        self.voxelGridSize = self.pipeline.settings.giVoxelGridSize

        # Grid resolution in pixels
        self.voxelGridResolution = [32, 64, 128, 192][self.qualityLevelIndex]

        # Has to be a multiple of 2
        self.distributionSteps = [16, 30, 60, 90][self.qualityLevelIndex]
        self.slideCount = int(self.voxelGridResolution / 8)
        self.slideVertCount = self.voxelGridResolution / self.slideCount

        self.bounds = BoundingBox()
        self.renderCount = 0

        # Create the task manager
        self.taskManager = DistributedTaskManager()

        self.gridPosLive = PTALVecBase3f.emptyArray(1)
        self.gridPosTemp = PTALVecBase3f.emptyArray(1)

        # Store ready state
        self.readyStateFlag = PTAFloat.emptyArray(1)
        self.readyStateFlag[0] = 0

        self.frameIndex = 0
        self.steps = []

    def _createDebugTexts(self):
        """ Creates a debug overlay to show GI status """
        self.debugText = None
        self.buildingText = None

        if self.pipeline.settings.displayDebugStats:
            self.debugText = FastText(pos=Vec2(
                Globals.base.getAspectRatio() - 0.1, 0.88),
                                      rightAligned=True,
                                      color=Vec3(1, 1, 0),
                                      size=0.03)
            self.buildingText = FastText(pos=Vec2(-0.3, 0),
                                         rightAligned=False,
                                         color=Vec3(1, 1, 0),
                                         size=0.03)
            self.buildingText.setText("PREPARING GI, PLEASE BE PATIENT ....")

    def stepVoxelize(self, idx):

        # If we are at the beginning of the frame, compute the new grid position
        if idx == 0:
            self.gridPosTemp[0] = self._computeGridPos()
            # Clear voxel grid at the beginning
            # for tex in self.generationTextures:
            # tex.clearImage()

            self.clearGridTarget.setActive(True)

            if self.debugText is not None:
                self.debugText.setText("GI Grid Center: " + ", ".join(
                    str(round(i, 2)) for i in self.gridPosTemp[0]) +
                                       " / GI Frame " + str(self.renderCount))

            self.renderCount += 1

            if self.renderCount == 3:
                self.readyStateFlag[0] = 1.0
                if self.buildingText:
                    self.buildingText.remove()
                    self.buildingText = None

        self.voxelizePass.voxelizeSceneFromDirection(self.gridPosTemp[0],
                                                     "xyz"[idx])

    def stepDistribute(self, idx):

        if idx == 0:

            skyBegin = 142.0
            skyInGrid = (skyBegin -
                         self.gridPosTemp[0].z) / (2.0 * self.voxelGridSize)
            skyInGrid = int(skyInGrid * self.voxelGridResolution)
            self.convertGridTarget.setShaderInput("skyStartZ", skyInGrid)
            self.convertGridTarget.setActive(True)

        self.distributeTarget.setActive(True)

        swap = idx % 2 == 0
        sources = self.pingDataTextures if swap else self.pongDataTextures
        dests = self.pongDataTextures if swap else self.pingDataTextures

        if idx == self.distributionSteps - 1:
            self.publishGrid()
            dests = self.dataTextures

        for i, direction in enumerate(self.directions):
            self.distributeTarget.setShaderInput("src" + direction, sources[i])
            self.distributeTarget.setShaderInput("dst" + direction, dests[i])

        # Only do the last blur-step on high+ quality, leads to artifacts otherwise
        # due to the low grid resolution
        if self.qualityLevel in ["High", "Ultra"]:
            self.distributeTarget.setShaderInput(
                "isLastStep", idx >= self.distributionSteps - 1)
        self.distributeTarget.setShaderInput("writeSolidness",
                                             idx >= self.distributionSteps - 1)

    def publishGrid(self):
        """ This function gets called when the grid is ready to be used, and updates
        the live grid data """
        self.gridPosLive[0] = self.gridPosTemp[0]
        self.bounds.setMinMax(self.gridPosLive[0] - Vec3(self.voxelGridSize),
                              self.gridPosLive[0] + Vec3(self.voxelGridSize))

    def getBounds(self):
        """ Returns the bounds of the gi grid """
        return self.bounds

    def update(self):
        """ Processes the gi, this method is called every frame """

        # Disable all buffers here before starting the rendering
        self.disableTargets()
        # for target in self.mipmapTargets:
        # target.setActive(False)

        self.taskManager.process()

    def disableTargets(self):
        """ Disables all active targets """
        self.voxelizePass.setActive(False)
        self.convertGridTarget.setActive(False)
        self.clearGridTarget.setActive(False)
        self.distributeTarget.setActive(False)

    def setup(self):
        """ Setups everything for the GI to work """
        assert (self.distributionSteps % 2 == 0)

        self._createDebugTexts()

        self.pipeline.getRenderPassManager().registerDefine(
            "USE_GLOBAL_ILLUMINATION", 1)
        self.pipeline.getRenderPassManager().registerDefine(
            "GI_SLIDE_COUNT", self.slideCount)
        self.pipeline.getRenderPassManager().registerDefine(
            "GI_QUALITY_LEVEL", self.qualityLevelIndex)

        # make the grid resolution a constant
        self.pipeline.getRenderPassManager().registerDefine(
            "GI_GRID_RESOLUTION", self.voxelGridResolution)

        self.taskManager.addTask(3, self.stepVoxelize)
        self.taskManager.addTask(self.distributionSteps, self.stepDistribute)

        # Create the voxelize pass which is used to voxelize the scene from
        # several directions
        self.voxelizePass = VoxelizePass(self.pipeline)
        self.voxelizePass.setVoxelGridResolution(self.voxelGridResolution)
        self.voxelizePass.setVoxelGridSize(self.voxelGridSize)
        self.voxelizePass.setGridResolutionMultiplier(1)
        self.pipeline.getRenderPassManager().registerPass(self.voxelizePass)

        self.generationTextures = []

        # Create the buffers used to create the voxel grid
        for color in "rgb":
            tex = Texture("VoxelGeneration-" + color)
            tex.setup3dTexture(self.voxelGridResolution,
                               self.voxelGridResolution,
                               self.voxelGridResolution, Texture.TInt,
                               Texture.FR32)
            tex.setClearColor(Vec4(0))
            self.generationTextures.append(tex)
            Globals.render.setShaderInput("voxelGenDest" + color.upper(), tex)

            MemoryMonitor.addTexture("VoxelGenerationTex-" + color.upper(),
                                     tex)

        self.bindTo(Globals.render, "giData")

        self.convertGridTarget = RenderTarget("ConvertGIGrid")
        self.convertGridTarget.setSize(
            self.voxelGridResolution * self.slideCount,
            self.voxelGridResolution * self.slideVertCount)

        if self.pipeline.settings.useDebugAttachments:
            self.convertGridTarget.addColorTexture()
        self.convertGridTarget.prepareOffscreenBuffer()

        # Set a near-filter to the texture
        if self.pipeline.settings.useDebugAttachments:
            self.convertGridTarget.getColorTexture().setMinfilter(
                Texture.FTNearest)
            self.convertGridTarget.getColorTexture().setMagfilter(
                Texture.FTNearest)

        self.clearGridTarget = RenderTarget("ClearGIGrid")
        self.clearGridTarget.setSize(
            self.voxelGridResolution * self.slideCount,
            self.voxelGridResolution * self.slideVertCount)
        if self.pipeline.settings.useDebugAttachments:
            self.clearGridTarget.addColorTexture()
        self.clearGridTarget.prepareOffscreenBuffer()

        for idx, color in enumerate("rgb"):
            self.convertGridTarget.setShaderInput(
                "voxelGenSrc" + color.upper(), self.generationTextures[idx])
            self.clearGridTarget.setShaderInput("voxelGenTex" + color.upper(),
                                                self.generationTextures[idx])

        # Create the data textures
        self.dataTextures = []
        self.directions = ["PosX", "NegX", "PosY", "NegY", "PosZ", "NegZ"]

        for i, direction in enumerate(self.directions):
            tex = Texture("GIDataTex" + direction)
            tex.setup3dTexture(self.voxelGridResolution,
                               self.voxelGridResolution,
                               self.voxelGridResolution, Texture.TFloat,
                               Texture.FR11G11B10)
            MemoryMonitor.addTexture("VoxelDataTex-" + direction, tex)
            self.dataTextures.append(tex)
            self.pipeline.getRenderPassManager().registerStaticVariable(
                "giVoxelData" + direction, tex)

        # Create ping / pong textures
        self.pingDataTextures = []
        self.pongDataTextures = []

        for i, direction in enumerate(self.directions):
            texPing = Texture("GIPingDataTex" + direction)
            texPing.setup3dTexture(self.voxelGridResolution,
                                   self.voxelGridResolution,
                                   self.voxelGridResolution, Texture.TFloat,
                                   Texture.FR11G11B10)
            MemoryMonitor.addTexture("VoxelPingDataTex-" + direction, texPing)
            self.pingDataTextures.append(texPing)

            texPong = Texture("GIPongDataTex" + direction)
            texPong.setup3dTexture(self.voxelGridResolution,
                                   self.voxelGridResolution,
                                   self.voxelGridResolution, Texture.TFloat,
                                   Texture.FR11G11B10)
            MemoryMonitor.addTexture("VoxelPongDataTex-" + direction, texPong)
            self.pongDataTextures.append(texPong)

            self.convertGridTarget.setShaderInput("voxelDataDest" + direction,
                                                  self.pingDataTextures[i])
            # self.clearGridTarget.setShaderInput("voxelDataDest" + str(i), self.pongDataTextures[i])

        # Set texture wrap modes
        for tex in self.pingDataTextures + self.pongDataTextures + self.dataTextures + self.generationTextures:
            tex.setMinfilter(Texture.FTLinear)
            tex.setMagfilter(Texture.FTLinear)
            tex.setWrapU(Texture.WMBorderColor)
            tex.setWrapV(Texture.WMBorderColor)
            tex.setWrapW(Texture.WMBorderColor)
            tex.setAnisotropicDegree(0)
            tex.setBorderColor(Vec4(0))

        for tex in self.dataTextures:
            tex.setMinfilter(Texture.FTLinear)
            tex.setMagfilter(Texture.FTLinear)

        self.distributeTarget = RenderTarget("DistributeVoxels")
        self.distributeTarget.setSize(
            self.voxelGridResolution * self.slideCount,
            self.voxelGridResolution * self.slideVertCount)
        if self.pipeline.settings.useDebugAttachments:
            self.distributeTarget.addColorTexture()
        self.distributeTarget.prepareOffscreenBuffer()

        # Set a near-filter to the texture
        if self.pipeline.settings.useDebugAttachments:
            self.distributeTarget.getColorTexture().setMinfilter(
                Texture.FTNearest)
            self.distributeTarget.getColorTexture().setMagfilter(
                Texture.FTNearest)

        self.distributeTarget.setShaderInput("isLastStep", False)

        # Create solidness texture
        self.voxelSolidTex = Texture("GIDataSolidTex")
        self.voxelSolidTex.setup3dTexture(self.voxelGridResolution,
                                          self.voxelGridResolution,
                                          self.voxelGridResolution,
                                          Texture.TFloat, Texture.FR16)
        self.convertGridTarget.setShaderInput("voxelSolidDest",
                                              self.voxelSolidTex)
        self.distributeTarget.setShaderInput("voxelSolidTex",
                                             self.voxelSolidTex)
        MemoryMonitor.addTexture("VoxelSolidTex", self.voxelSolidTex)

        self.voxelSolidStableTex = Texture("GIDataSolidStableTex")
        self.voxelSolidStableTex.setup3dTexture(self.voxelGridResolution,
                                                self.voxelGridResolution,
                                                self.voxelGridResolution,
                                                Texture.TFloat, Texture.FR16)

        self.distributeTarget.setShaderInput("voxelSolidWriteTex",
                                             self.voxelSolidStableTex)
        self.pipeline.getRenderPassManager().registerStaticVariable(
            "giVoxelSolidTex", self.voxelSolidStableTex)

        # Create the final gi pass
        self.finalPass = GlobalIlluminationPass()
        self.pipeline.getRenderPassManager().registerPass(self.finalPass)
        self.pipeline.getRenderPassManager().registerDynamicVariable(
            "giData", self.bindTo)
        self.pipeline.getRenderPassManager().registerStaticVariable(
            "giReadyState", self.readyStateFlag)

        # Visualize voxels
        if False:
            self.voxelCube = loader.loadModel("Box")
            self.voxelCube.reparentTo(render)
            # self.voxelCube.setTwoSided(True)
            self.voxelCube.node().setFinal(True)
            self.voxelCube.node().setBounds(OmniBoundingVolume())
            self.voxelCube.setInstanceCount(self.voxelGridResolution**3)
            # self.voxelCube.hide()
            self.bindTo(self.voxelCube, "giData")

            for i in xrange(5):
                self.voxelCube.setShaderInput("giDataTex" + str(i),
                                              self.pingDataTextures[i])

        self.disableTargets()

    def _createConvertShader(self):
        """ Loads the shader for converting the voxel grid """
        shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex",
                             "Shader/GI/ConvertGrid.fragment")
        self.convertGridTarget.setShader(shader)

    def _createClearShader(self):
        """ Loads the shader for converting the voxel grid """
        shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex",
                             "Shader/GI/ClearGrid.fragment")
        self.clearGridTarget.setShader(shader)

    def _createGenerateMipmapsShader(self):
        """ Loads the shader for generating the voxel grid mipmaps """
        computeSize = self.voxelGridResolution
        for child in self.mipmapTargets:
            computeSize /= 2
            shader = Shader.load(
                Shader.SLGLSL, "Shader/DefaultPostProcess.vertex",
                "Shader/GI/GenerateMipmaps/" + str(computeSize) + ".fragment")
            child.setShader(shader)

    def _createDistributionShader(self):
        """ Creates the photon distribution shader """
        shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex",
                             "Shader/GI/Distribute.fragment")
        self.distributeTarget.setShader(shader)

    def _createBlurShader(self):
        """ Creates the photon distribution shader """
        shader = Shader.load(Shader.SLGLSL, "Shader/DefaultPostProcess.vertex",
                             "Shader/GI/BlurPhotonGrid.fragment")
        self.blurBuffer.setShader(shader)

    def reloadShader(self):
        """ Reloads all shaders and updates the voxelization camera state aswell """
        self.debug("Reloading shaders")
        self._createConvertShader()
        self._createClearShader()
        self._createDistributionShader()
        # self._createGenerateMipmapsShader()
        # self._createPhotonBoxShader()
        # self._createBlurShader()

        if hasattr(self, "voxelCube"):
            self.pipeline.setEffect(self.voxelCube,
                                    "Effects/DisplayVoxels.effect", {
                                        "normalMapping": False,
                                        "castShadows": False,
                                        "castGI": False
                                    })

    def _createPhotonBoxShader(self):
        """ Loads the shader to visualize the photons """
        shader = Shader.load(Shader.SLGLSL,
                             "Shader/DefaultShaders/Photon/vertex.glsl",
                             "Shader/DefaultShaders/Photon/fragment.glsl")
        # self.photonBox.setShader(shader, 100)

    def _computeGridPos(self):
        """ Computes the new center of the voxel grid. The center pos is also
        snapped, to avoid flickering. """

        # It is important that the grid is snapped, otherwise it will flicker
        # while the camera moves. When using a snap of 32, everything until
        # the log2(32) = 5th mipmap is stable.
        snap = 1.0
        stepSizeX = float(self.voxelGridSize * 2.0) / float(
            self.voxelGridResolution) * snap
        stepSizeY = float(self.voxelGridSize * 2.0) / float(
            self.voxelGridResolution) * snap
        stepSizeZ = float(self.voxelGridSize * 2.0) / float(
            self.voxelGridResolution) * snap

        gridPos = Globals.base.camera.getPos(Globals.base.render)
        gridPos.x -= gridPos.x % stepSizeX
        gridPos.y -= gridPos.y % stepSizeY
        gridPos.z -= gridPos.z % stepSizeZ
        return gridPos

    def bindTo(self, node, prefix):
        """ Binds all required shader inputs to a target to compute / display
        the global illumination """

        node.setShaderInput(prefix + ".positionGeneration", self.gridPosTemp)
        node.setShaderInput(prefix + ".position", self.gridPosLive)
        node.setShaderInput(prefix + ".size", self.voxelGridSize)
        node.setShaderInput(prefix + ".resolution", self.voxelGridResolution)
class GlobalIllumination(DebugObject):

    """ This class handles the global illumination processing. To process the
    global illumination, the scene is first rasterized from 3 directions, and 
    a 3D voxel grid is created. After that, the mipmaps of the voxel grid are
    generated. The final shader then performs voxel cone tracing to compute 
    an ambient, diffuse and specular term.

    The gi is split over several frames to reduce the computation cost. Currently
    there are 5 different steps, split over 4 frames:

    Frame 1: 
        - Rasterize the scene from the x-axis

    Frame 2:
        - Rasterize the scene from the y-axis

    Frame 3: 
        - Rasterize the scene from the z-axis

    Frame 4:
        - Copy the generated temporary voxel grid into a stable voxel grid
        - Generate the mipmaps for that stable voxel grid using a gaussian filter

    In the final pass the stable voxel grid is sampled. The voxel tracing selects
    the mipmap depending on the cone size. This enables small scale details as well
    as blurry reflections and low-frequency ao / diffuse. For performance reasons,
    the final pass is executed at half window resolution and then bilateral upscaled.
    """

    QualityLevels = ["Low", "Medium", "High", "Ultra"]

    def __init__(self, pipeline):
        DebugObject.__init__(self, "GlobalIllumnination")
        self.pipeline = pipeline

        self.qualityLevel = self.pipeline.settings.giQualityLevel

        if self.qualityLevel not in self.QualityLevels:
            self.fatal("Unsupported gi quality level:" + self.qualityLevel)

        self.qualityLevelIndex = self.QualityLevels.index(self.qualityLevel)

        # Grid size in world space units
        self.voxelGridSize = self.pipeline.settings.giVoxelGridSize
        
        # Grid resolution in pixels
        self.voxelGridResolution = [32, 64, 128, 192][self.qualityLevelIndex]

        # Has to be a multiple of 2
        self.distributionSteps = [16, 30, 60, 90][self.qualityLevelIndex]
        self.slideCount = int(self.voxelGridResolution / 8) 
        self.slideVertCount = self.voxelGridResolution / self.slideCount       

        self.bounds = BoundingBox()
        self.renderCount = 0

        # Create the task manager
        self.taskManager = DistributedTaskManager()

        self.gridPosLive = PTALVecBase3f.emptyArray(1)
        self.gridPosTemp = PTALVecBase3f.emptyArray(1)

        # Store ready state
        self.readyStateFlag = PTAFloat.emptyArray(1)
        self.readyStateFlag[0] = 0

        self.frameIndex = 0
        self.steps = []

    def _createDebugTexts(self):
        """ Creates a debug overlay to show GI status """
        self.debugText = None
        self.buildingText = None

        if self.pipeline.settings.displayDebugStats:
            self.debugText = FastText(pos=Vec2(
                Globals.base.getAspectRatio() - 0.1, 0.88), rightAligned=True, color=Vec3(1, 1, 0), size=0.03)
            self.buildingText = FastText(pos=Vec2(-0.3, 0), rightAligned=False, color=Vec3(1, 1, 0), size=0.03)
            self.buildingText.setText("PREPARING GI, PLEASE BE PATIENT ....")

    def stepVoxelize(self, idx):
        
        # If we are at the beginning of the frame, compute the new grid position
        if idx == 0:
            self.gridPosTemp[0] = self._computeGridPos()
            # Clear voxel grid at the beginning
            # for tex in self.generationTextures:
                # tex.clearImage()

            self.clearGridTarget.setActive(True)

            if self.debugText is not None:
                self.debugText.setText("GI Grid Center: " + ", ".join(str(round(i, 2)) for i in self.gridPosTemp[0]) + " / GI Frame " + str(self.renderCount) )
            
            self.renderCount += 1

            if self.renderCount == 3:
                self.readyStateFlag[0] = 1.0
                if self.buildingText:
                    self.buildingText.remove()
                    self.buildingText = None

        self.voxelizePass.voxelizeSceneFromDirection(self.gridPosTemp[0], "xyz"[idx])


    def stepDistribute(self, idx):
        
        if idx == 0:

            skyBegin = 142.0
            skyInGrid = (skyBegin - self.gridPosTemp[0].z) / (2.0 * self.voxelGridSize)
            skyInGrid = int(skyInGrid * self.voxelGridResolution)
            self.convertGridTarget.setShaderInput("skyStartZ", skyInGrid)
            self.convertGridTarget.setActive(True)           

        self.distributeTarget.setActive(True)

        swap = idx % 2 == 0
        sources = self.pingDataTextures if swap else self.pongDataTextures
        dests = self.pongDataTextures if swap else self.pingDataTextures

        if idx == self.distributionSteps - 1:
            self.publishGrid()
            dests = self.dataTextures

        for i, direction in enumerate(self.directions):
            self.distributeTarget.setShaderInput("src" + direction, sources[i])
            self.distributeTarget.setShaderInput("dst" + direction, dests[i])

        # Only do the last blur-step on high+ quality, leads to artifacts otherwise
        # due to the low grid resolution
        if self.qualityLevel in ["High", "Ultra"]:
            self.distributeTarget.setShaderInput("isLastStep", idx >= self.distributionSteps-1)
        self.distributeTarget.setShaderInput("writeSolidness", idx >= self.distributionSteps-1)

    def publishGrid(self):
        """ This function gets called when the grid is ready to be used, and updates
        the live grid data """
        self.gridPosLive[0] = self.gridPosTemp[0]
        self.bounds.setMinMax(self.gridPosLive[0]-Vec3(self.voxelGridSize), self.gridPosLive[0]+Vec3(self.voxelGridSize))

    def getBounds(self):
        """ Returns the bounds of the gi grid """
        return self.bounds

    def update(self):
        """ Processes the gi, this method is called every frame """

        # Disable all buffers here before starting the rendering
        self.disableTargets()
        # for target in self.mipmapTargets:
            # target.setActive(False)

        self.taskManager.process()

    def disableTargets(self):
        """ Disables all active targets """
        self.voxelizePass.setActive(False)
        self.convertGridTarget.setActive(False)
        self.clearGridTarget.setActive(False)
        self.distributeTarget.setActive(False)


    def setup(self):
        """ Setups everything for the GI to work """
        assert(self.distributionSteps % 2 == 0)

        self._createDebugTexts()

        self.pipeline.getRenderPassManager().registerDefine("USE_GLOBAL_ILLUMINATION", 1)
        self.pipeline.getRenderPassManager().registerDefine("GI_SLIDE_COUNT", self.slideCount)
        self.pipeline.getRenderPassManager().registerDefine("GI_QUALITY_LEVEL", self.qualityLevelIndex)

        # make the grid resolution a constant
        self.pipeline.getRenderPassManager().registerDefine("GI_GRID_RESOLUTION", self.voxelGridResolution)

        self.taskManager.addTask(3, self.stepVoxelize)
        self.taskManager.addTask(self.distributionSteps, self.stepDistribute)

        # Create the voxelize pass which is used to voxelize the scene from
        # several directions
        self.voxelizePass = VoxelizePass(self.pipeline)
        self.voxelizePass.setVoxelGridResolution(self.voxelGridResolution)
        self.voxelizePass.setVoxelGridSize(self.voxelGridSize)
        self.voxelizePass.setGridResolutionMultiplier(1)
        self.pipeline.getRenderPassManager().registerPass(self.voxelizePass)

        self.generationTextures = []

        # Create the buffers used to create the voxel grid
        for color in "rgb":
            tex = Texture("VoxelGeneration-" + color)
            tex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TInt, Texture.FR32)
            tex.setClearColor(Vec4(0))
            self.generationTextures.append(tex)
            Globals.render.setShaderInput("voxelGenDest" + color.upper(), tex)
            
            MemoryMonitor.addTexture("VoxelGenerationTex-" + color.upper(), tex)

        self.bindTo(Globals.render, "giData")

        self.convertGridTarget = RenderTarget("ConvertGIGrid")
        self.convertGridTarget.setSize(self.voxelGridResolution * self.slideCount, self.voxelGridResolution * self.slideVertCount)

        if self.pipeline.settings.useDebugAttachments:
            self.convertGridTarget.addColorTexture()
        self.convertGridTarget.prepareOffscreenBuffer()

        # Set a near-filter to the texture
        if self.pipeline.settings.useDebugAttachments:
            self.convertGridTarget.getColorTexture().setMinfilter(Texture.FTNearest)
            self.convertGridTarget.getColorTexture().setMagfilter(Texture.FTNearest)

        self.clearGridTarget = RenderTarget("ClearGIGrid")
        self.clearGridTarget.setSize(self.voxelGridResolution * self.slideCount, self.voxelGridResolution * self.slideVertCount)
        if self.pipeline.settings.useDebugAttachments:
            self.clearGridTarget.addColorTexture()
        self.clearGridTarget.prepareOffscreenBuffer()

        for idx, color in enumerate("rgb"):
            self.convertGridTarget.setShaderInput("voxelGenSrc" + color.upper(), self.generationTextures[idx])
            self.clearGridTarget.setShaderInput("voxelGenTex" + color.upper(), self.generationTextures[idx])


        # Create the data textures
        self.dataTextures = []
        self.directions = ["PosX", "NegX", "PosY", "NegY", "PosZ", "NegZ"]

        for i, direction in enumerate(self.directions):
            tex = Texture("GIDataTex" + direction)
            tex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR11G11B10)
            MemoryMonitor.addTexture("VoxelDataTex-" + direction, tex)
            self.dataTextures.append(tex)
            self.pipeline.getRenderPassManager().registerStaticVariable("giVoxelData" + direction, tex)


        # Create ping / pong textures
        self.pingDataTextures = []
        self.pongDataTextures = []

        for i, direction in enumerate(self.directions):
            texPing = Texture("GIPingDataTex" + direction)
            texPing.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR11G11B10)
            MemoryMonitor.addTexture("VoxelPingDataTex-" + direction, texPing)
            self.pingDataTextures.append(texPing)

            texPong = Texture("GIPongDataTex" + direction)
            texPong.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR11G11B10)
            MemoryMonitor.addTexture("VoxelPongDataTex-" + direction, texPong)
            self.pongDataTextures.append(texPong)

            self.convertGridTarget.setShaderInput("voxelDataDest"+direction, self.pingDataTextures[i])
            # self.clearGridTarget.setShaderInput("voxelDataDest" + str(i), self.pongDataTextures[i])
        
        # Set texture wrap modes
        for tex in self.pingDataTextures + self.pongDataTextures + self.dataTextures + self.generationTextures:
            tex.setMinfilter(Texture.FTLinear)
            tex.setMagfilter(Texture.FTLinear)
            tex.setWrapU(Texture.WMBorderColor)
            tex.setWrapV(Texture.WMBorderColor)
            tex.setWrapW(Texture.WMBorderColor)
            tex.setAnisotropicDegree(0)
            tex.setBorderColor(Vec4(0))

        for tex in self.dataTextures:
            tex.setMinfilter(Texture.FTLinear)
            tex.setMagfilter(Texture.FTLinear)

        self.distributeTarget = RenderTarget("DistributeVoxels")
        self.distributeTarget.setSize(self.voxelGridResolution * self.slideCount, self.voxelGridResolution * self.slideVertCount)
        if self.pipeline.settings.useDebugAttachments:
            self.distributeTarget.addColorTexture()
        self.distributeTarget.prepareOffscreenBuffer()

        # Set a near-filter to the texture
        if self.pipeline.settings.useDebugAttachments:
            self.distributeTarget.getColorTexture().setMinfilter(Texture.FTNearest)
            self.distributeTarget.getColorTexture().setMagfilter(Texture.FTNearest)

        self.distributeTarget.setShaderInput("isLastStep", False)

        # Create solidness texture
        self.voxelSolidTex = Texture("GIDataSolidTex")
        self.voxelSolidTex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR16)
        self.convertGridTarget.setShaderInput("voxelSolidDest", self.voxelSolidTex)
        self.distributeTarget.setShaderInput("voxelSolidTex", self.voxelSolidTex)
        MemoryMonitor.addTexture("VoxelSolidTex", self.voxelSolidTex)

        self.voxelSolidStableTex = Texture("GIDataSolidStableTex")
        self.voxelSolidStableTex.setup3dTexture(self.voxelGridResolution, self.voxelGridResolution, self.voxelGridResolution, Texture.TFloat, Texture.FR16)

        self.distributeTarget.setShaderInput("voxelSolidWriteTex", self.voxelSolidStableTex)
        self.pipeline.getRenderPassManager().registerStaticVariable("giVoxelSolidTex", self.voxelSolidStableTex)





        # Create the final gi pass
        self.finalPass = GlobalIlluminationPass()
        self.pipeline.getRenderPassManager().registerPass(self.finalPass)
        self.pipeline.getRenderPassManager().registerDynamicVariable("giData", self.bindTo)
        self.pipeline.getRenderPassManager().registerStaticVariable("giReadyState", self.readyStateFlag)


        # Visualize voxels
        if False:
            self.voxelCube = loader.loadModel("Box")
            self.voxelCube.reparentTo(render)
            # self.voxelCube.setTwoSided(True)
            self.voxelCube.node().setFinal(True)
            self.voxelCube.node().setBounds(OmniBoundingVolume())
            self.voxelCube.setInstanceCount(self.voxelGridResolution**3)
            # self.voxelCube.hide()
            self.bindTo(self.voxelCube, "giData")
            
            for i in xrange(5):
                self.voxelCube.setShaderInput("giDataTex" + str(i), self.pingDataTextures[i])

        self.disableTargets()

    def _createConvertShader(self):
        """ Loads the shader for converting the voxel grid """
        shader = Shader.load(Shader.SLGLSL, 
            "Shader/DefaultPostProcess.vertex", "Shader/GI/ConvertGrid.fragment")
        self.convertGridTarget.setShader(shader)

    def _createClearShader(self):
        """ Loads the shader for converting the voxel grid """
        shader = Shader.load(Shader.SLGLSL, 
            "Shader/DefaultPostProcess.vertex", "Shader/GI/ClearGrid.fragment")
        self.clearGridTarget.setShader(shader)

    def _createGenerateMipmapsShader(self):
        """ Loads the shader for generating the voxel grid mipmaps """
        computeSize = self.voxelGridResolution
        for child in self.mipmapTargets:
            computeSize /= 2
            shader = Shader.load(Shader.SLGLSL, 
                "Shader/DefaultPostProcess.vertex", 
                "Shader/GI/GenerateMipmaps/" + str(computeSize) + ".fragment")
            child.setShader(shader)

    def _createDistributionShader(self):
        """ Creates the photon distribution shader """
        shader = Shader.load(Shader.SLGLSL, 
            "Shader/DefaultPostProcess.vertex", "Shader/GI/Distribute.fragment")
        self.distributeTarget.setShader(shader)

    def _createBlurShader(self):
        """ Creates the photon distribution shader """
        shader = Shader.load(Shader.SLGLSL, 
            "Shader/DefaultPostProcess.vertex", "Shader/GI/BlurPhotonGrid.fragment")
        self.blurBuffer.setShader(shader)

    def reloadShader(self):
        """ Reloads all shaders and updates the voxelization camera state aswell """
        self.debug("Reloading shaders")
        self._createConvertShader()
        self._createClearShader()
        self._createDistributionShader()
        # self._createGenerateMipmapsShader()
        # self._createPhotonBoxShader()
        # self._createBlurShader()

        if hasattr(self, "voxelCube"):
            self.pipeline.setEffect(self.voxelCube, "Effects/DisplayVoxels.effect", {
                "normalMapping": False,
                "castShadows": False,
                "castGI": False
            })

    def _createPhotonBoxShader(self):
        """ Loads the shader to visualize the photons """
        shader = Shader.load(Shader.SLGLSL, 
            "Shader/DefaultShaders/Photon/vertex.glsl",
            "Shader/DefaultShaders/Photon/fragment.glsl")
        # self.photonBox.setShader(shader, 100)

    def _computeGridPos(self):
        """ Computes the new center of the voxel grid. The center pos is also
        snapped, to avoid flickering. """

        # It is important that the grid is snapped, otherwise it will flicker 
        # while the camera moves. When using a snap of 32, everything until
        # the log2(32) = 5th mipmap is stable. 
        snap = 1.0
        stepSizeX = float(self.voxelGridSize * 2.0) / float(self.voxelGridResolution) * snap
        stepSizeY = float(self.voxelGridSize * 2.0) / float(self.voxelGridResolution) * snap
        stepSizeZ = float(self.voxelGridSize * 2.0) / float(self.voxelGridResolution) * snap

        gridPos = Globals.base.camera.getPos(Globals.base.render)
        gridPos.x -= gridPos.x % stepSizeX
        gridPos.y -= gridPos.y % stepSizeY
        gridPos.z -= gridPos.z % stepSizeZ
        return gridPos

    def bindTo(self, node, prefix):
        """ Binds all required shader inputs to a target to compute / display
        the global illumination """

        node.setShaderInput(prefix + ".positionGeneration", self.gridPosTemp)
        node.setShaderInput(prefix + ".position", self.gridPosLive)
        node.setShaderInput(prefix + ".size", self.voxelGridSize)
        node.setShaderInput(prefix + ".resolution", self.voxelGridResolution)
class WaterNode(NodePath):
    Nothing = 0
    Touching = 2
    Submerged = 4

    def __init__(self, size, pos, depth, mask, spec = WaterSpec()):
        NodePath.__init__(self, 'waterNode')
        self.setPos(pos)

        self.spec = spec
        self.pos = pos
        self.depth = depth
        self.size = size
        self.mask = mask
        self.height = pos[2]
        
        normal = (0, 0, 1)
        tangent = (normal[0], normal[2], -normal[1])
        binormal = (normal[2], normal[1], -normal[0])
        
        # Build array for new format.
        array = GeomVertexArrayFormat()
        array.addColumn(InternalName.make('vertex'), 3, Geom.NTFloat32, Geom.CPoint)
        array.addColumn(InternalName.make('texcoord'), 2, Geom.NTFloat32, Geom.CTexcoord)
        array.addColumn(InternalName.make('normal'), 3, Geom.NTFloat32, Geom.CVector)
        array.addColumn(InternalName.make('binormal'), 3, Geom.NTFloat32, Geom.CVector)
        array.addColumn(InternalName.make('tangent'), 3, Geom.NTFloat32, Geom.CVector)

        # Create and register format.
        format = GeomVertexFormat()
        format.addArray(array)
        format = GeomVertexFormat.registerFormat(format)

        vdata = GeomVertexData('waterPlanes', format, Geom.UHStatic)
        vdata.setNumRows(4)
        vtxWriter = GeomVertexWriter(vdata, 'vertex')
        tcWriter = GeomVertexWriter(vdata, 'texcoord')
        tnWriter = GeomVertexWriter(vdata, 'tangent')
        bnWriter = GeomVertexWriter(vdata, 'binormal')
        normWriter = GeomVertexWriter(vdata, 'normal')
        # top left corner
        vtxWriter.addData3f(size[0], size[3], 0)
        tcWriter.addData2f(0, 1)
        normWriter.addData3f(*normal)
        tnWriter.addData3f(*tangent)
        bnWriter.addData3f(*binormal)
        # bottom left corner
        vtxWriter.addData3f(size[0], size[2], 0)
        tcWriter.addData2f(0, 0)
        normWriter.addData3f(*normal)
        tnWriter.addData3f(*tangent)
        bnWriter.addData3f(*binormal)
        # top right corner
        vtxWriter.addData3f(size[1], size[3], 0)
        tcWriter.addData2f(1, 1)
        normWriter.addData3f(*normal)
        tnWriter.addData3f(*tangent)
        bnWriter.addData3f(*binormal)
        # bottom right corner
        vtxWriter.addData3f(size[1], size[2], 0)
        tcWriter.addData2f(1, 0)
        normWriter.addData3f(*normal)
        tnWriter.addData3f(*tangent)
        bnWriter.addData3f(*binormal)
        
        topTris = GeomTriangles(Geom.UHStatic)
        topTris.addVertices(0, 1, 2)
        topTris.addVertices(3, 2, 1)
        topGeom = Geom(vdata)
        topGeom.addPrimitive(topTris)
        self.topNP = self.attachNewNode(GeomNode('waterTop'))
        self.topNP.node().addGeom(topGeom)
        
        # Reverse the winding for the bottom water plane
        botTris = GeomTriangles(Geom.UHStatic)
        botTris.addVertices(2, 1, 0)
        botTris.addVertices(1, 2, 3)        
        botGeom = Geom(vdata)
        botGeom.addPrimitive(botTris)
        self.botNP = self.attachNewNode(GeomNode('waterBot'))
        self.botNP.node().addGeom(botGeom)

        # Create an AABB which defines the volume of this water.
        self.aabb = BoundingBox(Point3(size[0], size[2], -depth), Point3(size[1], size[3], 0))
        self.aabb.xform(self.getMat(render))
        
        self.cubemap = base.bspLoader.getClosestCubemapTexture(self.getPos(render))

        self.dudvFrame = 0

    def setup(self):
        self.reparentTo(render)
        self.hide(CIGlobals.ReflectionCameraBitmask | CIGlobals.RefractionCameraBitmask)
        #self.setLightOff(1)
        self.setMaterialOff(1)
        self.setTransparency(1)

    def disableEffects(self):
        self.topNP.clearShader()
        self.botNP.clearShader()

    def enableFakeEffect(self):
        self.disableEffects()

        staticTex = loader.loadTexture(self.spec.staticTex)
        self.topNP.setTexture(staticTex, 1)
        self.botNP.setTexture(staticTex, 1)

    def disableFakeEffect(self):
        self.topNP.clearTexture()
        self.botNP.clearTexture()

    def enableEffects(self, reflScene, refrScene, underwaterRefrScene):
        self.disableFakeEffect()

        static = loader.loadTexture(self.spec.staticTex)
        
        if not self.spec.cheap:
            self.topNP.setShader(Shader.load(Shader.SL_GLSL, "shaders/water_v.glsl",
                                             "shaders/water_f.glsl"), 2)
            self.topNP.setShaderInput("dudv", static)
            self.topNP.setShaderInput("dudv_tile", self.spec.dudvTile)
            self.topNP.setShaderInput("dudv_strength", self.spec.dudvStrength)
            self.topNP.setShaderInput("move_factor", self.spec.moveFactor)
            self.topNP.setShaderInput("near", CIGlobals.DefaultCameraNear)
            self.topNP.setShaderInput("far", CIGlobals.DefaultCameraFar)
            self.topNP.setShaderInput("normal_map", static)
            self.topNP.setShaderInput("fog_density", self.spec.fog.density)
            self.topNP.setShaderInput("fog_color", self.spec.fog.color)
            self.topNP.setShaderInput("water_tint", self.spec.tint)
            self.topNP.setShaderInput("reflect_factor", self.spec.reflectFactor)
            self.topNP.setShaderInput("static_depth", self.depth)

            self.botNP.setShader(Shader.load(Shader.SL_GLSL, "shaders/water_bottom_v.glsl",
                                             "shaders/water_bottom_f.glsl"), 2)
            self.botNP.setShaderInput("dudv", static)
            self.botNP.setShaderInput("dudv_tile", self.spec.dudvTile)
            self.botNP.setShaderInput("dudv_strength", self.spec.dudvStrength)
            self.botNP.setShaderInput("move_factor", self.spec.moveFactor)
            self.botNP.setShaderInput("water_tint", self.spec.tint)
        else:
            # We are doing cheap water
            self.topNP.setShader(Shader.load(Shader.SL_GLSL, "shaders/water_cheap_v.glsl",
                                             "shaders/water_cheap_f.glsl"), 2)
            self.topNP.setShaderInput("tex_scale", self.spec.dudvTile)
            self.topNP.setShaderInput("cube_map", self.cubemap)
            self.topNP.setShaderInput("normal_map", static)
            self.topNP.setShaderInput("base_map", static)
            self.topNP.setShaderInput("reflectivity", self.spec.reflectivity)
            self.topNP.setShaderInput("_exposureAdjustment", base.shaderGenerator.getExposureAdjustment())
            self.topNP.setShaderInput("water_tint", self.spec.tint)

        self.setTextureInputs(reflScene, refrScene, underwaterRefrScene)

    def setTextureInputs(self, reflScene, refrScene, underwaterRefrScene):
        if not self.spec.cheap:
            self.topNP.setShaderInput("refl", reflScene.texture)
            self.topNP.setShaderInput("refr", refrScene.texture)
            self.topNP.setShaderInput("refr_depth", refrScene.depthTex)

            self.botNP.setShaderInput("refr", underwaterRefrScene.texture)

    def isInWater(self, bottom, top):
        return self.aabb.contains(bottom, top)

    def isTouchingWater(self, position):
        return self.aabb.contains(position) != BoundingBox.IFNoIntersection

    def removeNode(self):
        self.topNP.removeNode()
        del self.topNP
        self.botNP.removeNode()
        del self.botNP
        del self.aabb
        del self.height
        del self.pos
        del self.size
        del self.depth
        NodePath.removeNode(self)
    def __init__(self, size, pos, depth, mask, spec = WaterSpec()):
        NodePath.__init__(self, 'waterNode')
        self.setPos(pos)

        self.spec = spec
        self.pos = pos
        self.depth = depth
        self.size = size
        self.mask = mask
        self.height = pos[2]
        
        normal = (0, 0, 1)
        tangent = (normal[0], normal[2], -normal[1])
        binormal = (normal[2], normal[1], -normal[0])
        
        # Build array for new format.
        array = GeomVertexArrayFormat()
        array.addColumn(InternalName.make('vertex'), 3, Geom.NTFloat32, Geom.CPoint)
        array.addColumn(InternalName.make('texcoord'), 2, Geom.NTFloat32, Geom.CTexcoord)
        array.addColumn(InternalName.make('normal'), 3, Geom.NTFloat32, Geom.CVector)
        array.addColumn(InternalName.make('binormal'), 3, Geom.NTFloat32, Geom.CVector)
        array.addColumn(InternalName.make('tangent'), 3, Geom.NTFloat32, Geom.CVector)

        # Create and register format.
        format = GeomVertexFormat()
        format.addArray(array)
        format = GeomVertexFormat.registerFormat(format)

        vdata = GeomVertexData('waterPlanes', format, Geom.UHStatic)
        vdata.setNumRows(4)
        vtxWriter = GeomVertexWriter(vdata, 'vertex')
        tcWriter = GeomVertexWriter(vdata, 'texcoord')
        tnWriter = GeomVertexWriter(vdata, 'tangent')
        bnWriter = GeomVertexWriter(vdata, 'binormal')
        normWriter = GeomVertexWriter(vdata, 'normal')
        # top left corner
        vtxWriter.addData3f(size[0], size[3], 0)
        tcWriter.addData2f(0, 1)
        normWriter.addData3f(*normal)
        tnWriter.addData3f(*tangent)
        bnWriter.addData3f(*binormal)
        # bottom left corner
        vtxWriter.addData3f(size[0], size[2], 0)
        tcWriter.addData2f(0, 0)
        normWriter.addData3f(*normal)
        tnWriter.addData3f(*tangent)
        bnWriter.addData3f(*binormal)
        # top right corner
        vtxWriter.addData3f(size[1], size[3], 0)
        tcWriter.addData2f(1, 1)
        normWriter.addData3f(*normal)
        tnWriter.addData3f(*tangent)
        bnWriter.addData3f(*binormal)
        # bottom right corner
        vtxWriter.addData3f(size[1], size[2], 0)
        tcWriter.addData2f(1, 0)
        normWriter.addData3f(*normal)
        tnWriter.addData3f(*tangent)
        bnWriter.addData3f(*binormal)
        
        topTris = GeomTriangles(Geom.UHStatic)
        topTris.addVertices(0, 1, 2)
        topTris.addVertices(3, 2, 1)
        topGeom = Geom(vdata)
        topGeom.addPrimitive(topTris)
        self.topNP = self.attachNewNode(GeomNode('waterTop'))
        self.topNP.node().addGeom(topGeom)
        
        # Reverse the winding for the bottom water plane
        botTris = GeomTriangles(Geom.UHStatic)
        botTris.addVertices(2, 1, 0)
        botTris.addVertices(1, 2, 3)        
        botGeom = Geom(vdata)
        botGeom.addPrimitive(botTris)
        self.botNP = self.attachNewNode(GeomNode('waterBot'))
        self.botNP.node().addGeom(botGeom)

        # Create an AABB which defines the volume of this water.
        self.aabb = BoundingBox(Point3(size[0], size[2], -depth), Point3(size[1], size[3], 0))
        self.aabb.xform(self.getMat(render))
        
        self.cubemap = base.bspLoader.getClosestCubemapTexture(self.getPos(render))

        self.dudvFrame = 0