278 lines
10 KiB
Python
278 lines
10 KiB
Python
#!/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()
|