Pārlūkot izejas kodu

samples: add shader-based particle sample

Closes #476

Co-authored-by: rdb <[email protected]>
Flavio Calva 6 gadi atpakaļ
vecāks
revīzija
d568282350
1 mainītis faili ar 278 papildinājumiem un 0 dzēšanām
  1. 278 0
      samples/particles/advanced.py

+ 278 - 0
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()