- Joined
- Nov 11, 2004
- Messages
- 1,986
Ribbon Emitters from a mesh perspective
by BlinkBoy//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:
Now their parameters seen from 3ds max’s view:
(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).
That’s pretty much the idea. Now let’s look at the unwrap when the rows and cols are both 1:
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:
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
Last edited: