class RenderingPipeline(DebugObject): """ This is the core class, driving all other classes. To use this pipeline, your code has to call this *after* the initialization of ShowBase: renderPipeline = RenderingPipeline() renderPipeline.loadSettings("pipeline.ini") renderPipeline.create() The pipeline will setup all required buffers, tasks and shaders itself. To add lights, see the documentation of LightManager. How it works: You can see an example buffer view at http://i.imgur.com/mZK6TVj.png The pipeline first renders all normal objects (parented to render) into a buffer, using multiple render targets. These buffers store normals, position and material properties. Your shaders have to output these values, but there is a handy api, just look at Shaders/DefaultObjectShader.fragment. After that, the pipeline splits the screen into tiles, typically of the size 32x32. For each tile, it computes which lights affect which tile, called Tiled Deferred Shading. This is written to a buffer. The next step is applying the lighting. This is done at half window resolution only, using Temporal Reprojection. I don't aim to explain Temporal Reprojection here, but basically, I render only each second pixel each frame. This is simply for performance. The lighting pass iterates through the list of lights per tile, and applies both lighting and shadows to each pixel, using the material information from the previous rendered buffers. After the lighting pass, a combiner pass combines both the current frame and the last frame, this is required because of Temporal Reprojection. At this step, we already have a frame we could display. In the next passes, only anti-aliasing and post-processing effects like motion blur are added. In the meantime, the LightManager builds a list of ShadowSources which need an update. It creates a scene render and renders the scene from the view of the shadow sources to the global shadow atlas. There are a limited amount of shadow updates per frame available, and the updates are stored in a queue. So when displaying many shadow-lights, not each shadowmap is update each frame. The reason is, again, performance. When you need a custom shadow caster shader, e.g. for alpha blending, you should use the Shader/DefaultShaowCaster.* as prefab. """ def __init__(self, showbase): """ Creates a new pipeline """ DebugObject.__init__(self, "RenderingPipeline") self.showbase = showbase self.settings = None self.mountManager = MountManager() def getMountManager(self): """ Returns the mount manager. You can use this to set the write directory and base path """ return self.mountManager def loadSettings(self, filename): """ Loads the pipeline settings from an ini file """ self.settings = PipelineSettingsManager() self.settings.loadFromFile(filename) def getSettings(self): """ Returns the current pipeline settings """ return self.settings def create(self): """ Creates this pipeline """ self.debug("Setting up render pipeline") if self.settings is None: self.error("You have to call loadSettings first!") return self.debug("Analyzing system ..") SystemAnalyzer.analyze() self.debug("Checking required Panda3D version ..") SystemAnalyzer.checkPandaVersionOutOfDate(01, 12, 2014) # Mount everything first self.mountManager.mount() # Store globals, as cython can't handle them self.debug("Setting up globals") Globals.load(self.showbase) Globals.font = loader.loadFont("Data/Font/SourceSansPro-Semibold.otf") Globals.font.setPixelsPerUnit(25) # Setting up shader loading BetterShader._DumpShaders = self.settings.dumpGeneratedShaders # We use PTA's for shader inputs, because that's faster than # using setShaderInput self.temporalProjXOffs = PTAInt.emptyArray(1) self.cameraPosition = PTAVecBase3f.emptyArray(1) self.motionBlurFactor = PTAFloat.emptyArray(1) self.lastMVP = PTALMatrix4f.emptyArray(1) self.currentMVP = PTALMatrix4f.emptyArray(1) self.currentShiftIndex = PTAInt.emptyArray(1) # Initialize variables self.camera = self.showbase.cam self.size = self._getSize() self.cullBounds = None # For the temporal reprojection it is important that the window width # is a multiple of 2 if self.settings.enableTemporalReprojection and self.size.x % 2 == 1: self.error( "The window has to have a width which is a multiple of 2 " "(Current: ", self.showbase.win.getXSize(), ")") self.error( "I'll correct that for you, but next time pass the correct " "window size!") wp = WindowProperties() wp.setSize(self.showbase.win.getXSize() + 1, self.showbase.win.getYSize()) self.showbase.win.requestProperties(wp) self.showbase.graphicsEngine.openWindows() # Get new size self.size = self._getSize() # Debug variables to disable specific features self.haveLightingPass = True # haveCombiner can only be true when haveLightingPass is enabled self.haveCombiner = True self.haveMRT = True # Not as good as I want it, so disabled. I'll work on it. self.blurEnabled = False self.debug("Window size is", self.size.x, "x", self.size.y) self.showbase.camLens.setNearFar(0.1, 50000) self.showbase.camLens.setFov(90) self.showbase.win.setClearColor(Vec4(1.0, 0.0, 1.0, 1.0)) # Create GI handler if self.settings.enableGlobalIllumination: self._setupGlobalIllumination() # Create occlusion handler self._setupOcclusion() if self.settings.displayOnscreenDebugger: self.guiManager = PipelineGuiManager(self) self.guiManager.setup() # Generate auto-configuration for shaders self._generateShaderConfiguration() # Create light manager, which handles lighting + shadows if self.haveLightingPass: self.lightManager = LightManager(self) self.patchSize = LVecBase2i(self.settings.computePatchSizeX, self.settings.computePatchSizeY) # Create separate scene graphs. The deferred graph is render self.forwardScene = NodePath("Forward-Rendering") self.transparencyScene = NodePath("Transparency-Rendering") self.transparencyScene.setBin("transparent", 30) # We need no transparency (we store other information in the alpha # channel) self.showbase.render.setAttrib( TransparencyAttrib.make(TransparencyAttrib.MNone), 100) # Now create deferred render buffers self._makeDeferredTargets() # Create the target which constructs the view-space normals and # position from world-space position if self.occlusion.requiresViewSpacePosNrm(): self._createNormalPrecomputeBuffer() if self.settings.enableGlobalIllumination: self._creatGIPrecomputeBuffer() # Setup the buffers for lighting self._createLightingPipeline() # Setup combiner for temporal reprojetion if self.haveCombiner and self.settings.enableTemporalReprojection: self._createCombiner() if self.occlusion.requiresBlurring(): self._createOcclusionBlurBuffer() self._setupAntialiasing() if self.blurEnabled: self._createDofStorage() self._createBlurBuffer() # Not sure why it has to be 0.25. But that leads to the best result aspect = float(self.size.y) / self.size.x self.onePixelShift = Vec2(0.125 / self.size.x, 0.125 / self.size.y / aspect) * self.settings.jitterAmount # Annoying that Vec2 has no multliply-operator for non-floats multiplyVec2 = lambda a, b: Vec2(a.x * b.x, a.y * b.y) if self.antialias.requiresJittering(): self.pixelShifts = [ multiplyVec2(self.onePixelShift, Vec2(-0.25, 0.25)), multiplyVec2(self.onePixelShift, Vec2(0.25, -0.25)) ] else: self.pixelShifts = [Vec2(0), Vec2(0)] self.currentPixelShift = PTAVecBase2f.emptyArray(1) self.lastPixelShift = PTAVecBase2f.emptyArray(1) self._setupFinalPass() self._setShaderInputs() # Give the gui a hint when the pipeline is done loading if self.settings.displayOnscreenDebugger: self.guiManager.onPipelineLoaded() # add update task self._attachUpdateTask() def getForwardScene(self): """ Reparent objects to this scene to use forward rendering. Objects in this scene will directly get rendered, with no lighting etc. applied. """ return self.forwardScene def getTransparentScene(self): """ Reparent objects to this scene to allow this objects to have transparency. Objects in this scene will get directly rendered and no lighting will get applied. """ return self.transparencyScene def _createCombiner(self): """ Creates the target which combines the result from the lighting computation and last frame together (Temporal Reprojection) """ self.combiner = RenderTarget("Combine-Temporal") self.combiner.addColorTexture() self.combiner.setColorBits(16) self.combiner.prepareOffscreenBuffer() self._setCombinerShader() def _setupGlobalIllumination(self): """ Creates the GI handler """ self.globalIllum = GlobalIllumination(self) self.globalIllum.setup() def _setupAntialiasing(self): """ Creates the antialiasing technique """ technique = self.settings.antialiasingTechnique self.debug("Creating antialiasing handler for", technique) if technique == "None": self.antialias = AntialiasingTechniqueNone() elif technique == "SMAA": self.antialias = AntialiasingTechniqueSMAA() elif technique == "FXAA": self.antialias = AntialiasingTechniqueFXAA() else: self.error("Unkown antialiasing technique", technique, "-> using None:") self.antialias = AntialiasingTechniqueNone() if self.occlusion.requiresBlurring(): self.antialias.setColorTexture( self.blurOcclusionH.getColorTexture()) else: if self.haveCombiner and self.settings.enableTemporalReprojection: self.antialias.setColorTexture(self.combiner.getColorTexture()) else: self.antialias.setColorTexture( self.lightingComputeContainer.getColorTexture()) self.antialias.setDepthTexture(self.deferredTarget.getDepthTexture()) self.antialias.setVelocityTexture(self.deferredTarget.getAuxTexture(1)) self.antialias.setup() def _setupOcclusion(self): """ Creates the occlusion technique """ technique = self.settings.occlusionTechnique self.debug("Creating occlusion handle for", technique) if technique == "None": self.occlusion = AmbientOcclusionTechniqueNone() elif technique == "SAO": self.occlusion = AmbientOcclusionTechniqueSAO() else: self.error("Unkown occlusion technique:", technique) self.occlusion = AmbientOcclusionTechniqueNone() def _makeDeferredTargets(self): """ Creates the multi-render-target """ self.debug("Creating deferred targets") self.deferredTarget = RenderTarget("DeferredTarget") self.deferredTarget.addColorAndDepth() if self.haveMRT: self.deferredTarget.addAuxTextures(3) self.deferredTarget.setAuxBits(16) self.deferredTarget.setColorBits(32) self.deferredTarget.setDepthBits(32) self.deferredTarget.prepareSceneRender() def _setupFinalPass(self): """ Setups the final pass which applies motion blur and so on """ # Set wrap for motion blur colorTex = self.antialias.getResultTexture() colorTex.setWrapU(Texture.WMClamp) colorTex.setWrapV(Texture.WMClamp) self._setFinalPassShader() def _makeLightPerTileStorage(self): """ Creates a texture to store the lights per tile into. Should get replaced with ssbos later """ storageSizeX = self.precomputeSize.x * 8 storageSizeY = self.precomputeSize.y * 8 self.debug("Creating per tile storage of size", storageSizeX, "x", storageSizeY) self.lightPerTileStorage = Texture("LightsPerTile") self.lightPerTileStorage.setup2dTexture(storageSizeX, storageSizeY, Texture.TUnsignedShort, Texture.FR32i) self.lightPerTileStorage.setMinfilter(Texture.FTNearest) self.lightPerTileStorage.setMagfilter(Texture.FTNearest) def _creatGIPrecomputeBuffer(self): """ Creates the half-resolution buffer which computes gi and gi reflections. We use half-res for performance """ self.giPrecomputeBuffer = RenderTarget("GICompute") self.giPrecomputeBuffer.setSize(self.size.x / 2, self.size.y / 2) self.giPrecomputeBuffer.addColorTexture() self.giPrecomputeBuffer.addAuxTextures(1) self.giPrecomputeBuffer.setColorBits(16) self.giPrecomputeBuffer.prepareOffscreenBuffer() def _createLightingPipeline(self): """ Creates the lighting pipeline, including shadow handling """ if not self.haveLightingPass: self.debug("Skipping lighting pipeline") return self.debug("Creating lighting pipeline ..") # size has to be a multiple of the compute unit size # but still has to cover the whole screen sizeX = int(math.ceil(float(self.size.x) / self.patchSize.x)) sizeY = int(math.ceil(float(self.size.y) / self.patchSize.y)) self.precomputeSize = LVecBase2i(sizeX, sizeY) self.debug("Batch size =", sizeX, "x", sizeY, "Actual Buffer size=", int(sizeX * self.patchSize.x), "x", int(sizeY * self.patchSize.y)) self._makeLightPerTileStorage() # Create a buffer which computes which light affects which tile self._makeLightBoundsComputationBuffer(sizeX, sizeY) # Create a buffer which applies the lighting self._makeLightingComputeBuffer() # Register for light manager self.lightManager.setLightingComputator(self.lightingComputeContainer) self.lightManager.setLightingCuller(self.lightBoundsComputeBuff) self._loadFallbackCubemap() self._loadLookupCubemap() def _setShaderInputs(self): """ Sets most of the required shader inputs to the targets """ # Shader inputs for the light-culling pass if self.haveLightingPass: self.lightBoundsComputeBuff.setShaderInput( "destination", self.lightPerTileStorage) self.lightBoundsComputeBuff.setShaderInput( "depth", self.deferredTarget.getDepthTexture()) self.lightBoundsComputeBuff.setShaderInput("mainCam", self.showbase.cam) self.lightBoundsComputeBuff.setShaderInput("mainRender", self.showbase.render) # Shader inputs for the light-applying pass self.lightingComputeContainer.setShaderInput( "data0", self.deferredTarget.getColorTexture()) self.lightingComputeContainer.setShaderInput( "data1", self.deferredTarget.getAuxTexture(0)) self.lightingComputeContainer.setShaderInput( "data2", self.deferredTarget.getAuxTexture(1)) self.lightingComputeContainer.setShaderInput( "data3", self.deferredTarget.getAuxTexture(2)) self.lightingComputeContainer.setShaderInput( "depth", self.deferredTarget.getDepthTexture()) self.lightingComputeContainer.setShaderInput( "mainCam", self.showbase.cam) self.lightingComputeContainer.setShaderInput( "mainRender", self.showbase.render) if self.occlusion.requiresViewSpacePosNrm(): self.lightingComputeContainer.setShaderInput( "viewSpaceNormals", self.normalPrecompute.getColorTexture()) self.lightingComputeContainer.setShaderInput( "viewSpacePosition", self.normalPrecompute.getAuxTexture(0)) self.lightingComputeContainer.setShaderInput( "shadowAtlas", self.lightManager.getAtlasTex()) if self.settings.useHardwarePCF: self.lightingComputeContainer.setShaderInput( "shadowAtlasPCF", self.lightManager.getAtlasTex(), self.lightManager.getPCFSampleState()) self.lightingComputeContainer.setShaderInput( "destination", self.lightingComputeCombinedTex) self.lightingComputeContainer.setShaderInput( "temporalProjXOffs", self.temporalProjXOffs) self.lightingComputeContainer.setShaderInput( "cameraPosition", self.cameraPosition) self.lightingComputeContainer.setShaderInput( "noiseTexture", self.showbase.loader.loadTexture( "Data/Occlusion/noise4x4.png")) self.lightingComputeContainer.setShaderInput( "lightsPerTile", self.lightPerTileStorage) if self.settings.enableGlobalIllumination: self.lightingComputeContainer.setShaderInput( "giDiffuseTex", self.giPrecomputeBuffer.getColorTexture()) self.lightingComputeContainer.setShaderInput( "giReflectionTex", self.giPrecomputeBuffer.getAuxTexture(0)) # Shader inputs for the occlusion blur passes if self.occlusion.requiresBlurring() and self.haveCombiner: self.blurOcclusionH.setShaderInput( "colorTex", self.blurOcclusionV.getColorTexture()) if self.settings.enableTemporalReprojection: self.blurOcclusionV.setShaderInput( "colorTex", self.combiner.getColorTexture()) else: self.blurOcclusionV.setShaderInput( "colorTex", self.lightingComputeContainer.getColorTexture()) self.blurOcclusionH.setShaderInput( "normalTex", self.deferredTarget.getAuxTexture(0)) self.blurOcclusionV.setShaderInput( "normalTex", self.deferredTarget.getAuxTexture(0)) self.blurOcclusionH.setShaderInput( "normalsView", self.normalPrecompute.getAuxTexture(0)) self.blurOcclusionV.setShaderInput( "normalsView", self.normalPrecompute.getAuxTexture(0)) # Shader inputs for the blur passes if self.blurEnabled: self.blurColorH.setShaderInput("dofStorage", self.dofStorage) self.blurColorV.setShaderInput("dofStorage", self.dofStorage) self.blurColorH.setShaderInput("colorTex", self.antialias.getResultTexture()) self.blurColorH.setShaderInput( "depthTex", self.deferredTarget.getDepthTexture()) self.blurColorV.setShaderInput("colorTex", self.blurColorH.getColorTexture()) # Shader inputs for the temporal reprojection if self.haveCombiner and self.settings.enableTemporalReprojection: self.combiner.setShaderInput( "currentComputation", self.lightingComputeContainer.getColorTexture()) self.combiner.setShaderInput("lastFrame", self.lightingComputeCombinedTex) self.combiner.setShaderInput("positionBuffer", self.deferredTarget.getColorTexture()) self.combiner.setShaderInput("velocityBuffer", self.deferredTarget.getAuxTexture(1)) self.combiner.setShaderInput("currentPixelShift", self.currentPixelShift) self.combiner.setShaderInput("lastPixelShift", self.lastPixelShift) if self.blurEnabled: self.combiner.setShaderInput("dofStorage", self.dofStorage) self.combiner.setShaderInput("depthTex", self.deferredTarget.getDepthTexture()) self.combiner.setShaderInput("lastPosition", self.lastPositionBuffer) self.combiner.setShaderInput("temporalProjXOffs", self.temporalProjXOffs) self.combiner.setShaderInput("lastMVP", self.lastMVP) self.combiner.setShaderInput("cameraPosition", self.cameraPosition) self.combiner.setShaderInput("currentMVP", self.lastMVP) # Shader inputs for the final pass if self.blurEnabled: self.deferredTarget.setShaderInput( "colorTex", self.blurColorV.getColorTexture()) else: self.deferredTarget.setShaderInput( "colorTex", self.antialias.getResultTexture()) if self.occlusion.requiresBlurring(): self.normalPrecompute.setShaderInput( "positionTex", self.deferredTarget.getColorTexture()) self.normalPrecompute.setShaderInput("mainCam", self.showbase.cam) self.normalPrecompute.setShaderInput("mainRender", self.showbase.render) self.normalPrecompute.setShaderInput( "depthTex", self.deferredTarget.getDepthTexture()) if self.haveMRT: self.deferredTarget.setShaderInput( "velocityTex", self.deferredTarget.getAuxTexture(1)) self.deferredTarget.setShaderInput( "depthTex", self.deferredTarget.getDepthTexture()) self.deferredTarget.setShaderInput("motionBlurFactor", self.motionBlurFactor) if self.haveLightingPass: self.deferredTarget.setShaderInput("lastFrame", self.lightingComputeCombinedTex) if self.haveCombiner and self.settings.enableTemporalReprojection: self.deferredTarget.setShaderInput("newFrame", self.combiner.getColorTexture()) self.deferredTarget.setShaderInput("lastPosition", self.lastPositionBuffer) self.deferredTarget.setShaderInput("debugTex", self.combiner.getColorTexture()) else: self.deferredTarget.setShaderInput( "debugTex", self.antialias.getResultTexture()) self.deferredTarget.setShaderInput( "currentPosition", self.deferredTarget.getColorTexture()) # Set last / current mvp handles self.showbase.render.setShaderInput("lastMVP", self.lastMVP) # Set GI inputs if self.settings.enableGlobalIllumination: self.globalIllum.bindTo(self.giPrecomputeBuffer, "giData") self.giPrecomputeBuffer.setShaderInput( "data0", self.deferredTarget.getColorTexture()) self.giPrecomputeBuffer.setShaderInput( "data1", self.deferredTarget.getAuxTexture(0)) self.giPrecomputeBuffer.setShaderInput( "data2", self.deferredTarget.getAuxTexture(1)) self.giPrecomputeBuffer.setShaderInput( "data3", self.deferredTarget.getAuxTexture(2)) self.giPrecomputeBuffer.setShaderInput("cameraPosition", self.cameraPosition) # Finally, set shaders self.reloadShaders() def _loadFallbackCubemap(self): """ Loads the cubemap for image based lighting """ print self.settings.defaultReflectionCubemap cubemap = self.showbase.loader.loadCubeMap( self.settings.defaultReflectionCubemap) cubemap.setMinfilter(Texture.FTLinearMipmapLinear) cubemap.setMagfilter(Texture.FTLinearMipmapLinear) cubemap.setFormat(Texture.F_srgb) print math.log(cubemap.getXSize(), 2) self.lightingComputeContainer.setShaderInput("fallbackCubemap", cubemap) self.lightingComputeContainer.setShaderInput( "fallbackCubemapMipmaps", math.log(cubemap.getXSize(), 2)) def _loadLookupCubemap(self): self.debug("Loading lookup cubemap") cubemap = self.showbase.loader.loadCubeMap( "Data/Cubemaps/DirectionLookup/#.png") cubemap.setMinfilter(Texture.FTNearest) cubemap.setMagfilter(Texture.FTNearest) cubemap.setFormat(Texture.F_rgb8) self.lightingComputeContainer.setShaderInput("directionToFace", cubemap) def _makeLightBoundsComputationBuffer(self, w, h): """ Creates the buffer which precomputes the lights per tile """ self.debug("Creating light precomputation buffer of size", w, "x", h) self.lightBoundsComputeBuff = RenderTarget("ComputeLightTileBounds") self.lightBoundsComputeBuff.setSize(w, h) self.lightBoundsComputeBuff.setColorWrite(False) self.lightBoundsComputeBuff.prepareOffscreenBuffer() def _makeLightingComputeBuffer(self): """ Creates the buffer which applies the lighting """ self.lightingComputeContainer = RenderTarget("ComputeLighting") if self.settings.enableTemporalReprojection: self.lightingComputeContainer.setSize(self.size.x / 2, self.size.y) else: self.lightingComputeContainer.setSize(self.size.x, self.size.y) self.lightingComputeContainer.addColorTexture() self.lightingComputeContainer.setColorBits(16) self.lightingComputeContainer.prepareOffscreenBuffer() self.lightingComputeCombinedTex = Texture("Lighting-Compute-Combined") self.lightingComputeCombinedTex.setup2dTexture(self.size.x, self.size.y, Texture.TFloat, Texture.FRgba8) self.lightingComputeCombinedTex.setMinfilter(Texture.FTLinear) self.lightingComputeCombinedTex.setMagfilter(Texture.FTLinear) self.lastPositionBuffer = Texture("Last-Position-Buffer") self.lastPositionBuffer.setup2dTexture(self.size.x, self.size.y, Texture.TFloat, Texture.FRgba16) self.lastPositionBuffer.setMinfilter(Texture.FTNearest) self.lastPositionBuffer.setMagfilter(Texture.FTNearest) def _createOcclusionBlurBuffer(self): """ Creates the buffers needed to blur the occlusion """ self.blurOcclusionV = RenderTarget("blurOcclusionVertical") self.blurOcclusionV.addColorTexture() self.blurOcclusionV.prepareOffscreenBuffer() self.blurOcclusionH = RenderTarget("blurOcclusionHorizontal") self.blurOcclusionH.addColorTexture() self.blurOcclusionH.prepareOffscreenBuffer() # Mipmaps for blur? # self.blurOcclusionV.getColorTexture().setMinfilter( # Texture.FTLinearMipmapLinear) # self.combiner.getColorTexture().setMinfilter( # Texture.FTLinearMipmapLinear) def _createBlurBuffer(self): """ Creates the buffers for the dof """ self.blurColorV = RenderTarget("blurColorVertical") self.blurColorV.addColorTexture() self.blurColorV.prepareOffscreenBuffer() self.blurColorH = RenderTarget("blurColorHorizontal") self.blurColorH.addColorTexture() self.blurColorH.prepareOffscreenBuffer() # self.blurColorH.getColorTexture().setMinfilter( # Texture.FTLinearMipmapLinear) # self.antialias.getResultTexture().setMinfilter( # Texture.FTLinearMipmapLinear) def _createNormalPrecomputeBuffer(self): """ Creates a buffer which reconstructs the normals and position from view-space """ self.normalPrecompute = RenderTarget("PrecomputeNormals") self.normalPrecompute.addColorTexture() self.normalPrecompute.addAuxTextures(1) self.normalPrecompute.setColorBits(16) self.normalPrecompute.setAuxBits(16) self.normalPrecompute.prepareOffscreenBuffer() def _createDofStorage(self): """ Creates the texture where the dof factor is stored in, so we don't recompute it each pass """ self.dofStorage = Texture("DOFStorage") self.dofStorage.setup2dTexture(self.size.x, self.size.y, Texture.TFloat, Texture.FRg16) def _setOcclusionBlurShader(self): """ Sets the shaders which blur the occlusion """ blurVShader = BetterShader.load( "Shader/DefaultPostProcess.vertex", "Shader/BlurOcclusionVertical.fragment") blurHShader = BetterShader.load( "Shader/DefaultPostProcess.vertex", "Shader/BlurOcclusionHorizontal.fragment") self.blurOcclusionV.setShader(blurVShader) self.blurOcclusionH.setShader(blurHShader) def _setGIComputeShader(self): """ Sets the shader which computes the GI """ giShader = BetterShader.load("Shader/DefaultPostProcess.vertex", "Shader/ComputeGI.fragment") self.giPrecomputeBuffer.setShader(giShader) def _setBlurShader(self): """ Sets the shaders which blur the color """ blurVShader = BetterShader.load("Shader/DefaultPostProcess.vertex", "Shader/BlurVertical.fragment") blurHShader = BetterShader.load("Shader/DefaultPostProcess.vertex", "Shader/BlurHorizontal.fragment") self.blurColorV.setShader(blurVShader) self.blurColorH.setShader(blurHShader) def _setLightingShader(self): """ Sets the shader which applies the light """ lightShader = BetterShader.load("Shader/DefaultPostProcess.vertex", "Shader/ApplyLighting.fragment") self.lightingComputeContainer.setShader(lightShader) def _setCombinerShader(self): """ Sets the shader which combines the lighting with the previous frame (temporal reprojection) """ cShader = BetterShader.load("Shader/DefaultPostProcess.vertex", "Shader/Combiner.fragment") self.combiner.setShader(cShader) def _setPositionComputationShader(self): """ Sets the shader which computes the lights per tile """ pcShader = BetterShader.load("Shader/DefaultPostProcess.vertex", "Shader/PrecomputeLights.fragment") self.lightBoundsComputeBuff.setShader(pcShader) def _setFinalPassShader(self): """ Sets the shader which computes the final frame, with motion blur and so on """ fShader = BetterShader.load("Shader/DefaultPostProcess.vertex", "Shader/Final.fragment") self.deferredTarget.setShader(fShader) def _getSize(self): """ Returns the window size. """ return LVecBase2i(self.showbase.win.getXSize(), self.showbase.win.getYSize()) def reloadShaders(self): """ Reloads all shaders """ if self.haveLightingPass: self.lightManager.debugReloadShader() self._setPositionComputationShader() self._setLightingShader() if self.haveCombiner and self.settings.enableTemporalReprojection: self._setCombinerShader() self._setFinalPassShader() if self.settings.enableGlobalIllumination: self._setGIComputeShader() if self.occlusion.requiresBlurring(): self._setOcclusionBlurShader() if self.blurEnabled: self._setBlurShader() if self.occlusion.requiresViewSpacePosNrm(): self._setNormalExtractShader() self.antialias.reloadShader() if self.settings.enableGlobalIllumination: self.globalIllum.reloadShader() def _setNormalExtractShader(self): """ Sets the shader which constructs the normals from position """ npShader = BetterShader.load("Shader/DefaultPostProcess.vertex", "Shader/ExtractNormals.fragment") self.normalPrecompute.setShader(npShader) def _attachUpdateTask(self): """ Attaches the update tasks to the showbase """ self.showbase.addTask(self._preRenderCallback, "RP_BeforeRender", sort=-5000) self.showbase.addTask(self._update, "RP_Update", sort=-10) if self.haveLightingPass: self.showbase.addTask(self._updateLights, "RP_UpdateLights", sort=-9) self.showbase.addTask(self._updateShadows, "RP_UpdateShadows", sort=-8) self.showbase.addTask(self._processShadowCallbacks, "RP_ShadowCallbacks", sort=-5) if self.settings.displayOnscreenDebugger: self.showbase.addTask(self._updateGUI, "RP_UpdateGUI", sort=7) self.showbase.addTask(self._postRenderCallback, "RP_AfterRender", sort=5000) def _preRenderCallback(self, task=None): """ Called before rendering """ if self.settings.enableGlobalIllumination: self.globalIllum.process() self.antialias.preRenderUpdate() if task is not None: return task.cont def _postRenderCallback(self, task=None): """ Called after rendering """ self.antialias.postRenderUpdate() if task is not None: return task.cont def _computeCameraBounds(self): """ Computes the current camera bounds, i.e. for light culling """ cameraBounds = self.camera.node().getLens().makeBounds() cameraBounds.xform(self.camera.getMat(self.showbase.render)) return cameraBounds def _updateLights(self, task=None): """ Task which updates/culls the lights """ self.lightManager.updateLights() if task is not None: return task.cont def _processShadowCallbacks(self, task=None): self.lightManager.processCallbacks() if task is not None: return task.cont def _updateShadows(self, task=None): """ Task which updates the shadow maps """ self.lightManager.updateShadows() if task is not None: return task.cont def _updateGUI(self, task=None): """ Task which updates the onscreen gui debugger """ self.guiManager.update() if task is not None: return task.cont def _update(self, task=None): """ Main update task """ self.currentShiftIndex[0] = 1 - self.currentShiftIndex[0] currentFPS = 1.0 / self.showbase.taskMgr.globalClock.getDt() self.temporalProjXOffs[0] = 1 - self.temporalProjXOffs[0] self.cameraPosition[0] = self.showbase.cam.getPos(self.showbase.render) self.motionBlurFactor[0] = min( 1.5, currentFPS / 60.0) * self.settings.motionBlurFactor self.cullBounds = self._computeCameraBounds() if self.haveLightingPass: self.lightManager.setCullBounds(self.cullBounds) self.lastMVP[0] = self.currentMVP[0] self.currentMVP[0] = self._computeMVP() shift = self.pixelShifts[self.currentShiftIndex[0]] self.lastPixelShift[0] = self.currentPixelShift[0] self.currentPixelShift[0] = shift Globals.base.camLens.setFilmOffset(shift.x, shift.y) if task is not None: return task.cont def _computeMVP(self): """ Computes the current mvp. Actually, this is the worldViewProjectionMatrix, but for convience it's called mvp. """ camLens = self.showbase.camLens projMat = Mat4.convertMat( CSYupRight, camLens.getCoordinateSystem()) * camLens.getProjectionMat() transformMat = TransformState.makeMat( Mat4.convertMat( self.showbase.win.getGsg().getInternalCoordinateSystem(), CSZupRight)) modelViewMat = transformMat.invertCompose( self.showbase.render.getTransform(self.showbase.cam)).getMat() return UnalignedLMatrix4f(modelViewMat * projMat) def getLightManager(self): """ Returns a handle to the light manager """ return self.lightManager def getDefaultObjectShader(self, tesselated=False): """ Returns the default shader for objects """ if not tesselated: shader = BetterShader.load( "Shader/DefaultObjectShader/vertex.glsl", "Shader/DefaultObjectShader/fragment.glsl") else: self.warn("Tesselation is only experimental! Remember " "to convert the geometry to patches first!") shader = BetterShader.load( "Shader/DefaultObjectShader/vertex.glsl", "Shader/DefaultObjectShader/fragment.glsl", "", "Shader/DefaultObjectShader/tesscontrol.glsl", "Shader/DefaultObjectShader/tesseval.glsl") return shader def _getDeferredBuffer(self): """ Returns a handle to the internal deferred target """ return self.deferredTarget.getInternalBuffer() def addLight(self, light): """ Adds a light to the list of rendered lights """ if self.haveLightingPass: self.lightManager.addLight(light) else: self.warn("Lighting is disabled, so addLight has no effect") def setScattering(self, scatteringModel): """ Sets a scattering model to use. Only has an effect if enableScattering is enabled """ self.debug("Loading scattering model ..") if not self.settings.enableScattering: self.error("You cannot set a scattering model as scattering is not" " enabled in your pipeline.ini!") return self.lightingComputeContainer.setShaderInput( "transmittanceSampler", scatteringModel.getTransmittanceResult()) self.lightingComputeContainer.setShaderInput( "inscatterSampler", scatteringModel.getInscatterTexture()) scatteringModel.bindTo(self.lightingComputeContainer, "scatteringOptions") def enableDefaultEarthScattering(self): """ Adds a standard scattering model, representing the atmosphere of the earth. This is a shortcut for creating a Scattering instance and precomputing it """ earthScattering = Scattering() scale = 1000000000 earthScattering.setSettings({ "atmosphereOffset": Vec3(0, 0, -(6360.0 + 9.5) * scale), "atmosphereScale": Vec3(scale) }) earthScattering.precompute() self.setScattering(earthScattering) def setGILightSource(self, light): """ Sets the light source for the global illumination. The GI uses this light to shade the voxels, so this light is the only light which "casts" global illumination. When GI is disabled, this has no effect """ if self.settings.enableGlobalIllumination: self.globalIllum.setTargetLight(light) def _generateShaderConfiguration(self): """ Genrates the global shader include which defines most values used in the shaders. """ self.debug("(Re)Generating shader configuration") # Generate list of defines defines = [] if self.settings.antialiasingTechnique == "SMAA": quality = self.settings.smaaQuality.upper() if quality in ["LOW", "MEDIUM", "HIGH", "ULTRA"]: defines.append(("SMAA_PRESET_" + quality, "")) else: self.error("Unrecognized SMAA quality:", quality) return defines.append( ("LIGHTING_COMPUTE_PATCH_SIZE_X", self.settings.computePatchSizeX)) defines.append( ("LIGHTING_COMPUTE_PATCH_SIZE_Y", self.settings.computePatchSizeY)) defines.append(("LIGHTING_MIN_MAX_DEPTH_ACCURACY", self.settings.minMaxDepthAccuracy)) if self.blurEnabled: defines.append(("USE_DOF", 1)) if self.settings.useSimpleLighting: defines.append(("USE_SIMPLE_LIGHTING", 1)) if self.settings.anyLightBoundCheck: defines.append(("LIGHTING_ANY_BOUND_CHECK", 1)) if self.settings.accurateLightBoundCheck: defines.append(("LIGHTING_ACCURATE_BOUND_CHECK", 1)) if self.settings.renderShadows: defines.append(("USE_SHADOWS", 1)) defines.append( ("AMBIENT_CUBEMAP_SAMPLES", self.settings.ambientCubemapSamples)) defines.append( ("SHADOW_MAP_ATLAS_SIZE", self.settings.shadowAtlasSize)) defines.append(("SHADOW_MAX_UPDATES_PER_FRAME", self.settings.maxShadowUpdatesPerFrame)) defines.append(("SHADOW_GEOMETRY_MAX_VERTICES", self.settings.maxShadowUpdatesPerFrame * 3)) defines.append(("SHADOW_NUM_PCF_SAMPLES", self.settings.numPCFSamples)) defines.append(("SHADOW_NUM_PCSS_SEARCH_SAMPLES", self.settings.numPCSSSearchSamples)) defines.append(("SHADOW_NUM_PCSS_FILTER_SAMPLES", self.settings.numPCSSFilterSamples)) defines.append(("SHADOW_PSSM_BORDER_PERCENTAGE", self.settings.shadowCascadeBorderPercentage)) if self.settings.useHardwarePCF: defines.append(("USE_HARDWARE_PCF", 1)) defines.append(("WINDOW_WIDTH", self.size.x)) defines.append(("WINDOW_HEIGHT", self.size.y)) if self.settings.motionBlurEnabled: defines.append(("USE_MOTION_BLUR", 1)) defines.append( ("MOTION_BLUR_SAMPLES", self.settings.motionBlurSamples)) # Occlusion defines.append( ("OCCLUSION_TECHNIQUE_" + self.occlusion.getIncludeName(), 1)) defines.append(("OCCLUSION_RADIUS", self.settings.occlusionRadius)) defines.append(("OCCLUSION_STRENGTH", self.settings.occlusionStrength)) defines.append( ("OCCLUSION_SAMPLES", self.settings.occlusionSampleCount)) if self.settings.displayOnscreenDebugger: defines.append(("DEBUGGER_ACTIVE", 1)) extraSettings = self.guiManager.getDefines() defines += extraSettings if self.settings.enableTemporalReprojection: defines.append(("USE_TEMPORAL_REPROJECTION", 1)) if self.settings.enableGlobalIllumination: defines.append(("USE_GLOBAL_ILLUMINATION", 1)) if self.settings.enableScattering: defines.append(("USE_SCATTERING", 1)) # Pass near far defines.append(("CAMERA_NEAR", Globals.base.camLens.getNear())) defines.append(("CAMERA_FAR", Globals.base.camLens.getFar())) # Generate output = "// Autogenerated by RenderingPipeline.py\n" output += "// Do not edit! Your changes will be lost.\n\n" for key, value in defines: output += "#define " + key + " " + str(value) + "\n" # Try to write the file try: with open("PipelineTemp/ShaderAutoConfig.include", "w") as handle: handle.write(output) except Exception, msg: self.fatal( "Error writing shader autoconfig. Maybe no write-access?") return
class LightManager(DebugObject): """ This class is internally used by the RenderingPipeline to handle Lights and their Shadows. It stores a list of lights, and updates the required ShadowSources per frame. There are two main update methods: updateLights processes each light and does a basic frustum check. If the light is in the frustum, its ID is passed to the light precompute container (set with setLightingCuller). Also, each shadowSource of the light is checked, and if it reports to be invalid, it's queued to the list of queued shadow updates. updateShadows processes the queued shadow updates and setups everything to render the shadow depth textures to the shadow atlas. Lights can be added with addLight. Notice you cannot change the shadow resolution or even wether the light casts shadows after you called addLight. This is because it might already have a position in the atlas, and so the atlas would have to delete it's map, which is not supported (yet). This shouldn't be an issue, as you usually always know before if a light will cast shadows or not. """ def __init__(self, pipeline): """ Creates a new LightManager. It expects a RenderPipeline as parameter. """ DebugObject.__init__(self, "LightManager") self._initArrays() self.pipeline = pipeline self.settings = pipeline.getSettings() # Create arrays to store lights & shadow sources self.lights = [] self.shadowSources = [] self.queuedShadowUpdates = [] self.allLightsArray = ShaderStructArray(Light, self.maxTotalLights) self.updateCallbacks = [] self.cullBounds = None self.shadowScene = Globals.render # Create atlas self.shadowAtlas = ShadowAtlas() self.shadowAtlas.setSize(self.settings.shadowAtlasSize) self.shadowAtlas.create() self.maxShadowMaps = 24 self.maxShadowUpdatesPerFrame = self.settings.maxShadowUpdatesPerFrame self.numShadowUpdatesPTA = PTAInt.emptyArray(1) self.updateShadowsArray = ShaderStructArray( ShadowSource, self.maxShadowUpdatesPerFrame) self.allShadowsArray = ShaderStructArray( ShadowSource, self.maxShadowMaps) # Create shadow compute buffer self._createShadowComputationBuffer() # Create the initial shadow state self.shadowComputeCamera.setTagStateKey("ShadowPassShader") # self.shadowComputeCamera.setInitialState(RenderState.make( # ColorWriteAttrib.make(ColorWriteAttrib.C_off), # ColorWriteAttrib.make(ColorWriteAttrib.C_rgb), # DepthWriteAttrib.make(DepthWriteAttrib.M_on), # CullFaceAttrib.make(CullFaceAttrib.MCullNone), # 100)) self._createTagStates() self.shadowScene.setTag("ShadowPassShader", "Default") # Create debug overlay self._createDebugTexts() # Disable buffer on start self.shadowComputeTarget.setActive(False) # Bind arrays self.updateShadowsArray.bindTo(self.shadowScene, "updateSources") self.updateShadowsArray.bindTo( self.shadowComputeTarget, "updateSources") # Set initial inputs for target in [self.shadowComputeTarget, self.shadowScene]: target.setShaderInput("numUpdates", self.numShadowUpdatesPTA) self.lightingComputator = None self.lightCuller = None self.skip = 0 self.skipRate = 0 def _createTagStates(self): # Create shadow caster shader self.shadowCasterShader = BetterShader.load( "Shader/DefaultShadowCaster/vertex.glsl", "Shader/DefaultShadowCaster/fragment.glsl", "Shader/DefaultShadowCaster/geometry.glsl") initialState = NodePath("ShadowCasterState") initialState.setShader(self.shadowCasterShader, 30) self.shadowComputeCamera.setTagState( "Default", initialState.getState()) def _createShadowComputationBuffer(self): """ This creates the internal shadow buffer which also is the shadow atlas. Shadow maps are rendered to this using Viewports (thank you rdb for adding this!). It also setups the base camera which renders the shadow objects, although a custom mvp is passed to the shaders, so the camera is mainly a dummy """ # Create camera showing the whole scene self.shadowComputeCamera = Camera("ShadowComputeCamera") self.shadowComputeCameraNode = self.shadowScene.attachNewNode( self.shadowComputeCamera) self.shadowComputeCamera.getLens().setFov(90, 90) self.shadowComputeCamera.getLens().setNearFar(10.0, 100000.0) # Disable culling self.shadowComputeCamera.setBounds(OmniBoundingVolume()) self.shadowComputeCameraNode.setPos(0, 0, 150) self.shadowComputeCameraNode.lookAt(0, 0, 0) self.shadowComputeTarget = RenderTarget("ShadowAtlas") self.shadowComputeTarget.setSize(self.shadowAtlas.getSize()) self.shadowComputeTarget.addDepthTexture() self.shadowComputeTarget.setDepthBits(32) if self.settings.enableGlobalIllumination: self.shadowComputeTarget.addColorTexture() self.shadowComputeTarget.setColorBits(16) self.shadowComputeTarget.setSource( self.shadowComputeCameraNode, Globals.base.win) self.shadowComputeTarget.prepareSceneRender() # This took me a long time to figure out. If not removing the quad # children, the color and aux buffers will be overridden each frame. # Quite annoying! self.shadowComputeTarget.getQuad().node().removeAllChildren() self.shadowComputeTarget.getInternalRegion().setSort(-200) self.shadowComputeTarget.getInternalRegion().setNumRegions( self.maxShadowUpdatesPerFrame + 1) self.shadowComputeTarget.getInternalRegion().setDimensions(0, (0, 0, 0, 0)) self.shadowComputeTarget.getInternalBuffer().setSort(-300) # We can't clear the depth per viewport. # But we need to clear it in any way, as we still want # z-testing in the buffers. So well, we create a # display region *below* (smaller sort value) each viewport # which has a depth-clear assigned. This is hacky, I know. self.depthClearer = [] for i in range(self.maxShadowUpdatesPerFrame): buff = self.shadowComputeTarget.getInternalBuffer() dr = buff.makeDisplayRegion() dr.setSort(-250) for k in xrange(16): dr.setClearActive(k, True) dr.setClearValue(k, Vec4(0.5,0.5,0.5,1)) dr.setClearDepthActive(True) dr.setClearDepth(1.0) dr.setDimensions(0,0,0,0) dr.setActive(False) self.depthClearer.append(dr) # When using hardware pcf, set the correct filter types dTex = self.shadowComputeTarget.getDepthTexture() if self.settings.useHardwarePCF: dTex.setMinfilter(Texture.FTShadow) dTex.setMagfilter(Texture.FTShadow) dTex.setWrapU(Texture.WMClamp) dTex.setWrapV(Texture.WMClamp) def getAllLights(self): """ Returns all attached lights """ return self.lights def _createDebugTexts(self): """ Creates a debug overlay if specified in the pipeline settings """ self.lightsVisibleDebugText = None self.lightsUpdatedDebugText = None if self.settings.displayDebugStats: try: from Code.GUI.FastText import FastText self.lightsVisibleDebugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.84), rightAligned=True, color=Vec3(1, 1, 0), size=0.036) self.lightsUpdatedDebugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.8), rightAligned=True, color=Vec3(1, 1, 0), size=0.036) except Exception, msg: self.debug( "Overlay is disabled because FastText wasn't loaded")
class LightManager(DebugObject): """ This class is internally used by the RenderingPipeline to handle Lights and their Shadows. It stores a list of lights, and updates the required ShadowSources per frame. There are two main update methods: updateLights processes each light and does a basic frustum check. If the light is in the frustum, its ID is passed to the light precompute container (set with setLightingCuller). Also, each shadowSource of the light is checked, and if it reports to be invalid, it's queued to the list of queued shadow updates. updateShadows processes the queued shadow updates and setups everything to render the shadow depth textures to the shadow atlas. Lights can be added with addLight. Notice you cannot change the shadow resolution or wether the light casts shadows after you called addLight. This is because it might already have a position in the atlas, and so the atlas would have to delete it's map, which is not supported (yet). This shouldn't be an issue, as you usually always know before if a light will cast shadows or not. """ def __init__(self, pipeline): """ Creates a new LightManager. It expects a RenderPipeline as parameter. """ DebugObject.__init__(self, "LightManager") self._initArrays() self.pipeline = pipeline self.settings = pipeline.getSettings() # Create arrays to store lights & shadow sources self.lights = [] self.shadowSources = [] self.queuedShadowUpdates = [] self.allLightsArray = ShaderStructArray(Light, self.maxTotalLights) self.updateCallbacks = [] self.cullBounds = None self.shadowScene = Globals.render # Create atlas self.shadowAtlas = ShadowAtlas() self.shadowAtlas.setSize(self.settings.shadowAtlasSize) self.shadowAtlas.create() self.maxShadowMaps = 24 self.maxShadowUpdatesPerFrame = self.settings.maxShadowUpdatesPerFrame self.numShadowUpdatesPTA = PTAInt.emptyArray(1) self.updateShadowsArray = ShaderStructArray( ShadowSource, self.maxShadowUpdatesPerFrame) self.allShadowsArray = ShaderStructArray( ShadowSource, self.maxShadowMaps) # Create shadow compute buffer self._createShadowComputationBuffer() # Create the initial shadow state self.shadowComputeCamera.setTagStateKey("ShadowPassShader") self._createTagStates() self.shadowScene.setTag("ShadowPassShader", "Default") # Create debug overlay self._createDebugTexts() # Disable buffer on start self.shadowComputeTarget.setActive(False) # Bind arrays self.updateShadowsArray.bindTo(self.shadowScene, "updateSources") self.updateShadowsArray.bindTo( self.shadowComputeTarget, "updateSources") # Set initial inputs for target in [self.shadowComputeTarget, self.shadowScene]: target.setShaderInput("numUpdates", self.numShadowUpdatesPTA) self.lightingComputator = None self.lightCuller = None self.skip = 0 self.skipRate = 0 def _createTagStates(self): # Create shadow caster shader self.shadowCasterShader = BetterShader.load( "Shader/DefaultShadowCaster/vertex.glsl", "Shader/DefaultShadowCaster/fragment.glsl", "Shader/DefaultShadowCaster/geometry.glsl") initialState = NodePath("ShadowCasterState") initialState.setShader(self.shadowCasterShader, 30) # initialState.setAttrib(CullFaceAttrib.make(CullFaceAttrib.MCullNone)) initialState.setAttrib(ColorWriteAttrib.make(ColorWriteAttrib.COff)) self.shadowComputeCamera.setTagState( "Default", initialState.getState()) def _createShadowComputationBuffer(self): """ This creates the internal shadow buffer which also is the shadow atlas. Shadow maps are rendered to this using Viewports (thank you rdb for adding this!). It also setups the base camera which renders the shadow objects, although a custom mvp is passed to the shaders, so the camera is mainly a dummy """ # Create camera showing the whole scene self.shadowComputeCamera = Camera("ShadowComputeCamera") self.shadowComputeCameraNode = self.shadowScene.attachNewNode( self.shadowComputeCamera) self.shadowComputeCamera.getLens().setFov(30, 30) self.shadowComputeCamera.getLens().setNearFar(1.0, 2.0) # Disable culling self.shadowComputeCamera.setBounds(OmniBoundingVolume()) self.shadowComputeCamera.setCullBounds(OmniBoundingVolume()) self.shadowComputeCamera.setFinal(True) self.shadowComputeCameraNode.setPos(0, 0, 1500) self.shadowComputeCameraNode.lookAt(0, 0, 0) self.shadowComputeTarget = RenderTarget("ShadowAtlas") self.shadowComputeTarget.setSize(self.shadowAtlas.getSize()) self.shadowComputeTarget.addDepthTexture() self.shadowComputeTarget.setDepthBits(32) self.shadowComputeTarget.setSource( self.shadowComputeCameraNode, Globals.base.win) self.shadowComputeTarget.prepareSceneRender() # This took me a long time to figure out. If not removing the quad # children, the color and aux buffers will be overridden each frame. # Quite annoying! self.shadowComputeTarget.getQuad().node().removeAllChildren() self.shadowComputeTarget.getInternalRegion().setSort(-200) self.shadowComputeTarget.getInternalRegion().setNumRegions( self.maxShadowUpdatesPerFrame + 1) self.shadowComputeTarget.getInternalRegion().setDimensions(0, (0, 0, 0, 0)) self.shadowComputeTarget.getInternalRegion().disableClears() self.shadowComputeTarget.getInternalBuffer().disableClears() self.shadowComputeTarget.getInternalBuffer().setSort(-300) # We can't clear the depth per viewport. # But we need to clear it in any way, as we still want # z-testing in the buffers. So well, we create a # display region *below* (smaller sort value) each viewport # which has a depth-clear assigned. This is hacky, I know. self.depthClearer = [] for i in range(self.maxShadowUpdatesPerFrame): buff = self.shadowComputeTarget.getInternalBuffer() dr = buff.makeDisplayRegion() dr.setSort(-250) for k in xrange(16): dr.setClearActive(k, True) dr.setClearValue(k, Vec4(0.5,0.5,0.5,1)) dr.setClearDepthActive(True) dr.setClearDepth(1.0) dr.setDimensions(0,0,0,0) dr.setActive(False) self.depthClearer.append(dr) # When using hardware pcf, set the correct filter types if self.settings.useHardwarePCF: self.pcfSampleState = SamplerState() self.pcfSampleState.setMinfilter(SamplerState.FTShadow) self.pcfSampleState.setMagfilter(SamplerState.FTShadow) self.pcfSampleState.setWrapU(SamplerState.WMClamp) self.pcfSampleState.setWrapV(SamplerState.WMClamp) dTex = self.getAtlasTex() dTex.setWrapU(Texture.WMClamp) dTex.setWrapV(Texture.WMClamp) def getAllLights(self): """ Returns all attached lights """ return self.lights def getPCFSampleState(self): """ Returns the pcf sample state used to sample the shadow map """ return self.pcfSampleState def processCallbacks(self): """ Processes all updates from the previous frame """ for update in self.updateCallbacks: update.onUpdated() self.updateCallbacks = [] def _createDebugTexts(self): """ Creates a debug overlay if specified in the pipeline settings """ self.lightsVisibleDebugText = None self.lightsUpdatedDebugText = None if self.settings.displayDebugStats: try: from Code.GUI.FastText import FastText self.lightsVisibleDebugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.84), rightAligned=True, color=Vec3(1, 1, 0), size=0.036) self.lightsUpdatedDebugText = FastText(pos=Vec2( Globals.base.getAspectRatio() - 0.1, 0.8), rightAligned=True, color=Vec3(1, 1, 0), size=0.036) except Exception, msg: self.debug( "Overlay is disabled because FastText wasn't loaded")
class LightManager(DebugObject): def __init__(self): DebugObject.__init__(self, "LightManager") self._initArrays() # create arrays to store lights & shadow sources self.lights = [] self.shadowSources = [] self.allLightsArray = ShaderStructArray(Light, 30) self.cullBounds = None self.shadowScene = render ## SHADOW ATLAS ## # todo: move to separate class # When you change this, change also SHADOW_MAP_ATLAS_SIZE in configuration.include, # and reduce the default shadow map resolution of point lights self.shadowAtlasSize = 512 self.maxShadowMaps = 24 # When you change it , change also SHAODOW_GEOMETRY_MAX_VERTICES and # SHADOW_MAX_UPDATES_PER_FRAME in configuration.include! self.maxShadowUpdatesPerFrame = 2 self.tileSize = 128 self.tileCount = self.shadowAtlasSize / self.tileSize self.tiles = [] self.updateShadowsArray = ShaderStructArray( ShadowSource, self.maxShadowUpdatesPerFrame) self.allShadowsArray = ShaderStructArray(ShadowSource, self.maxShadowMaps) # self.shadowAtlasTex = Texture("ShadowAtlas") # self.shadowAtlasTex.setup2dTexture( # self.shadowAtlasSize, self.shadowAtlasSize, Texture.TFloat, Texture.FRg16) # self.shadowAtlasTex.setMinfilter(Texture.FTLinear) # self.shadowAtlasTex.setMagfilter(Texture.FTLinear) self.debug("Init shadow atlas with tileSize =", self.tileSize, ", tileCount =", self.tileCount) for i in xrange(self.tileCount): self.tiles.append([None for j in xrange(self.tileCount)]) # create shadow compute buffer self.shadowComputeCamera = Camera("ShadowComputeCamera") self.shadowComputeCameraNode = self.shadowScene.attachNewNode( self.shadowComputeCamera) self.shadowComputeCamera.getLens().setFov(90, 90) self.shadowComputeCamera.getLens().setNearFar(10.0, 100000.0) self.shadowComputeCameraNode.setPos(0, 0, 150) self.shadowComputeCameraNode.lookAt(0, 0, 0) self.shadowComputeTarget = RenderTarget("ShadowCompute") self.shadowComputeTarget.setSize(self.shadowAtlasSize, self.shadowAtlasSize) # self.shadowComputeTarget.setLayers(self.maxShadowUpdatesPerFrame) self.shadowComputeTarget.addRenderTexture(RenderTargetType.Depth) self.shadowComputeTarget.setDepthBits(32) self.shadowComputeTarget.setSource(self.shadowComputeCameraNode, base.win) self.shadowComputeTarget.prepareSceneRender() self.shadowComputeTarget.getInternalRegion().setSort(3) self.shadowComputeTarget.getRegion().setSort(3) self.shadowComputeTarget.getInternalRegion().setNumRegions( self.maxShadowUpdatesPerFrame + 1) self.shadowComputeTarget.getInternalRegion().setDimensions( 0, (0, 1, 0, 1)) self.shadowComputeTarget.setClearDepth(False) self.depthClearer = [] for i in xrange(self.maxShadowUpdatesPerFrame): buff = self.shadowComputeTarget.getInternalBuffer() dr = buff.makeDisplayRegion() dr.setSort(2) dr.setClearDepthActive(True) dr.setClearDepth(1.0) dr.setClearColorActive(False) dr.setDimensions(0, 0, 0, 0) self.depthClearer.append(dr) self.queuedShadowUpdates = [] # Assign copy shader self._setCopyShader() # self.shadowComputeTarget.setShaderInput("atlas", self.shadowComputeTarget.getColorTexture()) # self.shadowComputeTarget.setShaderInput( # "renderResult", self.shadowComputeTarget.getDepthTexture()) # self.shadowComputeTarget.setActive(False) # Create shadow caster shader self.shadowCasterShader = BetterShader.load( "Shader/DefaultShadowCaster.vertex", "Shader/DefaultShadowCaster.fragment", "Shader/DefaultShadowCaster.geometry") self.shadowComputeCamera.setTagStateKey("ShadowPass") initialState = NodePath("ShadowCasterState") initialState.setShader(self.shadowCasterShader, 30) self.shadowComputeCamera.setInitialState( RenderState.make(ColorWriteAttrib.make(ColorWriteAttrib.C_off), DepthWriteAttrib.make(DepthWriteAttrib.M_on), 100)) self.shadowComputeCamera.setTagState("True", initialState.getState()) self.shadowScene.setTag("ShadowPass", "True") self._createDebugTexts() self.updateShadowsArray.bindTo(self.shadowScene, "updateSources") self.updateShadowsArray.bindTo(self.shadowComputeTarget, "updateSources") self.numShadowUpdatesPTA = PTAInt.emptyArray(1) # Set initial inputs for target in [self.shadowComputeTarget, self.shadowScene]: target.setShaderInput("numUpdates", self.numShadowUpdatesPTA) self.lightingComputator = None self.lightCuller = None # Tries to create debug text to show how many lights are currently visible # / rendered def _createDebugTexts(self): try: from FastText import FastText self.lightsVisibleDebugText = FastText(pos=Vec2( base.getAspectRatio() - 0.1, 0.84), rightAligned=True, color=Vec3(1, 0, 0), size=0.036) self.lightsUpdatedDebugText = FastText(pos=Vec2( base.getAspectRatio() - 0.1, 0.8), rightAligned=True, color=Vec3(1, 0, 0), size=0.036) except Exception, msg: self.debug("Could not load fast text:", msg) self.debug("Overlay is disabled because FastText wasn't loaded") self.lightsVisibleDebugText = None self.lightsUpdatedDebugText = None
class GlobalIllumination(DebugObject): """ This class handles the global illumination processing. It is still experimental, and thus not commented. """ updateEnabled = False def __init__(self, pipeline): DebugObject.__init__(self, "GlobalIllumnination") self.pipeline = pipeline self.targetCamera = Globals.base.cam self.targetSpace = Globals.base.render self.voxelBaseResolution = 512 * 4 self.voxelGridSizeWS = Vec3(50, 50, 20) self.voxelGridResolution = LVecBase3i(512, 512, 128) self.targetLight = None self.helperLight = None self.ptaGridPos = PTALVecBase3f.emptyArray(1) self.gridPos = Vec3(0) @classmethod def setUpdateEnabled(self, enabled): self.updateEnabled = enabled def setTargetLight(self, light): """ Sets the sun light which is the main source of GI """ if light._getLightType() != LightType.Directional: self.error("setTargetLight expects a directional light!") return self.targetLight = light self._createHelperLight() def _prepareVoxelScene(self): """ Creates the internal buffer to voxelize the scene on the fly """ self.voxelizeScene = Globals.render self.voxelizeCamera = Camera("VoxelizeScene") self.voxelizeCameraNode = self.voxelizeScene.attachNewNode( self.voxelizeCamera) self.voxelizeLens = OrthographicLens() self.voxelizeLens.setFilmSize(self.voxelGridSizeWS.x * 2, self.voxelGridSizeWS.y * 2) self.voxelizeLens.setNearFar(0.0, self.voxelGridSizeWS.x * 2) self.voxelizeCamera.setLens(self.voxelizeLens) self.voxelizeCamera.setTagStateKey("VoxelizePassShader") self.targetSpace.setTag("VoxelizePassShader", "Default") self.voxelizeCameraNode.setPos(0, 0, 0) self.voxelizeCameraNode.lookAt(0, 0, 0) self.voxelizeTarget = RenderTarget("DynamicVoxelization") self.voxelizeTarget.setSize(self.voxelBaseResolution) # self.voxelizeTarget.addDepthTexture() # self.voxelizeTarget.addColorTexture() # self.voxelizeTarget.setColorBits(16) self.voxelizeTarget.setSource(self.voxelizeCameraNode, Globals.base.win) self.voxelizeTarget.prepareSceneRender() self.voxelizeTarget.getQuad().node().removeAllChildren() self.voxelizeTarget.getInternalRegion().setSort(-400) self.voxelizeTarget.getInternalBuffer().setSort(-399) # for tex in [self.voxelizeTarget.getColorTexture()]: # tex.setWrapU(Texture.WMClamp) # tex.setWrapV(Texture.WMClamp) # tex.setMinfilter(Texture.FTNearest) # tex.setMagfilter(Texture.FTNearest) voxelSize = Vec3( self.voxelGridSizeWS.x * 2.0 / self.voxelGridResolution.x, self.voxelGridSizeWS.y * 2.0 / self.voxelGridResolution.y, self.voxelGridSizeWS.z * 2.0 / self.voxelGridResolution.z) self.targetSpace.setShaderInput("dv_gridSize", self.voxelGridSizeWS * 2) self.targetSpace.setShaderInput("dv_voxelSize", voxelSize) self.targetSpace.setShaderInput("dv_gridResolution", self.voxelGridResolution) def _createVoxelizeState(self): """ Creates the tag state and loades the voxelizer shader """ self.voxelizeShader = BetterShader.load("Shader/GI/Voxelize.vertex", "Shader/GI/Voxelize.fragment" # "Shader/GI/Voxelize.geometry" ) initialState = NodePath("VoxelizerState") initialState.setShader(self.voxelizeShader, 50) initialState.setAttrib(CullFaceAttrib.make(CullFaceAttrib.MCullNone)) initialState.setDepthWrite(False) initialState.setDepthTest(False) initialState.setAttrib(DepthTestAttrib.make(DepthTestAttrib.MNone)) initialState.setShaderInput("dv_dest_tex", self.voxelGenTex) self.voxelizeCamera.setTagState("Default", initialState.getState()) 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 """ 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( "dv_uv_size", float(self.helperLight.shadowResolution) / self.pipeline.settings.shadowAtlasSize) self.targetSpace.setShaderInput( "dv_atlas", self.pipeline.getLightManager().getAtlasTex()) self._updateGridPos() def setup(self): """ Setups everything for the GI to work """ # if self.pipeline.settings.useHardwarePCF: # self.fatal( # "Global Illumination does not work in combination with PCF!") # return self._prepareVoxelScene() # Create 3D Texture to store the voxel generation grid self.voxelGenTex = Texture("VoxelsTemp") self.voxelGenTex.setup3dTexture(self.voxelGridResolution.x, self.voxelGridResolution.y, self.voxelGridResolution.z, Texture.TInt, Texture.FR32i) self.voxelGenTex.setMinfilter(Texture.FTLinearMipmapLinear) self.voxelGenTex.setMagfilter(Texture.FTLinear) # Create 3D Texture which is a copy of the voxel generation grid but # stable, as the generation grid is updated part by part self.voxelStableTex = Texture("VoxelsStable") self.voxelStableTex.setup3dTexture(self.voxelGridResolution.x, self.voxelGridResolution.y, self.voxelGridResolution.z, Texture.TFloat, Texture.FRgba8) self.voxelStableTex.setMinfilter(Texture.FTLinearMipmapLinear) self.voxelStableTex.setMagfilter(Texture.FTLinear) for prepare in [self.voxelGenTex, self.voxelStableTex]: prepare.setMagfilter(Texture.FTLinear) prepare.setMinfilter(Texture.FTLinearMipmapLinear) prepare.setWrapU(Texture.WMBorderColor) prepare.setWrapV(Texture.WMBorderColor) prepare.setWrapW(Texture.WMBorderColor) prepare.setBorderColor(Vec4(0, 0, 0, 0)) self.voxelGenTex.setMinfilter(Texture.FTNearest) self.voxelGenTex.setMagfilter(Texture.FTNearest) self.voxelGenTex.setWrapU(Texture.WMClamp) self.voxelGenTex.setWrapV(Texture.WMClamp) self.voxelGenTex.setWrapW(Texture.WMClamp) # self.voxelStableTex.generateRamMipmapImages() self._createVoxelizeState() self.clearTextureNode = NodePath("ClearTexture") self.copyTextureNode = NodePath("CopyTexture") self.generateMipmapsNode = NodePath("GenerateMipmaps") self.convertGridNode = NodePath("ConvertGrid") self.reloadShader() def _generateMipmaps(self, tex): """ Generates all mipmaps for a 3D texture, using a gaussian function """ pstats_GenerateMipmaps.start() currentMipmap = 0 computeSize = LVecBase3i(self.voxelGridResolution) self.generateMipmapsNode.setShaderInput("source", tex) self.generateMipmapsNode.setShaderInput("pixelSize", 1.0 / computeSize.x) while computeSize.z > 1: computeSize /= 2 self.generateMipmapsNode.setShaderInput("sourceMipmap", LVecBase3i(currentMipmap)) self.generateMipmapsNode.setShaderInput("currentMipmapSize", LVecBase3i(computeSize)) self.generateMipmapsNode.setShaderInput("dest", tex, False, True, -1, currentMipmap + 1) self._executeShader(self.generateMipmapsNode, (computeSize.x + 7) / 8, (computeSize.y + 7) / 8, (computeSize.z + 7) / 8) currentMipmap += 1 pstats_GenerateMipmaps.stop() def _createCleanShader(self): shader = BetterShader.loadCompute("Shader/GI/ClearTexture.compute") self.clearTextureNode.setShader(shader) def _createConvertShader(self): shader = BetterShader.loadCompute("Shader/GI/ConvertGrid.compute") self.convertGridNode.setShader(shader) def _createGenerateMipmapsShader(self): shader = BetterShader.loadCompute("Shader/GI/GenerateMipmaps.compute") self.generateMipmapsNode.setShader(shader) def reloadShader(self): self._createCleanShader() self._createGenerateMipmapsShader() self._createConvertShader() self._createVoxelizeState() self.frameIndex = 0 def _clear3DTexture(self, tex, clearVal=None): """ Clears a 3D Texture to <clearVal> """ if clearVal is None: clearVal = Vec4(0) self.clearTextureNode.setShaderInput("target", tex, False, True, -1, 0) self.clearTextureNode.setShaderInput("clearValue", clearVal) self._executeShader(self.clearTextureNode, (tex.getXSize() + 7) / 8, (tex.getYSize() + 7) / 8, (tex.getZSize() + 7) / 8) def _updateGridPos(self): 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 process(self): if self.targetLight is None: self.fatal("The GI cannot work without a target light! Set one " "with setTargetLight() first!") if not self.updateEnabled: self.voxelizeTarget.setActive(False) return direction = self.targetLight.getDirection() # time.sleep(0.4) if self.frameIndex == 0: # Find out cam pos self.targetSpace.setShaderInput( "dv_uv_start", self.helperLight.shadowSources[0].getAtlasPos()) self.voxelizeTarget.setActive(True) # self.voxelizeTarget.setActive(False) self.voxelizeLens.setFilmSize(self.voxelGridSizeWS.y * 2, self.voxelGridSizeWS.z * 2) self.voxelizeLens.setNearFar(0.0, self.voxelGridSizeWS.x * 2) self.targetSpace.setShaderInput( "dv_mvp", Mat4(self.helperLight.shadowSources[0].mvp)) self.targetSpace.setShaderInput( "dv_gridStart", self.gridPos - self.voxelGridSizeWS) self.targetSpace.setShaderInput( "dv_gridEnd", self.gridPos + self.voxelGridSizeWS) self.targetSpace.setShaderInput("dv_lightdir", direction) # Clear textures self._clear3DTexture(self.voxelGenTex, Vec4(0, 0, 0, 0)) # Voxelize from x axis self.voxelizeCameraNode.setPos(self.gridPos - Vec3(self.voxelGridSizeWS.x, 0, 0)) self.voxelizeCameraNode.lookAt(self.gridPos) self.targetSpace.setShaderInput("dv_direction", LVecBase3i(0)) elif self.frameIndex == 1: # Voxelize from y axis # self.voxelizeTarget.setActive(False) self.voxelizeLens.setFilmSize(self.voxelGridSizeWS.x * 2, self.voxelGridSizeWS.z * 2) self.voxelizeLens.setNearFar(0.0, self.voxelGridSizeWS.y * 2) self.voxelizeCameraNode.setPos(self.gridPos - Vec3(0, self.voxelGridSizeWS.y, 0)) self.voxelizeCameraNode.lookAt(self.gridPos) self.targetSpace.setShaderInput("dv_direction", LVecBase3i(1)) elif self.frameIndex == 2: # self.voxelizeTarget.setActive(False) # Voxelize from z axis self.voxelizeLens.setFilmSize(self.voxelGridSizeWS.x * 2, self.voxelGridSizeWS.y * 2) self.voxelizeLens.setNearFar(0.0, self.voxelGridSizeWS.z * 2) self.voxelizeCameraNode.setPos(self.gridPos + Vec3(0, 0, self.voxelGridSizeWS.z)) self.voxelizeCameraNode.lookAt(self.gridPos) self.targetSpace.setShaderInput("dv_direction", LVecBase3i(2)) elif self.frameIndex == 3: self.voxelizeTarget.setActive(False) # Copy the cache to the actual texture self.convertGridNode.setShaderInput("src", self.voxelGenTex) self.convertGridNode.setShaderInput("dest", self.voxelStableTex) self._executeShader(self.convertGridNode, (self.voxelGridResolution.x + 7) / 8, (self.voxelGridResolution.y + 7) / 8, (self.voxelGridResolution.z + 7) / 8) # Generate the mipmaps self._generateMipmaps(self.voxelStableTex) self.helperLight.setPos(self.gridPos) self.helperLight.setDirection(direction) # We are done now, update the inputs self.ptaGridPos[0] = Vec3(self.gridPos) self._updateGridPos() self.frameIndex += 1 self.frameIndex = self.frameIndex % 5 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 _executeShader(self, node, threadsX, threadsY, threadsZ=1): """ Executes a compute shader, fetching the shader attribute from a NodePath """ sattr = node.getAttrib(ShaderAttrib) Globals.base.graphicsEngine.dispatchCompute( (threadsX, threadsY, threadsZ), sattr, Globals.base.win.get_gsg())