8879 Rate this article:
5.0

Animated Object Graphics - Emulating Rocket Exhaust

Jim Pendleton

It's fun to investigate the more obscure features available in IDL's graphics system, just to see where creativity can take you.  A feature is only obscure if you never use it, after all.


A few years ago I'd been watching some videos of rocket motor testing and I wondered how easy or hard it would be to emulate, in a simple graphics rather than physical sense, the hot exhaust from such a motor using only IDL, without the use of crazy GLSL shader programs or GPU kernels.  Perhaps it could be used in animation of a future project, or at the very least I could learn some fun tricks.


IDL's Object Graphics routines expose low-level graphics functionality simply and consistently from our high level language.


Here's the full routine, up-front, so you can copy and paste it to IDL and run it.  Let's see where we are going before we get there!


pro rocket_exhaust
compile_opt strictarr
on_error, 2
p1 = [2, 0, 0]
p2 = [5, 10, 0]
p = [[p1], [p2]]
mesh_obj, 6, vertex, poly, p, p1 = 361, p2 = [0, 0, 0], p3 = [0, 1, 0], $
  p4 = 0, p5 = 2*!dpi, /closed
omodel = idlgrmodel()
ndims = size(vertex, /dimensions)
vertex += randomn(seed, ndims[0], ndims[1])*1.e-2
opoly = idlgrpolygon(vertex, poly = poly, $
  style = 2, color = [255, 255, 255], shading = 1, $
  emission = [255, 0, 0])
opolymodel = idlgrmodel()
opolymodel->add, opoly
omodel->add, opolymodel
opolymodel2 = idlgrmodel()
opolymodel2->scale, .5, 1.5, .5
opolymodel2->rotate, [0, 1, 0], 73.
omodel->add, opolymodel2
opolymodel2->add, opolymodel, /alias
opolymodel3 = idlgrmodel()
opolymodel3->scale, .75, 1.25, .75
opolymodel3->add, opolymodel, /alias
opolymodel3->rotate, [0, 1, 0], 23.
omodel->add, opolymodel3
image = bytarr(4, 256, 256)
redplane = replicate(1B, 256) # reverse(bindgen(256)/2 + 150b)
image[0, *, *] = redplane
image[1, *, *] = redplane
oimage = idlgrimage(image, transform_mode = 1)
vertexx = reform(vertex[0, *])
vertexy = reform(vertex[1, *])
minvertexx = min(vertexx, max = maxvertexx)
minvertexy = min(vertexy, max = maxvertexy)
normalizedx = (vertexx - minvertexx)/(maxvertexx - minvertexx)
normalizedy = (vertexy - minvertexy)/(maxvertexy - minvertexy)
texturecoords = transpose([[normalizedx], [normalizedy]])
opoly->setproperty, texture_map = oimage, $
  texture_coord = texturecoords
xobjview, omodel, tlb = tlb, background = [0, 0, 0], xsize = 512, ysize=512
newimage = long(image) + randomn(seed, 4, 256, 256)
highblue = where(abs(newimage[2, *, *]) gt 2, nhighblue, $
complement = lowblue, ncomplement = nlowblue)
if (nhighblue ne 0) then begin
  for i = 0, 2 do begin
    planeimage = reform(newimage[i, *, *], 256l*256)
    planeimage[highblue] = 255
    newimage[i, *, *] = reform(planeimage, 256, 256)
  endfor
endif
if (nlowblue ne 0) then begin
  planeimage = reform(newimage[2, *, *], 256l*256)
  planeimage[lowblue] = 0
  newimage[2, *, *] = reform(planeimage, 256, 256)
endif
newimage <= 255b
oimage->setproperty, data = newimage
for i = 0, 511 do begin
  newvertex = vertex + randomn(seed, ndims[0], ndims[1])*.2
  opoly->setproperty, data = newvertex
  oimage->setproperty, data = newimage
  newimage = shift(newimage, 0, 0, 10)
  xobjview_rotate, [1, 0, 1], 1
endfor
end

The main features I wanted to emulate from the videos were the nested conic structure of the exhaust, the pulsation over time, and the apparent randomness of brighter "sparking" features.


Creating a conic section graphically is straightforward.  The MESH_OBJ function, written by my former officemate-turned-artist Daniel Carr, can generate many types of polygonal meshes that can be input directly into an IDLgrPolygon object's vertex and connectivity lists.


A simple conic section can be generated via a surface of revolution.  (That's the magic "6" in the parameter list.)


IDL> p1 = [2, 0, 0]
IDL> p2 = [5, 10, 0]
IDL> p = [[p1], [p2]]
IDL> mesh_obj, 6, vertex, poly, p, p1 = 361, p2 = [0, 0, 0], p3 = [0, 1, 0], $
IDL>    p4 = 0, p5 = 2*!dpi, /closed


To test what this initial cone looks like, create an IDLgrPolygon and pass it to the utility XOBJVIEW.


IDL> opoly =idlgrpolygon(vertex, poly = poly, $
IDL>    style = 2, color = [255, 255, 255], shading = 1, $
IDL>    emission = [255, 0, 0])
IDL> xobjview, opoly


It's a tall lampshade, but the shape is generally correct.  Because we want to add a couple more conic sections and treat them all as one entity when we perform 3D transformations, let's create an outer container IDLgrModel that will hold our conic sections.


IDL> omodel = idlgrmodel()

Create a model that will hold only the first IDLgrPolygon, then add it to the outer model.

IDL> opolymodel = idlgrmodel()
IDL> opolymodel->add, opoly
IDL> omodel->add, opolymodel 


I want to nest two other cones within the first.  Simplistically, I could create two more sets of vertices with MESH_OBJ having slightly different input parameters, but instead I'll use a trick.


It's possible to add something called an ALIAS of an object model to another model.  The original data is only stored once in the graphics system, reducing the memory footprint.  This is important if you're storing large 3-dimensional objects and you want to view them simultaneously in different ways.  This might include axial, sagittal, and coronal views of a medical specimen for example, or showing multiple cutaway sections of a single, complex automobile part.  Replication wastes resources needlessly and should be avoided when graphics performance is a primary desire.


Creating an alias to an object does not mean it needs to appear exactly in the same place as the original.  Any transforms applied to the new containing model will be applied to the alias of the original data, without changing the original at all.  You can't change the basic graphic properties of the atom in your alias, such as its color or vertex connectivity, but you can apply spatial transformations to the object via the container, stretching, rotating, translating, shearing, etc.


We'll add a nested cone within the first cone, using an alias of the first cone as the starting point.  The scaling of the container of the alias is what will give us a new shape.


IDL> opolymodel2 = idlgrmodel()
IDL> opolymodel2->scale, .5, 1.5, .5
IDL> omodel->add, opolymodel2
IDL> opolymodel2->add, opolymodel, /alias


Run XOBJVIEW again with omodel to see the nested structure we're creating.


IDL> xobjview, omodel


Just for fun, let's follow the pattern above and add another cone.  We'll use slightly different scaling parameters which will stretch this cone a different way.


IDL> opolymodel3 = obj_new('idlgrmodel')
IDL> opolymodel3->scale, .75, 1.25, .75
IDL> opolymodel3->add, opolymodel, /alias
IDL> omodel->add, opolymodel3

If you still have xobjview up, rotate the view.  The third polygon has not-so-magically appeared.  This doesn't look very much like exhaust, yet.  It's only a set of nested white conic sections.


Let's make a colored IDLgrImage to wrap around the cones as a texture map.  We'll use transparency in our texture map so we'll be able to see through the cones to the ones behind.  We'll give the basic yellow color a level of gradient as well.


IDL> image = bytarr(4, 256, 256)
IDL> redplane = replicate(1b, 256) # reverse(bindgen(256)/2 + 150b)
IDL> image[0, *, *] = redplane
IDL> image[1, *, *] = redplane
IDL> oimage = obj_new('idlgrimage', image, transform_mode = 1)


Just the image alone:


IDL> xobjview, oimage


We want to apply this color image to our cone.  To do so, we need to generate a set of texture coordinates in normalized space that map to the "corners" of the cone, in X-Y space.


IDL> vertexx = reform(vertex[0, *])
IDL> vertexy = reform(vertex[1, *])
IDL> minvertexx = min(vertexx, max = maxvertexx)
IDL> minvertexy = min(vertexy, max = maxvertexy)
IDL> normalizedx = (vertexx - minvertexx)/(maxvertexx - minvertexx)
IDL> normalizedy = (vertexy - minvertexy)/(maxvertexy - minvertexy)
IDL> texturecoords = transpose([[normalizedx], [normalizedy]])


Apply the image and texture map coordinates to the original polygon object, then take a look at the result graphically.


IDL> opoly->setproperty, texture_map = oimage, $
IDL>    texture_coord = texturecoords
IDL> xobjview, omodel


Wait a second!  Where did the cones go?!  Try rotating the display in xobjview.  The trick here is that we've made the entire texture map transparent initially.  The rotation action in xobjview temporarily disables that transparency.  Notice the black band in the image.  We'll shift the image during our animation so that band gives the illusion of the exhaust pulsing.


In our final animation we don't want to show the entire cone, just "the essence" of the cone. We'll use randomness in our texture map to create a new image where the majority of it is transparent, while adding in some white "sparks".


The pixels in our image are presently some gradient of yellow, plus the black band.  The following code adds white "sparks" to the image.


newimage = long(image) + randomn(seed, 4, 256, 256)
highblue = where(abs(newimage[2, *, *]) gt 2, nhighblue, $
    complement = lowblue, ncomplement = nlowblue)
if (nhighblue ne 0) then begin
    for i = 0, 2 do begin
        planeimage = reform(newimage[i, *, *], 256l*256)
        planeimage[highblue] = 255
        newimage[i, *, *] = reform(planeimage, 256, 256)
    endfor
endif


This code section turns off the blue channel for those pixels that aren't white, leaving yellow.


if (nlowblue ne 0) then begin
   planeimage = reform(newimage[2, *, *], 256l*256)
   planeimage[lowblue] = 0
   newimage[2, *, *] = reform(planeimage, 256, 256)
endif
newimage <= 255b 


Let's update our image object and view our progress, this time using a black background


IDL> oimage->setproperty, data = newimage
IDL>  xobjview, omodel, background = [0, 0, 0]


This is too regular.  There needs to be more chaos!  Tweak the vertices of the polygon a little.


IDL> newvertex = vertex + randomn(seed, 256, 256)*.2
IDL> opoly->setproperty, data = newvertex
IDL> xobjview, omodel, background = [0, 0, 0]


The next step is to animate, here over 512 frames.  We perform three different steps between frames.  We tweak the vertices of the polygon to provide even more randomness, we shift the texture map image "down" 10 pixels, and we rotate the model itself.


for i = 0, 511 do begin
    newvertex = vertex + randomn(seed, ndims[0], ndims[1])*.2
    opoly->setproperty, data = newvertex
    oimage->setproperty, data = newimage
    newimage = shift(newimage, 0, 0, 10)
    xobjview_rotate, [1, 0, 1], 1
endfor


Execute the routine from the top-most section of code to see the result.  There are a couple other tweaks in the full example, not shown in the descriptive text, that add more randomness to the animation.


Remember we only have one "real" cone with one "real" image applied to it, but by aliasing, animating, and randomizing, we give the illusion that we have both motion and multiple, unique objects in our view.


SIGN UP AND STAY INFORMED

Sign up to receive the latest news, events, technologies and special offers.

SIGN ME UP