|
|
@@ -0,0 +1,278 @@
|
|
|
+#!/usr/bin/env python
|
|
|
+
|
|
|
+# This program shows a shader-based particle system. With this approach, you
|
|
|
+# can define an inertial particle system with a moving emitter whose position
|
|
|
+# can not be pre-determined.
|
|
|
+
|
|
|
+from array import array
|
|
|
+from itertools import chain
|
|
|
+from random import uniform
|
|
|
+from math import pi, sin, cos
|
|
|
+from panda3d.core import TextNode
|
|
|
+from panda3d.core import AmbientLight, DirectionalLight
|
|
|
+from panda3d.core import LVector3
|
|
|
+from panda3d.core import NodePath
|
|
|
+from panda3d.core import GeomPoints
|
|
|
+from panda3d.core import GeomEnums
|
|
|
+from panda3d.core import GeomVertexFormat
|
|
|
+from panda3d.core import GeomVertexData
|
|
|
+from panda3d.core import GeomNode
|
|
|
+from panda3d.core import Geom
|
|
|
+from panda3d.core import OmniBoundingVolume
|
|
|
+from panda3d.core import Texture
|
|
|
+from panda3d.core import TextureStage
|
|
|
+from panda3d.core import TexGenAttrib
|
|
|
+from panda3d.core import Shader
|
|
|
+from panda3d.core import ShaderAttrib
|
|
|
+from panda3d.core import loadPrcFileData
|
|
|
+from direct.showbase.ShowBase import ShowBase
|
|
|
+from direct.gui.OnscreenText import OnscreenText
|
|
|
+import sys
|
|
|
+
|
|
|
+HELP_TEXT = """
|
|
|
+left/right arrow: Rotate teapot
|
|
|
+ESC: Quit
|
|
|
+"""
|
|
|
+
|
|
|
+# We need to use GLSL 1.50 for these, and some drivers (notably Mesa) require
|
|
|
+# us to explicitly ask for an OpenGL 3.2 context in that case.
|
|
|
+config = """
|
|
|
+gl-version 3 2
|
|
|
+"""
|
|
|
+
|
|
|
+vert = """
|
|
|
+#version 150
|
|
|
+#extension GL_ARB_shader_image_load_store : require
|
|
|
+
|
|
|
+layout(rgba32f) uniform imageBuffer positions; // current positions
|
|
|
+layout(rgba32f) uniform imageBuffer start_vel; // emission velocities
|
|
|
+layout(rgba32f) uniform imageBuffer velocities; // current velocities
|
|
|
+layout(rgba32f) uniform imageBuffer emission_times; // emission times
|
|
|
+uniform mat4 p3d_ModelViewProjectionMatrix;
|
|
|
+uniform vec3 emitter_pos; // emitter's position
|
|
|
+uniform vec3 accel; // the acceleration of the particles
|
|
|
+uniform float osg_FrameTime; // time of the current frame (absolute)
|
|
|
+uniform float osg_DeltaFrameTime;// time since last frame
|
|
|
+uniform float start_time; // particle system's start time (absolute)
|
|
|
+uniform float part_duration; // single particle's duration
|
|
|
+
|
|
|
+out float from_emission; // time from specific particle's emission
|
|
|
+out vec4 color;
|
|
|
+
|
|
|
+void main() {
|
|
|
+ float emission_time = imageLoad(emission_times, gl_VertexID).x;
|
|
|
+ vec4 pos = imageLoad(positions, gl_VertexID);
|
|
|
+ vec4 vel = imageLoad(velocities, gl_VertexID);
|
|
|
+ float from_start = osg_FrameTime - start_time; // time from system's start
|
|
|
+ from_emission = 0;
|
|
|
+ color = vec4(1);
|
|
|
+ if (from_start > emission_time) { // we've to show the particle
|
|
|
+ from_emission = from_start - emission_time;
|
|
|
+ if (from_emission <= osg_DeltaFrameTime + .01) {
|
|
|
+ // it's particle's emission frame: let's set its position at the
|
|
|
+ // emitter's position and set the initial velocity
|
|
|
+ pos = vec4(emitter_pos, 1);
|
|
|
+ vel = imageLoad(start_vel, gl_VertexID);
|
|
|
+ }
|
|
|
+ pos += vec4((vel * osg_DeltaFrameTime).xyz, 0);
|
|
|
+ vel += vec4(accel, 0) * osg_DeltaFrameTime;
|
|
|
+ } else color = vec4(0);
|
|
|
+
|
|
|
+ // update the emission time (for particle recycling)
|
|
|
+ if (from_start >= emission_time + part_duration) {
|
|
|
+ imageStore(emission_times, gl_VertexID, vec4(from_start, 0, 0, 1));
|
|
|
+ }
|
|
|
+ gl_PointSize = 10;
|
|
|
+ gl_Position = p3d_ModelViewProjectionMatrix * pos;
|
|
|
+ imageStore(positions, gl_VertexID, pos);
|
|
|
+ imageStore(velocities, gl_VertexID, vel);
|
|
|
+}
|
|
|
+"""
|
|
|
+
|
|
|
+frag = """
|
|
|
+#version 150
|
|
|
+
|
|
|
+in float from_emission; // time elapsed from particle's emission
|
|
|
+in vec4 color;
|
|
|
+uniform float part_duration; // single particle's duration
|
|
|
+uniform sampler2D image; // particle's texture
|
|
|
+out vec4 p3d_FragData[1];
|
|
|
+
|
|
|
+void main() {
|
|
|
+ vec4 col = texture(image, gl_PointCoord) * color;
|
|
|
+ // fade the particle considering the time from its emission
|
|
|
+ float alpha = clamp(1 - from_emission / part_duration, 0, 1);
|
|
|
+ p3d_FragData[0] = vec4(col.rgb, col.a * alpha);
|
|
|
+}
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
+class Particle:
|
|
|
+
|
|
|
+ def __init__(
|
|
|
+ self,
|
|
|
+ emitter, # the node which is emitting
|
|
|
+ texture, # particle's image
|
|
|
+ rate=.001, # the emission rate
|
|
|
+ gravity=-9.81, # z-component of the gravity force
|
|
|
+ vel=1.0, # length of emission vector
|
|
|
+ partDuration=1.0 # single particle's duration
|
|
|
+ ):
|
|
|
+ self.__emitter = emitter
|
|
|
+ self.__texture = texture
|
|
|
+ # let's compute the total number of particles
|
|
|
+ self.__numPart = int(round(partDuration * 1 / rate))
|
|
|
+ self.__rate = rate
|
|
|
+ self.__gravity = gravity
|
|
|
+ self.__vel = vel
|
|
|
+ self.__partDuration = partDuration
|
|
|
+ self.__nodepath = render.attachNewNode(self.__node())
|
|
|
+ self.__nodepath.setTransparency(True) # particles have alpha
|
|
|
+ self.__nodepath.setBin("fixed", 0) # render it at the end
|
|
|
+ self.__setTextures()
|
|
|
+ self.__setShader()
|
|
|
+ self.__nodepath.setRenderModeThickness(10) # we want sprite particles
|
|
|
+ self.__nodepath.setTexGen(TextureStage.getDefault(),
|
|
|
+ TexGenAttrib.MPointSprite)
|
|
|
+ self.__nodepath.setDepthWrite(False) # don't sort the particles
|
|
|
+ self.__upd_tsk = taskMgr.add(self.__update, "update")
|
|
|
+
|
|
|
+ def __node(self):
|
|
|
+ # this function creates and returns particles' GeomNode
|
|
|
+ points = GeomPoints(GeomEnums.UH_static)
|
|
|
+ points.addNextVertices(self.__numPart)
|
|
|
+ format_ = GeomVertexFormat.getEmpty()
|
|
|
+ geom = Geom(GeomVertexData("abc", format_, GeomEnums.UH_static))
|
|
|
+ geom.addPrimitive(points)
|
|
|
+ geom.setBounds(OmniBoundingVolume()) # always render it
|
|
|
+ node = GeomNode("node")
|
|
|
+ node.addGeom(geom)
|
|
|
+ return node
|
|
|
+
|
|
|
+ def __setTextures(self):
|
|
|
+ # initial positions are all zeros (each position is denoted by 4 values)
|
|
|
+ # positions are stored in a texture
|
|
|
+ positions = [(0, 0, 0, 1) for i in range(self.__numPart)]
|
|
|
+ posLst = list(chain.from_iterable(positions))
|
|
|
+ self.__texPos = self.__buffTex(posLst)
|
|
|
+
|
|
|
+ # define emission times' texture
|
|
|
+ emissionTimes = [(self.__rate * i, 0, 0, 0)
|
|
|
+ for i in range(self.__numPart)]
|
|
|
+ timesLst = list(chain.from_iterable(emissionTimes))
|
|
|
+ self.__texTimes = self.__buffTex(timesLst)
|
|
|
+
|
|
|
+ # define a list with emission velocities
|
|
|
+ velocities = [self.__rndVel() for _ in range(self.__numPart)]
|
|
|
+ velLst = list(chain.from_iterable(velocities))
|
|
|
+ # we need two textures,
|
|
|
+ # the first one contains the emission velocity (we need to keep it for
|
|
|
+ # particle recycling)...
|
|
|
+ self.__texStartVel = self.__buffTex(velLst)
|
|
|
+ # ... and the second one contains the current velocities
|
|
|
+ self.__texCurrVel = self.__buffTex(velLst)
|
|
|
+
|
|
|
+ def __buffTex(self, values):
|
|
|
+ # this function returns a buffer texture with the received values
|
|
|
+ data = array("f", values)
|
|
|
+ tex = Texture("tex")
|
|
|
+ tex.setupBufferTexture(self.__numPart, Texture.T_float,
|
|
|
+ Texture.F_rgba32, GeomEnums.UH_static)
|
|
|
+ tex.setRamImage(data)
|
|
|
+ return tex
|
|
|
+
|
|
|
+ def __rndVel(self):
|
|
|
+ # this method returns a random vector for emitting the particle
|
|
|
+ theta = uniform(0, pi / 12)
|
|
|
+ phi = uniform(0, 2 * pi)
|
|
|
+ vec = LVector3(
|
|
|
+ sin(theta) * cos(phi),
|
|
|
+ sin(theta) * sin(phi),
|
|
|
+ cos(theta))
|
|
|
+ vec *= uniform(self.__vel * .8, self.__vel * 1.2)
|
|
|
+ return [vec.x, vec.y, vec.z, 1]
|
|
|
+
|
|
|
+ def __setShader(self):
|
|
|
+ shader = Shader.make(Shader.SL_GLSL, vert, frag)
|
|
|
+
|
|
|
+ # Apply the shader to the node, but set a special flag indicating that
|
|
|
+ # the point size is controlled bythe shader.
|
|
|
+ attrib = ShaderAttrib.make(shader)
|
|
|
+ attrib = attrib.setFlag(ShaderAttrib.F_shader_point_size, True)
|
|
|
+ self.__nodepath.setAttrib(attrib)
|
|
|
+
|
|
|
+ self.__nodepath.setShaderInputs(
|
|
|
+ positions=self.__texPos,
|
|
|
+ emitter_pos=self.__emitter.getPos(render),
|
|
|
+ start_vel=self.__texStartVel,
|
|
|
+ velocities=self.__texCurrVel,
|
|
|
+ accel=(0, 0, self.__gravity),
|
|
|
+ start_time=globalClock.getFrameTime(),
|
|
|
+ emission_times=self.__texTimes,
|
|
|
+ part_duration=self.__partDuration,
|
|
|
+ image=loader.loadTexture(self.__texture))
|
|
|
+
|
|
|
+ def __update(self, task):
|
|
|
+ pos = self.__emitter.getPos(render)
|
|
|
+ self.__nodepath.setShaderInput("emitter_pos", pos)
|
|
|
+ return task.again
|
|
|
+
|
|
|
+
|
|
|
+class ParticleDemo(ShowBase):
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ loadPrcFileData("config", config)
|
|
|
+
|
|
|
+ ShowBase.__init__(self)
|
|
|
+
|
|
|
+ # Standard title and instruction text
|
|
|
+ self.title = OnscreenText(
|
|
|
+ text="Panda3D: Tutorial - Shader-based Particles",
|
|
|
+ parent=base.a2dBottomCenter,
|
|
|
+ style=1, fg=(1, 1, 1, 1), pos=(0, 0.1), scale=.08)
|
|
|
+ self.escapeEvent = OnscreenText(
|
|
|
+ text=HELP_TEXT, parent=base.a2dTopLeft,
|
|
|
+ style=1, fg=(1, 1, 1, 1), pos=(0.06, -0.06),
|
|
|
+ align=TextNode.ALeft, scale=.05)
|
|
|
+
|
|
|
+ # More standard initialization
|
|
|
+ self.accept('escape', sys.exit)
|
|
|
+ self.accept('arrow_left', self.rotate, ['left'])
|
|
|
+ self.accept('arrow_right', self.rotate, ['right'])
|
|
|
+ base.disableMouse()
|
|
|
+ base.camera.setPos(0, -20, 2)
|
|
|
+ base.setBackgroundColor(0, 0, 0)
|
|
|
+
|
|
|
+ self.teapot = loader.loadModel("teapot")
|
|
|
+ self.teapot.setPos(0, 10, 0)
|
|
|
+ self.teapot.reparentTo(render)
|
|
|
+ self.setupLights()
|
|
|
+
|
|
|
+ # we define a nodepath as particle's emitter
|
|
|
+ self.emitter = NodePath("emitter")
|
|
|
+ self.emitter.reparentTo(self.teapot)
|
|
|
+ self.emitter.setPos(3.000, 0.000, 2.550)
|
|
|
+
|
|
|
+ # let's create the particle system
|
|
|
+ Particle(self.emitter, "smoke.png", gravity=.01, vel=1.2,
|
|
|
+ partDuration=5.0)
|
|
|
+
|
|
|
+ def rotate(self, direction):
|
|
|
+ direction_factor = (1 if direction == "left" else -1)
|
|
|
+ self.teapot.setH(self.teapot.getH() + 10 * direction_factor)
|
|
|
+
|
|
|
+ # Set up lighting
|
|
|
+ def setupLights(self):
|
|
|
+ ambientLight = AmbientLight("ambientLight")
|
|
|
+ ambientLight.setColor((.4, .4, .35, 1))
|
|
|
+ directionalLight = DirectionalLight("directionalLight")
|
|
|
+ directionalLight.setDirection(LVector3(0, 8, -2.5))
|
|
|
+ directionalLight.setColor((0.9, 0.8, 0.9, 1))
|
|
|
+
|
|
|
+ # Set lighting on teapot so steam doesn't get affected
|
|
|
+ self.teapot.setLight(self.teapot.attachNewNode(directionalLight))
|
|
|
+ self.teapot.setLight(self.teapot.attachNewNode(ambientLight))
|
|
|
+
|
|
|
+
|
|
|
+demo = ParticleDemo()
|
|
|
+demo.run()
|