advanced.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. #!/usr/bin/env python
  2. # This program shows a shader-based particle system. With this approach, you
  3. # can define an inertial particle system with a moving emitter whose position
  4. # can not be pre-determined.
  5. from array import array
  6. from itertools import chain
  7. from random import uniform
  8. from math import pi, sin, cos
  9. from panda3d.core import TextNode
  10. from panda3d.core import AmbientLight, DirectionalLight
  11. from panda3d.core import LVector3
  12. from panda3d.core import NodePath
  13. from panda3d.core import GeomPoints
  14. from panda3d.core import GeomEnums
  15. from panda3d.core import GeomVertexFormat
  16. from panda3d.core import GeomVertexData
  17. from panda3d.core import GeomNode
  18. from panda3d.core import Geom
  19. from panda3d.core import OmniBoundingVolume
  20. from panda3d.core import Texture
  21. from panda3d.core import TextureStage
  22. from panda3d.core import TexGenAttrib
  23. from panda3d.core import Shader
  24. from panda3d.core import ShaderAttrib
  25. from panda3d.core import loadPrcFileData
  26. from direct.showbase.ShowBase import ShowBase
  27. from direct.gui.OnscreenText import OnscreenText
  28. import sys
  29. HELP_TEXT = """
  30. left/right arrow: Rotate teapot
  31. ESC: Quit
  32. """
  33. # We need to use GLSL 1.50 for these, and some drivers (notably Mesa) require
  34. # us to explicitly ask for an OpenGL 3.2 context in that case.
  35. config = """
  36. gl-version 3 2
  37. """
  38. vert = """
  39. #version 150
  40. #extension GL_ARB_shader_image_load_store : require
  41. layout(rgba32f) uniform imageBuffer positions; // current positions
  42. layout(rgba32f) uniform imageBuffer start_vel; // emission velocities
  43. layout(rgba32f) uniform imageBuffer velocities; // current velocities
  44. layout(rgba32f) uniform imageBuffer emission_times; // emission times
  45. uniform mat4 p3d_ModelViewProjectionMatrix;
  46. uniform vec3 emitter_pos; // emitter's position
  47. uniform vec3 accel; // the acceleration of the particles
  48. uniform float osg_FrameTime; // time of the current frame (absolute)
  49. uniform float osg_DeltaFrameTime;// time since last frame
  50. uniform float start_time; // particle system's start time (absolute)
  51. uniform float part_duration; // single particle's duration
  52. out float from_emission; // time from specific particle's emission
  53. out vec4 color;
  54. void main() {
  55. float emission_time = imageLoad(emission_times, gl_VertexID).x;
  56. vec4 pos = imageLoad(positions, gl_VertexID);
  57. vec4 vel = imageLoad(velocities, gl_VertexID);
  58. float from_start = osg_FrameTime - start_time; // time from system's start
  59. from_emission = 0;
  60. color = vec4(1);
  61. if (from_start > emission_time) { // we've to show the particle
  62. from_emission = from_start - emission_time;
  63. if (from_emission <= osg_DeltaFrameTime + .01) {
  64. // it's particle's emission frame: let's set its position at the
  65. // emitter's position and set the initial velocity
  66. pos = vec4(emitter_pos, 1);
  67. vel = imageLoad(start_vel, gl_VertexID);
  68. }
  69. pos += vec4((vel * osg_DeltaFrameTime).xyz, 0);
  70. vel += vec4(accel, 0) * osg_DeltaFrameTime;
  71. } else color = vec4(0);
  72. // update the emission time (for particle recycling)
  73. if (from_start >= emission_time + part_duration) {
  74. imageStore(emission_times, gl_VertexID, vec4(from_start, 0, 0, 1));
  75. }
  76. gl_PointSize = 10;
  77. gl_Position = p3d_ModelViewProjectionMatrix * pos;
  78. imageStore(positions, gl_VertexID, pos);
  79. imageStore(velocities, gl_VertexID, vel);
  80. }
  81. """
  82. frag = """
  83. #version 150
  84. in float from_emission; // time elapsed from particle's emission
  85. in vec4 color;
  86. uniform float part_duration; // single particle's duration
  87. uniform sampler2D image; // particle's texture
  88. out vec4 p3d_FragData[1];
  89. void main() {
  90. vec4 col = texture(image, gl_PointCoord) * color;
  91. // fade the particle considering the time from its emission
  92. float alpha = clamp(1 - from_emission / part_duration, 0, 1);
  93. p3d_FragData[0] = vec4(col.rgb, col.a * alpha);
  94. }
  95. """
  96. class Particle:
  97. def __init__(
  98. self,
  99. emitter, # the node which is emitting
  100. texture, # particle's image
  101. rate=.001, # the emission rate
  102. gravity=-9.81, # z-component of the gravity force
  103. vel=1.0, # length of emission vector
  104. partDuration=1.0 # single particle's duration
  105. ):
  106. self.__emitter = emitter
  107. self.__texture = texture
  108. # let's compute the total number of particles
  109. self.__numPart = int(round(partDuration * 1 / rate))
  110. self.__rate = rate
  111. self.__gravity = gravity
  112. self.__vel = vel
  113. self.__partDuration = partDuration
  114. self.__nodepath = render.attachNewNode(self.__node())
  115. self.__nodepath.setTransparency(True) # particles have alpha
  116. self.__nodepath.setBin("fixed", 0) # render it at the end
  117. self.__setTextures()
  118. self.__setShader()
  119. self.__nodepath.setRenderModeThickness(10) # we want sprite particles
  120. self.__nodepath.setTexGen(TextureStage.getDefault(),
  121. TexGenAttrib.MPointSprite)
  122. self.__nodepath.setDepthWrite(False) # don't sort the particles
  123. self.__upd_tsk = taskMgr.add(self.__update, "update")
  124. def __node(self):
  125. # this function creates and returns particles' GeomNode
  126. points = GeomPoints(GeomEnums.UH_static)
  127. points.addNextVertices(self.__numPart)
  128. format_ = GeomVertexFormat.getEmpty()
  129. geom = Geom(GeomVertexData("abc", format_, GeomEnums.UH_static))
  130. geom.addPrimitive(points)
  131. geom.setBounds(OmniBoundingVolume()) # always render it
  132. node = GeomNode("node")
  133. node.addGeom(geom)
  134. return node
  135. def __setTextures(self):
  136. # initial positions are all zeros (each position is denoted by 4 values)
  137. # positions are stored in a texture
  138. positions = [(0, 0, 0, 1) for i in range(self.__numPart)]
  139. posLst = list(chain.from_iterable(positions))
  140. self.__texPos = self.__buffTex(posLst)
  141. # define emission times' texture
  142. emissionTimes = [(self.__rate * i, 0, 0, 0)
  143. for i in range(self.__numPart)]
  144. timesLst = list(chain.from_iterable(emissionTimes))
  145. self.__texTimes = self.__buffTex(timesLst)
  146. # define a list with emission velocities
  147. velocities = [self.__rndVel() for _ in range(self.__numPart)]
  148. velLst = list(chain.from_iterable(velocities))
  149. # we need two textures,
  150. # the first one contains the emission velocity (we need to keep it for
  151. # particle recycling)...
  152. self.__texStartVel = self.__buffTex(velLst)
  153. # ... and the second one contains the current velocities
  154. self.__texCurrVel = self.__buffTex(velLst)
  155. def __buffTex(self, values):
  156. # this function returns a buffer texture with the received values
  157. data = array("f", values)
  158. tex = Texture("tex")
  159. tex.setupBufferTexture(self.__numPart, Texture.T_float,
  160. Texture.F_rgba32, GeomEnums.UH_static)
  161. tex.setRamImage(data)
  162. return tex
  163. def __rndVel(self):
  164. # this method returns a random vector for emitting the particle
  165. theta = uniform(0, pi / 12)
  166. phi = uniform(0, 2 * pi)
  167. vec = LVector3(
  168. sin(theta) * cos(phi),
  169. sin(theta) * sin(phi),
  170. cos(theta))
  171. vec *= uniform(self.__vel * .8, self.__vel * 1.2)
  172. return [vec.x, vec.y, vec.z, 1]
  173. def __setShader(self):
  174. shader = Shader.make(Shader.SL_GLSL, vert, frag)
  175. # Apply the shader to the node, but set a special flag indicating that
  176. # the point size is controlled bythe shader.
  177. attrib = ShaderAttrib.make(shader)
  178. attrib = attrib.setFlag(ShaderAttrib.F_shader_point_size, True)
  179. self.__nodepath.setAttrib(attrib)
  180. self.__nodepath.setShaderInputs(
  181. positions=self.__texPos,
  182. emitter_pos=self.__emitter.getPos(render),
  183. start_vel=self.__texStartVel,
  184. velocities=self.__texCurrVel,
  185. accel=(0, 0, self.__gravity),
  186. start_time=base.clock.getFrameTime(),
  187. emission_times=self.__texTimes,
  188. part_duration=self.__partDuration,
  189. image=loader.loadTexture(self.__texture))
  190. def __update(self, task):
  191. pos = self.__emitter.getPos(render)
  192. self.__nodepath.setShaderInput("emitter_pos", pos)
  193. return task.again
  194. class ParticleDemo(ShowBase):
  195. def __init__(self):
  196. loadPrcFileData("config", config)
  197. ShowBase.__init__(self)
  198. # Standard title and instruction text
  199. self.title = OnscreenText(
  200. text="Panda3D: Tutorial - Shader-based Particles",
  201. parent=base.a2dBottomCenter,
  202. style=1, fg=(1, 1, 1, 1), pos=(0, 0.1), scale=.08)
  203. self.escapeEvent = OnscreenText(
  204. text=HELP_TEXT, parent=base.a2dTopLeft,
  205. style=1, fg=(1, 1, 1, 1), pos=(0.06, -0.06),
  206. align=TextNode.ALeft, scale=.05)
  207. # More standard initialization
  208. self.accept('escape', sys.exit)
  209. self.accept('arrow_left', self.rotate, ['left'])
  210. self.accept('arrow_right', self.rotate, ['right'])
  211. base.disableMouse()
  212. base.camera.setPos(0, -20, 2)
  213. base.setBackgroundColor(0, 0, 0)
  214. self.teapot = loader.loadModel("teapot")
  215. self.teapot.setPos(0, 10, 0)
  216. self.teapot.reparentTo(render)
  217. self.setupLights()
  218. # we define a nodepath as particle's emitter
  219. self.emitter = NodePath("emitter")
  220. self.emitter.reparentTo(self.teapot)
  221. self.emitter.setPos(3.000, 0.000, 2.550)
  222. # let's create the particle system
  223. Particle(self.emitter, "smoke.png", gravity=.01, vel=1.2,
  224. partDuration=5.0)
  225. def rotate(self, direction):
  226. direction_factor = (1 if direction == "left" else -1)
  227. self.teapot.setH(self.teapot.getH() + 10 * direction_factor)
  228. # Set up lighting
  229. def setupLights(self):
  230. ambientLight = AmbientLight("ambientLight")
  231. ambientLight.setColor((.4, .4, .35, 1))
  232. directionalLight = DirectionalLight("directionalLight")
  233. directionalLight.setDirection(LVector3(0, 8, -2.5))
  234. directionalLight.setColor((0.9, 0.8, 0.9, 1))
  235. # Set lighting on teapot so steam doesn't get affected
  236. self.teapot.setLight(self.teapot.attachNewNode(directionalLight))
  237. self.teapot.setLight(self.teapot.attachNewNode(ambientLight))
  238. demo = ParticleDemo()
  239. demo.run()