Smoke Trail Rendering

Smoke Trail Rendering

I am experimenting with several methods of rendering missile smoke trails and each of these methods has limitations or deficiencies that make them unusable. I am posting this thread in hopes that someone can enlighten me on how to improve these methods or give me another method entirely. I have create a test harness P3D application for testing the methods so I can make changes or additions and quickly assess the feasibility of potential improvements so … please feel free to brainstorm here.

Note … I have a missile model with a trajectory stored in memory as a table with interpolation capabilities. I can retrieve each discrete state stored in the table or I can specify a time and let the table class interpolate the appropriate state. Trajectory states are stored as (x,y,z,h,p,r,velocity,thrust).

The basic concept for each method is to place billboards along the trajectory and texture map transparent smoke puff images on them. I use one of two methods for placement of the billboards; ‘By Sample’ or ‘By Distance’. In the ‘By Sample’ method, I place a billboard at every ‘i’th discrete state in the table. In the ‘By Distance’ method, I place billboards at equally spaced distances along the trajectory.

I am using two different methods for representing the billboards; ‘Sprite Rendering’ and ‘NodePath Rendering’. In ‘Sprite Rendering’ I create a GeomNode containing sprites for all of the placed billboards along the trajectory. In ‘NodePath Rendering’ I create a NodePath for every placed billboard.

Now, ‘NodePath Rendering’ gives me a beautiful exhaust trail but brings my PC to it’s knees when the trajectory is long enough to contain a few thousand samples. The ‘Sprite Rendering’ method performs well for even the longest trajectories but the particle sizes stay constant regardless of the distance they are from your eye. The net effect is the fattening effect as in the following figure.

Ideas anyone? I am posting my smoke trail code below and can post the rest if needed.

Thanks,
Paul

# SmokeTrails.py

""" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	Description: Smoke trail models

	$Author: pleopard $

	$Modtime: 10/19/06 8:46p $

	$Revision: 1 $ 

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """

# Standard imports

import sys

# Panda imports

from pandac.PandaModules import *

# PHL imports

from PHL.Core.Diagnostics import *
from PHL.Math.TimeSeriesTable import *
from PHL.Core.Properties import *
from PHL.P3D.Shapes import *

# Local imports

#from Missile import Missile

# Other stuff

Time=0
X=1
Y=2
Z=3
PhiRoll=4
ThetaPitch=5
PsiYaw=6

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Base class

class SmokeTrail:

	PlaceByDistance = 1
	PlaceBySample = 2

	RenderSprites = 1
	RenderNodePaths = 2

	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	# 
	def timeUpdate(self,t):
		xyz = self.mParentMissile.getPos()
		for n in self.mNodes:
			n.removeNode()

		if self.mPlacementMethod == SmokeTrail.PlaceByDistance:
			self.placeByDistance(t)
		else:
			self.placeBySample(t)

	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	# 
	def __init__(\
			self,
			parentMsl,
			texture,
			placementMethod,
			placementArg,
			renderMethod
		):
		Assert(texture!=None,"Invalid smoke trail texture")
		Assert(\
			placementMethod==SmokeTrail.PlaceByDistance or
			placementMethod==SmokeTrail.PlaceBySample,
			"Invalid smoke trail placement method : "+str(placementMethod)
		)
		Assert(\
			renderMethod==SmokeTrail.RenderSprites or
			renderMethod==SmokeTrail.RenderNodePaths,
			"Invalid smoke trail render method : "+str(renderMethod)
		)

		self.mParentMissile = parentMsl
		self.mTexture = texture
		self.mPlacementMethod = placementMethod
		self.mRenderDistance = float(placementArg)
		self.mRenderSpacing = int(placementArg)
		self.mRenderMethod = renderMethod
		self.mSpriteNode = None
		self.mNodes = []

	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	# 
	def placeBySample(self,t):

		# Create sample points list
		traj = self.mParentMissile.getTrajectory()
		n = traj.sampleCount()
		points = []
		for i in range(0,n,self.mRenderSpacing):
			s = traj.getDiscreteState(i)
			ts = s[Time]
			if ts>t:
				break
			xyz = (s[X],s[Y],s[Z])
			points.append(xyz)

		# Render sprites
		if self.mRenderMethod==SmokeTrail.RenderSprites:
			pass

		# Render NodePaths
		elif self.mRenderMethod==SmokeTrail.RenderNodePaths:
			for p in points:
				n = NodePath(P3DCreateQuadXY())
				n.setBillboardPointEye()
				n.setScale(10,10,10)
				n.setColor(1,1,1)
				n.setPos(p[0],p[1],p[2])
				n.setTexture(self.mTexture)
				n.setTwoSided(True)
				n.setTransparency(1)
				self.mNodes.append(n)
				n.reparentTo(render)

		else:
			Assert(False,"Invalid smoke trail rendering method")

	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	# 
	def placeByDistance(self,t):

		# Create sample points list
		traj = self.mParentMissile.getTrajectory()
		n = traj.sampleCount()
		points = []
		s = traj.getDiscreteState(0)
		xyzLast = (s[X],s[Y],s[Z])
		for i in range(0,n,self.mRenderSpacing):
			s = traj.getDiscreteState(i)
			ts = s[Time]
			if ts>t:
				break
			xyz = (s[X],s[Y],s[Z])
			dx = xyzLast[0]-xyz[0]
			dy = xyzLast[1]-xyz[1]
			dz = xyzLast[2]-xyz[2]
			r2 = dx*dx + dy*dy + dz*dz
			r = math.sqrt(r2)
			if r>=self.mRenderDistance:
				points.append(xyz)
				xyzLast = xyz

		# Render sprites
		if self.mRenderMethod==SmokeTrail.RenderSprites:

			gvf = GeomVertexFormat.getV3()
			vertexData = GeomVertexData('SpriteVertices',gvf,Geom.UHStatic)
			vtxWriter = GeomVertexWriter(vertexData,'vertex')

			for p in points:
				vtxWriter.addData3f(p[0],p[1],p[2])

			geom = Geom(vertexData)
			gPoints = GeomPoints(Geom.UHStatic)
			gPoints.setIndexType(Geom.NTUint32)
			n = len(points)
			for i in range(0,n):
				gPoints.addVertex(i)

			gPoints.closePrimitive()
			geom.addPrimitive(gPoints)
			geomNode = GeomNode('Sprites')
			geomNode.addGeom(geom)

			if self.mSpriteNode!=None:
				self.mSpriteNode.removeNode()

			billboardSize = 20
			spriteColor = (1,1,1)

			self.mSpriteNode = NodePath(geomNode)
			self.mSpriteNode.setColor(\
				spriteColor[0],
				spriteColor[1],
				spriteColor[2]
			)
			self.mSpriteNode.setTwoSided(True)
			self.mSpriteNode.setRenderModePerspective(True)
			self.mSpriteNode.setRenderModeThickness(billboardSize)
			self.mSpriteNode.setTexGen(\
				TextureStage.getDefault(),
				TexGenAttrib.MPointSprite
			)
			self.mSpriteNode.setTexture(self.mTexture,1)
			self.mSpriteNode.setTransparency(TransparencyAttrib.MAlpha) 
			self.mSpriteNode.setDepthTest(0) 
			self.mSpriteNode.reparentTo(render)

		# Render NodePaths
		else:
			for p in points:
				n = NodePath(P3DCreateQuadXY())
				n.setBillboardPointEye()
				n.setScale(10,10,10)
				n.setColor(1,1,1)
				n.setPos(p[0],p[1],p[2])
				n.setTexture(self.mTexture)
				n.setTwoSided(True)
				n.setTransparency(1)
				self.mNodes.append(n)
				n.reparentTo(render)

Hmm, the line:

self.mSpriteNode.setRenderModePerspective(True)

is supposed to enable perspective-rendering of your point sprites. On some hardware, though, this fails if the sprites exceed a certain size. You might try setting:

hardware-point-sprites 0

in your Config.prc. This will disable the use of hardware extensions to do the perspective sprites, and they should scale correctly with distance.

Note that you may have your particles set too large anyway. This may become more obvious when you disable the hardware perspective, which will allow the sprites to render at the size you’re actually asking them to render. :slight_smile:

David

Ok now we are getting somewhere, thanks. With hardware sprites turned off and perspective rendering turned on I get the following when rendering with sprites. I don’t suppose that there is a way to exaggerate the sprite size effect so that the ones close to the camera are even smaller?

I experimented with lowering the sprite sizes and they get so small that the trajectory is broken up … see below …

Well, sure–if you make the sprites smaller, you’ll need more of them to fill the same amount of space.

You might also try turning off the depth write for your sprites, and/or move them to render last in the scene, like this:

self.mSpriteNode.setDepthWrite(0)
self.mSpriteNode.setBin('fixed', 0)

This will fix up the problems where you see a bit of the corners of the polygons where they overlap, and make the smoke blend together better.

David

That fixed the edge effects, thanks. I suppose that if I want control of the individual sprites separately I will have to go the particle system method.

I am dropping the NodePath rendering method altogether as it has no hope of performing well enough.

You can set a different size for each sprite, if you like. To do this, add a new column to your vertex format called “size”; it will be a 1-float column:

arrayFormat.addColumn(InternalName.make('size'), 1, Geom.NTFloat32, Geom.COther)

Then whatever value you set for each sprite in this column, using the GeomVertexWriter, will override the value you set via setRenderModeThickness().

You can also rotate the individual smoke puffs using a similar column called “rotate”, which receives an angle in degrees. This can help avoid the sameness of adjacent smoke puffs.

Both of these tricks are employed by the SpriteParticleRenderer. Since the particle system is built on top of the low-level Panda rendering engine, anything that the particle system can do, you can do by hand too. It just becomes a question of how much effort do you want to put into duplicating what the particle system already does for you automatically. :slight_smile:

David

Good point, Thanks!