class GPUFFT(DebugObject): """ This is a collection of compute shaders to generate the inverse fft efficiently on the gpu, with butterfly FFT and precomputed weights """ def __init__(self, N, sourceTex, normalizationFactor): """ Creates a new fft instance. The source texture has to specified from the begining, as the shaderAttributes are pregenerated for performance reasons """ DebugObject.__init__(self, "GPU-FFT") self.size = N self.log2Size = int(math.log(N, 2)) self.normalizationFactor = normalizationFactor # Create a ping and a pong texture, because we can't write to the # same texture while reading to it (that would lead to unexpected # behaviour, we could solve that by using an appropriate thread size, # but it works fine so far) self.pingTexture = Texture("FFTPing") self.pingTexture.setup2dTexture( self.size, self.size, Texture.TFloat, Texture.FRgba32) self.pongTexture = Texture("FFTPong") self.pongTexture.setup2dTexture( self.size, self.size, Texture.TFloat, Texture.FRgba32) self.sourceTex = sourceTex for tex in [self.pingTexture, self.pongTexture, sourceTex]: tex.setMinfilter(Texture.FTNearest) tex.setMagfilter(Texture.FTNearest) tex.setWrapU(Texture.WMClamp) tex.setWrapV(Texture.WMClamp) # Pregenerate weights & indices for the shaders self._computeWeighting() # Pre generate the shaders, we have 2 passes: Horizontal and Vertical # which both execute log2(N) times with varying radii self.horizontalFFTShader = BetterShader.loadCompute( "Shader/Water/HorizontalFFT.compute") self.horizontalFFT = NodePath("HorizontalFFT") self.horizontalFFT.setShader(self.horizontalFFTShader) self.horizontalFFT.setShaderInput( "precomputedWeights", self.weightsLookupTex) self.horizontalFFT.setShaderInput("N", LVecBase2i(self.size)) self.verticalFFTShader = BetterShader.loadCompute( "Shader/Water/VerticalFFT.compute") self.verticalFFT = NodePath("VerticalFFT") self.verticalFFT.setShader(self.verticalFFTShader) self.verticalFFT.setShaderInput( "precomputedWeights", self.weightsLookupTex) self.verticalFFT.setShaderInput("N", LVecBase2i(self.size)) # Create a texture where the result is stored self.resultTexture = Texture("Result") self.resultTexture.setup2dTexture( self.size, self.size, Texture.TFloat, Texture.FRgba16) self.resultTexture.setMinfilter(Texture.FTLinear) self.resultTexture.setMagfilter(Texture.FTLinear) # Prepare the shader attributes, so we don't have to regenerate them # every frame -> That is VERY slow (3ms per fft instance) self._prepareAttributes() def getResultTexture(self): """ Returns the result texture, only contains valid data after execute was called at least once """ return self.resultTexture def _generateIndices(self, storageA, storageB): """ This method generates the precompute indices, see http://cnx.org/content/m12012/latest/image1.png """ numIter = self.size offset = 1 step = 0 for i in xrange(self.log2Size): numIter = numIter >> 1 step = offset for j in xrange(self.size): goLeft = (j / step) % 2 == 1 indexA, indexB = 0, 0 if goLeft: indexA, indexB = j - step, j else: indexA, indexB = j, j + step storageA[i][j] = indexA storageB[i][j] = indexB offset = offset << 1 def _generateWeights(self, storage): """ This method generates the precomputed weights """ # Using a custom pi variable should force the calculations to use # high precision (I hope so) pi = 3.141592653589793238462643383 numIter = self.size / 2 numK = 1 resolutionFloat = float(self.size) for i in xrange(self.log2Size): start = 0 end = 2 * numK for b in xrange(numIter): K = 0 for k in xrange(start, end, 2): fK = float(K) fNumIter = float(numIter) weightA = Vec2( math.cos(2.0 * pi * fK * fNumIter / resolutionFloat), -math.sin(2.0 * pi * fK * fNumIter / resolutionFloat)) weightB = Vec2( -math.cos(2.0 * pi * fK * fNumIter / resolutionFloat), math.sin(2.0 * pi * fK * fNumIter / resolutionFloat)) storage[i][k / 2] = weightA storage[i][k / 2 + numK] = weightB K += 1 start += 4 * numK end = start + 2 * numK numIter = numIter >> 1 numK = numK << 1 def _reverseRow(self, indices): """ Reverses the bits in the given row. This is required for inverse fft (actually we perform a normal fft, but reversing the bits gives us an inverse fft) """ mask = 0x1 for j in xrange(self.size): val = 0x0 temp = int(indices[j]) # Int is required, for making a copy for i in xrange(self.log2Size): t = mask & temp val = (val << 1) | t temp = temp >> 1 indices[j] = val def _computeWeighting(self): """ Precomputes the weights & indices, and stores them in a texture """ indicesA = [[0 for i in xrange(self.size)] for k in xrange(self.log2Size)] indicesB = [[0 for i in xrange(self.size)] for k in xrange(self.log2Size)] weights = [[Vec2(0.0) for i in xrange(self.size)] for k in xrange(self.log2Size)] self.debug("Pre-Generating indices ..") self._generateIndices(indicesA, indicesB) self._reverseRow(indicesA[0]) self._reverseRow(indicesB[0]) self.debug("Pre-Generating weights ..") self._generateWeights(weights) # Create storage for the weights & indices self.weightsLookup = PNMImage(self.size, self.log2Size, 4) self.weightsLookup.setMaxval((2 ** 16) - 1) self.weightsLookup.fill(0.0) # Populate storage for x in xrange(self.size): for y in xrange(self.log2Size): indexA = indicesA[y][x] indexB = indicesB[y][x] weight = weights[y][x] self.weightsLookup.setRed(x, y, indexA / float(self.size)) self.weightsLookup.setGreen(x, y, indexB / float(self.size)) self.weightsLookup.setBlue(x, y, weight.x * 0.5 + 0.5) self.weightsLookup.setAlpha(x, y, weight.y * 0.5 + 0.5) # Convert storage to texture so we can use it in a shader self.weightsLookupTex = Texture("Weights Lookup") self.weightsLookupTex.load(self.weightsLookup) self.weightsLookupTex.setFormat(Texture.FRgba16) self.weightsLookupTex.setMinfilter(Texture.FTNearest) self.weightsLookupTex.setMagfilter(Texture.FTNearest) self.weightsLookupTex.setWrapU(Texture.WMClamp) self.weightsLookupTex.setWrapV(Texture.WMClamp) def _prepareAttributes(self): """ Prepares all shaderAttributes, so that we have a list of ShaderAttributes we can simply walk through in the update method, that is MUCH faster than using setShaderInput, as each call to setShaderInput forces the generation of a new ShaderAttrib """ self.attributes = [] textures = [self.pingTexture, self.pongTexture] currentIndex = 0 firstPass = True # Horizontal for step in xrange(self.log2Size): source = textures[currentIndex] dest = textures[1 - currentIndex] if firstPass: source = self.sourceTex firstPass = False index = self.log2Size - step - 1 self.horizontalFFT.setShaderInput("source", source) self.horizontalFFT.setShaderInput("dest", dest) self.horizontalFFT.setShaderInput( "butterflyIndex", LVecBase2i(index)) self._queueShader(self.horizontalFFT) currentIndex = 1 - currentIndex # Vertical for step in xrange(self.log2Size): source = textures[currentIndex] dest = textures[1 - currentIndex] isLastPass = step == self.log2Size - 1 if isLastPass: dest = self.resultTexture index = self.log2Size - step - 1 self.verticalFFT.setShaderInput("source", source) self.verticalFFT.setShaderInput("dest", dest) self.verticalFFT.setShaderInput( "isLastPass", isLastPass) self.verticalFFT.setShaderInput( "normalizationFactor", self.normalizationFactor) self.verticalFFT.setShaderInput( "butterflyIndex", LVecBase2i(index)) self._queueShader(self.verticalFFT) currentIndex = 1 - currentIndex def execute(self): """ Executes the inverse fft once """ for attr in self.attributes: self._executeShader(attr) def _queueShader(self, node): """ Internal method to fetch the ShaderAttrib of a node and store it in the update queue """ sattr = node.getAttrib(ShaderAttrib) self.attributes.append(sattr) def _executeShader(self, sattr): """ Internal method to execute a shader by a given ShaderAttrib """ Globals.base.graphicsEngine.dispatch_compute( (self.size / 16, self.size / 16, 1), sattr, Globals.base.win.get_gsg())
class WaterManager(DebugObject): """ Simple wrapper arround WaterDisplacement which combines 3 displacement maps into one, and also generates a normal map """ def __init__(self): DebugObject.__init__(self, "WaterManager") self.options = OceanOptions() self.options.size = 512 self.options.windDir.normalize() self.options.waveAmplitude *= 1e-7 self.displacementTex = Texture("Displacement") self.displacementTex.setup2dTexture(self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) self.normalTex = Texture("Normal") self.normalTex.setup2dTexture(self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) self.combineShader = BetterShader.loadCompute( "Shader/Water/Combine.compute") self.ptaTime = PTAFloat.emptyArray(1) # Create a gaussian random texture, as shaders aren't well suited # for that setRandomSeed(523) self.randomStorage = PNMImage(self.options.size, self.options.size, 4) self.randomStorage.setMaxval((2**16) - 1) for x in xrange(self.options.size): for y in xrange(self.options.size): rand1 = self._getGaussianRandom() / 10.0 + 0.5 rand2 = self._getGaussianRandom() / 10.0 + 0.5 self.randomStorage.setXel(x, y, LVecBase3d(rand1, rand2, 0)) self.randomStorage.setAlpha(x, y, 1.0) self.randomStorageTex = Texture("RandomStorage") self.randomStorageTex.load(self.randomStorage) self.randomStorageTex.setFormat(Texture.FRgba16) self.randomStorageTex.setMinfilter(Texture.FTNearest) self.randomStorageTex.setMagfilter(Texture.FTNearest) # Create the texture wwhere the intial height (H0 + Omega0) is stored. self.texInitialHeight = Texture("InitialHeight") self.texInitialHeight.setup2dTexture(self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) self.texInitialHeight.setMinfilter(Texture.FTNearest) self.texInitialHeight.setMagfilter(Texture.FTNearest) # Create the shader which populates the initial height texture self.shaderInitialHeight = BetterShader.loadCompute( "Shader/Water/InitialHeight.compute") self.nodeInitialHeight = NodePath("initialHeight") self.nodeInitialHeight.setShader(self.shaderInitialHeight) self.nodeInitialHeight.setShaderInput("dest", self.texInitialHeight) self.nodeInitialHeight.setShaderInput("N", LVecBase2i(self.options.size)) self.nodeInitialHeight.setShaderInput("patchLength", self.options.patchLength) self.nodeInitialHeight.setShaderInput("windDir", self.options.windDir) self.nodeInitialHeight.setShaderInput("windSpeed", self.options.windSpeed) self.nodeInitialHeight.setShaderInput("waveAmplitude", self.options.waveAmplitude) self.nodeInitialHeight.setShaderInput("windDependency", self.options.windDependency) self.nodeInitialHeight.setShaderInput("randomTex", self.randomStorageTex) self.attrInitialHeight = self.nodeInitialHeight.getAttrib(ShaderAttrib) self.heightTextures = [] for i in xrange(3): tex = Texture("Height") tex.setup2dTexture(self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) tex.setMinfilter(Texture.FTNearest) tex.setMagfilter(Texture.FTNearest) tex.setWrapU(Texture.WMClamp) tex.setWrapV(Texture.WMClamp) self.heightTextures.append(tex) # Also create the shader which updates the spectrum self.shaderUpdate = BetterShader.loadCompute( "Shader/Water/Update.compute") self.nodeUpdate = NodePath("update") self.nodeUpdate.setShader(self.shaderUpdate) self.nodeUpdate.setShaderInput("outH0x", self.heightTextures[0]) self.nodeUpdate.setShaderInput("outH0y", self.heightTextures[1]) self.nodeUpdate.setShaderInput("outH0z", self.heightTextures[2]) self.nodeUpdate.setShaderInput("initialHeight", self.texInitialHeight) self.nodeUpdate.setShaderInput("N", LVecBase2i(self.options.size)) self.nodeUpdate.setShaderInput("time", self.ptaTime) self.attrUpdate = self.nodeUpdate.getAttrib(ShaderAttrib) # Create 3 FFTs self.fftX = GPUFFT(self.options.size, self.heightTextures[0], self.options.normalizationFactor) self.fftY = GPUFFT(self.options.size, self.heightTextures[1], self.options.normalizationFactor) self.fftZ = GPUFFT(self.options.size, self.heightTextures[2], self.options.normalizationFactor) self.combineNode = NodePath("Combine") self.combineNode.setShader(self.combineShader) self.combineNode.setShaderInput("displacementX", self.fftX.getResultTexture()) self.combineNode.setShaderInput("displacementY", self.fftY.getResultTexture()) self.combineNode.setShaderInput("displacementZ", self.fftZ.getResultTexture()) self.combineNode.setShaderInput("normalDest", self.normalTex) self.combineNode.setShaderInput("displacementDest", self.displacementTex) self.combineNode.setShaderInput("N", LVecBase2i(self.options.size)) self.combineNode.setShaderInput("choppyScale", self.options.choppyScale) self.combineNode.setShaderInput("gridLength", self.options.patchLength) # Store only the shader attrib as this is way faster self.attrCombine = self.combineNode.getAttrib(ShaderAttrib) def _getGaussianRandom(self): """ Returns a gaussian random number """ u1 = generateRandom() u2 = generateRandom() if u1 < 1e-6: u1 = 1e-6 return sqrt(-2 * log(u1)) * cos(2 * pi * u2) def setup(self): """ Setups the manager """ Globals.base.graphicsEngine.dispatch_compute( (self.options.size / 16, self.options.size / 16, 1), self.attrInitialHeight, Globals.base.win.get_gsg()) def getDisplacementTexture(self): """ Returns the displacement texture, storing the 3D Displacement in the RGB channels """ return self.displacementTex def getNormalTexture(self): """ Returns the normal texture, storing the normal in world space in the RGB channels """ return self.normalTex def update(self): """ Updates the displacement / normal map """ self.ptaTime[0] = 1 + \ Globals.clock.getFrameTime() * self.options.timeScale Globals.base.graphicsEngine.dispatch_compute( (self.options.size / 16, self.options.size / 16, 1), self.attrUpdate, Globals.base.win.get_gsg()) self.fftX.execute() self.fftY.execute() self.fftZ.execute() # Execute the shader which combines the 3 displacement maps into # 1 displacement texture and 1 normal texture. We could use dFdx in # the fragment shader, however that gives no accurate results as # dFdx returns the same value for a 2x2 pixel block Globals.base.graphicsEngine.dispatch_compute( (self.options.size / 16, self.options.size / 16, 1), self.attrCombine, Globals.base.win.get_gsg())
class WaterManager(DebugObject): """ Simple wrapper arround WaterDisplacement which combines 3 displacement maps into one, and also generates a normal map """ def __init__(self): DebugObject.__init__(self, "WaterManager") self.options = OceanOptions() self.options.size = 512 self.options.windDir.normalize() self.options.waveAmplitude *= 1e-7 self.displacementTex = Texture("Displacement") self.displacementTex.setup2dTexture( self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) self.normalTex = Texture("Normal") self.normalTex.setup2dTexture( self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) self.combineShader = Shader.loadCompute(Shader.SLGLSL, "Shader/WaterFFT/Combine.compute") self.ptaTime = PTAFloat.emptyArray(1) # Create a gaussian random texture, as shaders aren't well suited # for that setRandomSeed(523) self.randomStorage = PNMImage(self.options.size, self.options.size, 4) self.randomStorage.setMaxval((2 ** 16) - 1) for x in xrange(self.options.size): for y in xrange(self.options.size): rand1 = self._getGaussianRandom() / 10.0 + 0.5 rand2 = self._getGaussianRandom() / 10.0 + 0.5 self.randomStorage.setXel(x, y, float(rand1), float(rand2), 0) self.randomStorage.setAlpha(x, y, 1.0) self.randomStorageTex = Texture("RandomStorage") self.randomStorageTex.load(self.randomStorage) self.randomStorageTex.setFormat(Texture.FRgba16) self.randomStorageTex.setMinfilter(Texture.FTNearest) self.randomStorageTex.setMagfilter(Texture.FTNearest) # Create the texture wwhere the intial height (H0 + Omega0) is stored. self.texInitialHeight = Texture("InitialHeight") self.texInitialHeight.setup2dTexture( self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) self.texInitialHeight.setMinfilter(Texture.FTNearest) self.texInitialHeight.setMagfilter(Texture.FTNearest) # Create the shader which populates the initial height texture self.shaderInitialHeight = Shader.loadCompute(Shader.SLGLSL, "Shader/WaterFFT/InitialHeight.compute") self.nodeInitialHeight = NodePath("initialHeight") self.nodeInitialHeight.setShader(self.shaderInitialHeight) self.nodeInitialHeight.setShaderInput("dest", self.texInitialHeight) self.nodeInitialHeight.setShaderInput( "N", LVecBase2i(self.options.size)) self.nodeInitialHeight.setShaderInput( "patchLength", self.options.patchLength) self.nodeInitialHeight.setShaderInput("windDir", self.options.windDir) self.nodeInitialHeight.setShaderInput( "windSpeed", self.options.windSpeed) self.nodeInitialHeight.setShaderInput( "waveAmplitude", self.options.waveAmplitude) self.nodeInitialHeight.setShaderInput( "windDependency", self.options.windDependency) self.nodeInitialHeight.setShaderInput( "randomTex", self.randomStorageTex) self.attrInitialHeight = self.nodeInitialHeight.getAttrib(ShaderAttrib) self.heightTextures = [] for i in xrange(3): tex = Texture("Height") tex.setup2dTexture(self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) tex.setMinfilter(Texture.FTNearest) tex.setMagfilter(Texture.FTNearest) tex.setWrapU(Texture.WMClamp) tex.setWrapV(Texture.WMClamp) self.heightTextures.append(tex) # Also create the shader which updates the spectrum self.shaderUpdate = Shader.loadCompute(Shader.SLGLSL, "Shader/WaterFFT/Update.compute") self.nodeUpdate = NodePath("update") self.nodeUpdate.setShader(self.shaderUpdate) self.nodeUpdate.setShaderInput("outH0x", self.heightTextures[0]) self.nodeUpdate.setShaderInput("outH0y", self.heightTextures[1]) self.nodeUpdate.setShaderInput("outH0z", self.heightTextures[2]) self.nodeUpdate.setShaderInput("initialHeight", self.texInitialHeight) self.nodeUpdate.setShaderInput("N", LVecBase2i(self.options.size)) self.nodeUpdate.setShaderInput("time", self.ptaTime) self.attrUpdate = self.nodeUpdate.getAttrib(ShaderAttrib) # Create 3 FFTs self.fftX = GPUFFT(self.options.size, self.heightTextures[0], self.options.normalizationFactor) self.fftY = GPUFFT(self.options.size, self.heightTextures[1], self.options.normalizationFactor) self.fftZ = GPUFFT(self.options.size, self.heightTextures[2], self.options.normalizationFactor) self.combineNode = NodePath("Combine") self.combineNode.setShader(self.combineShader) self.combineNode.setShaderInput( "displacementX", self.fftX.getResultTexture()) self.combineNode.setShaderInput( "displacementY", self.fftY.getResultTexture()) self.combineNode.setShaderInput( "displacementZ", self.fftZ.getResultTexture()) self.combineNode.setShaderInput("normalDest", self.normalTex) self.combineNode.setShaderInput( "displacementDest", self.displacementTex) self.combineNode.setShaderInput( "N", LVecBase2i(self.options.size)) self.combineNode.setShaderInput( "choppyScale", self.options.choppyScale) self.combineNode.setShaderInput( "gridLength", self.options.patchLength) # Store only the shader attrib as this is way faster self.attrCombine = self.combineNode.getAttrib(ShaderAttrib) def _getGaussianRandom(self): """ Returns a gaussian random number """ u1 = generateRandom() u2 = generateRandom() if u1 < 1e-6: u1 = 1e-6 return sqrt(-2 * log(u1)) * cos(2 * pi * u2) def setup(self): """ Setups the manager """ Globals.base.graphicsEngine.dispatch_compute( (self.options.size / 16, self.options.size / 16, 1), self.attrInitialHeight, Globals.base.win.get_gsg()) def getDisplacementTexture(self): """ Returns the displacement texture, storing the 3D Displacement in the RGB channels """ return self.displacementTex def getNormalTexture(self): """ Returns the normal texture, storing the normal in world space in the RGB channels """ return self.normalTex def update(self): """ Updates the displacement / normal map """ self.ptaTime[0] = 1 + \ Globals.clock.getFrameTime() * self.options.timeScale Globals.base.graphicsEngine.dispatch_compute( (self.options.size / 16, self.options.size / 16, 1), self.attrUpdate, Globals.base.win.get_gsg()) self.fftX.execute() self.fftY.execute() self.fftZ.execute() # Execute the shader which combines the 3 displacement maps into # 1 displacement texture and 1 normal texture. We could use dFdx in # the fragment shader, however that gives no accurate results as # dFdx returns the same value for a 2x2 pixel block Globals.base.graphicsEngine.dispatch_compute( (self.options.size / 16, self.options.size / 16, 1), self.attrCombine, Globals.base.win.get_gsg())
class WaterManager: """ Simple wrapper around WaterDisplacement which combines 3 displacement maps into one, and also generates a normal map """ def __init__(self, water_options): self.options = water_options self.options.size = 512 self.options.wind_dir.normalize() self.options.wave_amplitude *= 1e-7 self.displacement_tex = Texture("Displacement") self.displacement_tex.setup_2d_texture( self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) self.normal_tex = Texture("Normal") self.normal_tex.setup_2d_texture( self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) self.combine_shader = Shader.load_compute(Shader.SLGLSL, "/$$rp/rpcore/water/shader/combine.compute") self.pta_time = PTAFloat.emptyArray(1) # Create a gaussian random texture, as shaders aren't well suited # for that setRandomSeed(523) self.random_storage = PNMImage(self.options.size, self.options.size, 4) self.random_storage.setMaxval((2 ** 16) - 1) for x in range(self.options.size): for y in range(self.options.size): rand1 = self._get_gaussian_random() / 10.0 + 0.5 rand2 = self._get_gaussian_random() / 10.0 + 0.5 self.random_storage.setXel(x, y, float(rand1), float(rand2), 0) self.random_storage.setAlpha(x, y, 1.0) self.random_storage_tex = Texture("RandomStorage") self.random_storage_tex.load(self.random_storage) self.random_storage_tex.set_format(Texture.FRgba16) self.random_storage_tex.set_minfilter(Texture.FTNearest) self.random_storage_tex.set_magfilter(Texture.FTNearest) # Create the texture wwhere the intial height (H0 + Omega0) is stored. self.tex_initial_height = Texture("InitialHeight") self.tex_initial_height.setup_2d_texture( self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) self.tex_initial_height.set_minfilter(Texture.FTNearest) self.tex_initial_height.set_magfilter(Texture.FTNearest) # Create the shader which populates the initial height texture self.shader_initial_height = Shader.load_compute(Shader.SLGLSL, "/$$rp/rpcore/water/shader/initial_height.compute") self.node_initial_height = NodePath("initialHeight") self.node_initial_height.set_shader(self.shader_initial_height) self.node_initial_height.set_shader_input("dest", self.tex_initial_height) self.node_initial_height.set_shader_input( "N", LVecBase2i(self.options.size)) self.node_initial_height.set_shader_input( "patchLength", self.options.patch_length) self.node_initial_height.set_shader_input("windDir", self.options.wind_dir) self.node_initial_height.set_shader_input( "windSpeed", self.options.wind_speed) self.node_initial_height.set_shader_input( "waveAmplitude", self.options.wave_amplitude) self.node_initial_height.set_shader_input( "windDependency", self.options.wind_dependency) self.node_initial_height.set_shader_input( "randomTex", self.random_storage_tex) self.attr_initial_height = self.node_initial_height.get_attrib(ShaderAttrib) self.height_textures = [] for i in range(3): tex = Texture("Height") tex.setup_2d_texture(self.options.size, self.options.size, Texture.TFloat, Texture.FRgba16) tex.set_minfilter(Texture.FTNearest) tex.set_magfilter(Texture.FTNearest) tex.set_wrap_u(Texture.WMClamp) tex.set_wrap_v(Texture.WMClamp) self.height_textures.append(tex) # Also create the shader which updates the spectrum self.shader_update = Shader.load_compute(Shader.SLGLSL, "/$$rp/rpcore/water/shader/update.compute") self.node_update = NodePath("update") self.node_update.set_shader(self.shader_update) self.node_update.set_shader_input("outH0x", self.height_textures[0]) self.node_update.set_shader_input("outH0y", self.height_textures[1]) self.node_update.set_shader_input("outH0z", self.height_textures[2]) self.node_update.set_shader_input("initialHeight", self.tex_initial_height) self.node_update.set_shader_input("N", LVecBase2i(self.options.size)) self.node_update.set_shader_input("time", self.pta_time) self.attr_update = self.node_update.get_attrib(ShaderAttrib) # Create 3 FFTs self.fftX = GPUFFT(self.options.size, self.height_textures[0], self.options.normalization_factor) self.fftY = GPUFFT(self.options.size, self.height_textures[1], self.options.normalization_factor) self.fftZ = GPUFFT(self.options.size, self.height_textures[2], self.options.normalization_factor) self.combine_node = NodePath("Combine") self.combine_node.set_shader(self.combine_shader) self.combine_node.set_shader_input( "displacementX", self.fftX.get_result_texture()) self.combine_node.set_shader_input( "displacementY", self.fftY.get_result_texture()) self.combine_node.set_shader_input( "displacementZ", self.fftZ.get_result_texture()) self.combine_node.set_shader_input("normalDest", self.normal_tex) self.combine_node.set_shader_input( "displacementDest", self.displacement_tex) self.combine_node.set_shader_input( "N", LVecBase2i(self.options.size)) self.combine_node.set_shader_input( "choppyScale", self.options.choppy_scale) self.combine_node.set_shader_input( "gridLength", self.options.patch_length) # Store only the shader attrib as this is way faster self.attr_combine = self.combine_node.get_attrib(ShaderAttrib) def _get_gaussian_random(self): """ Returns a gaussian random number """ u1 = generateRandom() u2 = generateRandom() if u1 < 1e-6: u1 = 1e-6 return sqrt(-2 * log(u1)) * cos(2 * pi * u2) def setup(self): """ Setups the manager """ Globals.base.graphicsEngine.dispatch_compute( (self.options.size // 16, self.options.size // 16, 1), self.attr_initial_height, Globals.base.win.get_gsg()) def get_displacement_texture(self): """ Returns the displacement texture, storing the 3D Displacement in the RGB channels """ return self.displacement_tex def get_normal_texture(self): """ Returns the normal texture, storing the normal in world space in the RGB channels """ return self.normal_tex def update(self): """ Updates the displacement / normal map """ self.pta_time[0] = 1 + Globals.clock.get_frame_time() * self.options.time_scale Globals.base.graphicsEngine.dispatch_compute( (self.options.size // 16, self.options.size // 16, 1), self.attr_update, Globals.base.win.get_gsg()) self.fftX.execute() self.fftY.execute() self.fftZ.execute() # Execute the shader which combines the 3 displacement maps into # 1 displacement texture and 1 normal texture. We could use dFdx in # the fragment shader, however that gives no accurate results as # dFdx returns the same value for a 2x2 pixel block Globals.base.graphicsEngine.dispatch_compute( (self.options.size // 16, self.options.size // 16, 1), self.attr_combine, Globals.base.win.get_gsg())