• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

Ribbon Emitters from a mesh perspective

Status
Not open for further replies.

Ribbon Emitters from a mesh perspective

by BlinkBoy
128662d1377004291-view-3d-ribbonemitters.jpg


//for people wanting to know everything, even the way their are rendered.

Ribbon Emitters are a kind of emitter that generate ribbon effects, like long meshes that simulate a ribbon moving along a path. They are used from effects, to missiles, to producing movement effects on weapons; they are very practical for adding that final touches to your model. So how do they work? A ribbon emitter, emits “edges” . All these edges are then connected in one big mesh. This mesh’s unwrap goes from the end of the texture to the start of it. A ribbon emitter, only emits edges when it is visible. Here’s a simple that illustrates how they work:
attachment.php
attachment.php
attachment.php


Now their parameters seen from 3ds max’s view:

attachment.php


(The ones with a blue dot are animateable, vertex color is also animateable)
The x represents the center, the right line is the above and left line is the below. Here’s what each parameter means:
  • Above: defines the +y expansion of the generated edge.
  • Below: defines the –y expansion of the generated edge.
  • Edges/Sec: defines how many edges are emitted per second.
  • Edge Life: defines how long (in time) an edge will stay alive.
  • Gravity: defines the –g gravity which affects the edge’s movement along the z axis. (free fall physics)
  • Texture Rows: defines how is the unwrap divided along the v axis.
  • Texture Cols: defines how is the unwrap divided along the u axis.
  • Texture slot: defines which is the current quarter of unwrap which is used.
  • Vertex Color: defines the coloring of the ribbon generated mesh applied as a vertex shader over the material.
  • Vertex Alpha: defines the alpha of the generated mesh.
  • Material: The material associated with the ribbon emitter. (just like any other wc3 material for a geoset).

Now let’s look at how the mesh is generated. (the shown mesh is the mesh which should appear at last frame, but it is to give you an idea).

attachment.php


That’s pretty much the idea. Now let’s look at the unwrap when the rows and cols are both 1:

attachment.php


Notice that the last edges are sent to the end of the unwrap, they keep moving along, but no need to worry, we only need to compute the unwrap once for each quarter.
In this example, the number of rows is 2 and cols 1:

attachment.php


If we wanted to make it change unwrap, we just animate the texture slot. At 0, it will use the first quarter, at 1 it will use the next section.

Rendering Ribbon Emitters


-- for programmers.

Rendering ribbon emitters is not really that hard, it’s very similar to how we manage particle emitters. The difference is that instead of Particles we use edges.
The next code is by no means optimized and covers all of the characteristics. It’s also pseudo code from a combination of C, C++ and java syntax. You should only use it to have a general idea, it has not been tested or used, it’s just to have an idea.
Structuring should be similar to this:

C:
        struct Edge {
            float health; //0 to 1.0 based
            vector3 above;
            vector3 below;
            vector3 position;
        };
        
        struct ribbonEmitter {
            // blah blah
            Queue<Edge> aliveEdges;
            int maxEdges; //edges_persec*life
            float accumEmissionPower; //starts on 0
            //accumEmissionPower + (edges_per_sec)*(passed time)
            vector2[][] tvertices; // first dimension -> slot, second dimension -> [u,v] positions
            //sizeof tvertices = cols*rows*maxEdges*(sizeof vector2)
            //more prebaking if needed.
            //blah blah
        };

Now how to render them. Simple, we start with an empty Queue of edges and we add edges as we emit them. Everytime we have to render, we damage our edges, we remove the dead ones and we add new if needed, then we create the ribbon mesh based on them.

The code you'll see here is made generic, it lacks serious optimizations but will give the general idea:
C:
        //Vector3 rotate function
        void rotate(Quaternion* q) {
            Quaternion q2 = new Quaternion(this.x,this.y,this.z,0.0);
            Quaternion qInv = q->copy();
            qInv.inverse();
            // q2 = q*q2*(q.inverse())
            q2.multiply(q,2); //as second parameter since it's not commutative
            q2.multiple(qInv,1);
            // this should be simplified for optimizaton reasons.
            this.x = q2.x;
            this.y = q2.y;
            this.z = q2.z;
            free(qInv);
            free(q2);
            //ehm wrong syntax here, but u get the idea...
        }
        
        Edge* createEdge
        (
            vector3* position, vector3* rotation, 
            float above, float below, float startHealth
        ) 
        {
            Edge* e = newEdge(); //or malloc, new or whatever you use
            e->health = startHealth;
            e->position = position;
            e->above = newVector3(0.0,above,0.0);
            e->above->rotate(rotation);
            e->below = newVector3(0.0,-below,0.0);
            e->below->rotate(rotation);
            // you can also apply the emitter's transformation matrix to both vectors.
            return e;
        }
        
        void renderRibbonEmitter(ribbonEmitter* emitter) {
            float t = getTimePassed() //whatever you use to get time since last render
            for (e : emitter->aliveEdges) { //for each alive edge
                //shoot at it and decrease it's health according to time passed
                //if health is relative based 0 to 1 then 
                // the decrease could be (time_passed/life)
            }
            while ((emitter->aliveEdges->top())->health <= 0.0) {
                Edge* e = emitter->aliveEdges->drop();
                kill(e); //free, delete, whatever
            }
            if (emitter->IsVisible(currentTime)) {
                float lastTime = getlastRenderTime();
                float currentTime = getCurrentTime();
                float power = emitter->accumEmissionPower + \
                              (emitter->edges_per_sec*t);
                int new_edges = (int) power;
                emitter->accumEmissionPower = power - (float)new_edges;
                for (int i = new_edges;i != 0; i--) {
                    //We must create the edge when it was supossed to be created
                    float creationTime = (power - i)/t; //relative time
                    //The interpolation will depend on your software as 
                    //it should depend on bezier, hermite and linear, 
                    // for the sake of example I do linear.
                    // You can also just bake them after loading for each animation where the emitter
                    // will be visible, this will save you processing time at cost of memory.
                    float newHealth = 1.0 - t*life*creationTime;
                    // this should be calculated in local context of that time, using it's transformation matrix
                    // that's why baking is important.
                    vector3* position = emitter->position->Interpolate(lastTime,currentTime,creationTime);
                    quaternion* rot = emitter->rotation->Interpolate(lastTime,currentTime,creationTime);
                    float above = emitter->above->Interpolate(lastTime,currentTime,creationTime);
                    float below = emitter->below->Interpolate(lastTime,currentTime,creationTime);
                    emitter->aliveEdges.add(createEdge(position,rot,above,below,newHealth));
                    free(rot);
                    free(position);
                }
            }
            else emitter->accumEmissionPower = 0.0;
            renderRibbonMesh(mitter->aliveEdges,emitter);
            // just want to show you what you'll need 
            // renderRibbonMesh(emitter->aliveEdges,emitter->tvertices,
                             // emitter->getAlpha(currentTime),
                             // emitter->getVertexColor(currentTime),
                             // emitter->getTextureSlot(currentTime),
                             // emitter->materialId);
        }

renderRibbonMesh should generate the opengl triangles, apply materials, shaders and whats' left. The tvertices should be generated at start. Also recalculating position, rotation, above, below to be exact, could be optimized by either baking it after load into some data array or assume, only one edge will be added at the time the new frame buffer must be rendered.

Each edge generates 2 vertices, (position + above), (position + below). The final vertices of the emitter will generate in the same order as the queue goes. The number of quads or faces (whatever you decide) is number of edges-1. You create a quad between every edge.
 

Attachments

  • ribbonDescription.jpg
    ribbonDescription.jpg
    67.7 KB · Views: 709
  • RibbonUnwrap.jpg
    RibbonUnwrap.jpg
    198.6 KB · Views: 749
  • RibbonUnwrap2.jpg
    RibbonUnwrap2.jpg
    242.4 KB · Views: 718
  • RibbonMove.gif
    RibbonMove.gif
    12 KB · Views: 731
  • ribb.jpg
    ribb.jpg
    4.9 KB · Views: 653
  • ribb2.jpg
    ribb2.jpg
    5.2 KB · Views: 787
  • ribb3.jpg
    ribb3.jpg
    5.2 KB · Views: 705
Last edited:
Level 29
Joined
Jul 29, 2007
Messages
5,174
The fact that a material is referenced rather than a texture means you can do anything a geoset can? e.g. multiple layers, team color, texture animations, different filters, and so on?

The only documented key framed values for ribbons are KRVS, KRHA and KRHB (visibility, above, below).
Your pseudo code and images suggest there are more values, can you give me their names?
In fact, do you have a list of all key frame names? the documentations are pretty lacking.

Added a WIP to the model viewer.
 
Here's some: Some Chunk flags

Here's the rest of ribbon emitters tags
Code:
			writeMDXAnimateableParameter sub.heightAbove 0.0 "KRHA" animations writeMDXFloat
			writeMDXAnimateableParameter sub.heightBelow 0.0 "KRHB" animations writeMDXFloat
			writeMDXAnimateableParameter sub.alpha 0.0 "KRAL" animations writeMDXFloat
			writeMDXAnimateableParameter sub.color [255,255,255] "KRCO" animations writeMDXColor
			writeMDXAnimateableParameter sub.textureSlot 0 "KRTX" animations writeMDXLong

and if you wanna catch them all:
Code:
local tag_MDLX = 0x584c444d
local tag_VERS = 0x53524556
local tag_MODL = 0x4c444f4d
local tag_SEQS = 0x53514553
local tag_GLBS = 0x53424c47
local tag_TEXS = 0x53584554
local tag_LAYS = 0x5359414c
local tag_KMTF = 0x46544d4b
local tag_KMTA = 0x41544d4b
local tag_MTLS = 0x534c544d
local tag_SNDS = 0x53444e53
local tag_TXAN = 0x4e415854
local tag_KTAT = 0x5441544b
local tag_KTAR = 0x5241544b
local tag_KTAS = 0x5341544b
local tag_GEOS = 0x534f4547
local tag_VRTX = 0x58545256
local tag_NRMS = 0x534d524e
local tag_PTYP = 0x50595450
local tag_PCNT = 0x544e4350
local tag_PVTX = 0x58545650
local tag_GNDX = 0x58444e47
local tag_MTGC = 0x4347544d
local tag_MATS = 0x5354414d
local tag_UVAS = 0x53415655
local tag_UVBS = 0x53425655
local tag_GEOA = 0x414f4547
local tag_KGAO = 0x4f41474b
local tag_KGAC = 0x4341474b
local tag_KGTR = 0x5254474b
local tag_KGRT = 0x5452474b
local tag_KGSC = 0x4353474b
local tag_BONE = 0x454e4f42
local tag_LITE = 0x4554494c
local tag_KLAS = 0x53414c4b
local tag_KLAE = 0x45414c4b
local tag_KLAC = 0x43414c4b
local tag_KLAI = 0x49414c4b
local tag_KLBI = 0x49424c4b
local tag_KLBC = 0x43424c4b
local tag_KLAV = 0x56414c4b
local tag_HELP = 0x504c4548
local tag_ATCH = 0x48435441
local tag_KATV = 0x5654414b
local tag_PIVT = 0x54564950
local tag_PREM = 0x4d455250
local tag_KPEE = 0x4545504b
local tag_KPEG = 0x4745504b
local tag_KPLN = 0x4e4c504b
local tag_KPEL = 0x4c45504b
local tag_KPES = 0x5345504b
local tag_KPEV = 0x5645504b
local tag_PRE2 = 0x32455250
local tag_KP2S = 0x5332504b
local tag_KP2R = 0x5232504b
local tag_KP2L = 0x4c32504b
local tag_KP2G = 0x4732504b
local tag_KP2E = 0x4532504b
local tag_KP2N = 0x4e32504b
local tag_KP2W = 0x5732504b
local tag_KP2V = 0x5632504b
local tag_SNEM = 0x4d454e53
local tag_KESK = 0x4b53454b
local tag_RIBB = 0x42424952
local tag_KRHA = 0x4148524b
local tag_KRHB = 0x4248524b
local tag_KRAL = 0x4c41524b
local tag_KRCO = 0x4f43524b
local tag_KRTX = 0x5854524b
local tag_KRVS = 0x5356524b
local tag_EVTS = 0x53545645
local tag_KEVT = 0x5456454b
local tag_CAMS = 0x534d4143
local tag_KCTR = 0x5254434b
local tag_KTTR = 0x5254544b
local tag_CLID = 0x44494c43
 
Status
Not open for further replies.
Top