From d5682823507597ef791bae9f600164d7c792dfe9 Mon Sep 17 00:00:00 2001 From: Flavio Calva Date: Sun, 17 Mar 2019 20:15:12 +0100 Subject: [PATCH] samples: add shader-based particle sample Closes #476 Co-authored-by: rdb --- samples/particles/advanced.py | 278 ++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100755 samples/particles/advanced.py diff --git a/samples/particles/advanced.py b/samples/particles/advanced.py new file mode 100755 index 0000000000..1521c1453d --- /dev/null +++ b/samples/particles/advanced.py @@ -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()