(Yet another?) motion trail scene node.

Post those lines of code you feel like sharing or find what you require for your project here; or simply use them as tutorials.
Post Reply
Mel
Competition winner
Posts: 2292
Joined: Wed May 07, 2008 11:40 am
Location: Granada, Spain

(Yet another?) motion trail scene node.

Post by Mel »

Another motion trail node. This updates with the position of a parent node, has several options, and makes use of the meshbuffer hardware mapping hints.

Image

I have called it "ghost trails", because "ghost" is a term used in animation, it is used when the previous frames of an animation are drawn over the current frame, this is used to check the correctness of the motions.

ghostTrailSceneNode.h

Code: Select all

 
#ifndef _GHOSTRAILSCENENODE_H_
#define _GHOSTRAILSCENENODE_H_
 
#include <irrlicht.h>
 
using namespace irr;
 
/**
This class creates a motion or ghost, in the sense of the animation term "ghosting", trail.
It maybe used for many types of effects, like, but not reduced to, motion trails, moving ribbons, flags, and others
 
Requires a parent scene node so it can follow it, and updates its mesh with the positions of this node.
Has several alignment options, suited for many situations, but just in the case a single scene node isn't enough
it can be specified a second node to have full control of how the trail behaves.
 
It has a single texture coordinate applied, and it doesn't support tangent space for now, though the normals for this node
may update if there is need for lighting, for instance.
 
It may animate on its own, or it can be specified to be animated on demand, for instance on a multiple rendering pass setup. (shadow maps)
 
It has a wind parameter designed to make the whole set of points move in the specified direction, if set. Though it is a bit too much sensitive
 
This node isn't affected by the rotations or scale operations, simply because it is an special effect, and depends on the parent node
to move. Not obstant, the position shifts the location of the effect. This shift is done in the parents space, not in global space, unless the
parent was the root scene node...
 
If there is no parent node, it doesn't render at all.
 
TODO: serialization options...?
*/
 
class ghostTrailSceneNode : public scene::ISceneNode
{
public:
 
    enum E_ALIGNMENT_MODE
        /*This enumerates all the modes in which the segments of the trail are updated. Note that the scope of this enumerator is*/
    {
        //Aligns segments with regard to the X axis (i.e. the "right" vector) of the camera
        EALM_CAMERA_X_AXIS, 
 
        //Aligns segments with regard to the Y axis (i.e. the "up" vector) of the camera
        EALM_CAMERA_Y_AXIS, 
 
        //Aligns segments with regard to the X world axis
        EALM_GLOBAL_X_AXIS, 
 
        //Aligns segments with regard to the Y world axis
        EALM_GLOBAL_Y_AXIS, 
 
        //Aligns segments with regard to the Z world axis
        EALM_GLOBAL_Z_AXIS, 
 
        //Aligns segments with regard to the X parent axis
        EALM_PARENT_X_AXIS, 
 
        //Aligns segments with regard to the Y parent axis
        EALM_PARENT_Y_AXIS, 
 
        //Aligns segments with regard to the Z parent axis
        EALM_PARENT_Z_AXIS, 
 
        //32 bits enum
        EALM_COUNT = 0x7FFFFFF
    };
 
private:
    //number of segments of the scene node
    s32 numberOfSegments;
 
    //width of the trail
    f32 trailWidth;
 
    //secondary node, for alignment purposes
    scene::ISceneNode* secondaryNode;
 
    //alignment mode.
    E_ALIGNMENT_MODE mode;
 
    //wind strength
    core::vector3df wind;
 
    //Internal variable to handle the circular stack of the trail
    s32 startingPoint;
 
    //The circular stack of the trail
    core::vector3df* pointBuffer;
    
    //the material of the trail
    video::SMaterial material;
 
    //The bounding box of the trail
    core::aabbox3df boundingBox;
 
    //the mesh of the trail
    scene::SMesh* trailMesh;
 
    //the meshBuffer of the trail
    scene::SMeshBuffer* trailMeshBuffer;
 
    //should normals be updated also?
    bool updateNormals;
 
    //should this node animate itself?
    bool autoAnimate;
 
public:
//constructor.
 
    ghostTrailSceneNode(scene::ISceneNode* parent, scene::ISceneManager* smgr, s32 id = -1, f32 trailWidth = 10.0f, s32 numberSegments = 64);
    
//destructor.
 
    ~ghostTrailSceneNode();
 
//gets the material of the trail.
 
    virtual video::SMaterial& getMaterial(u32 index);
 
//there is only 1 material, which is returned here.
 
    virtual u32 getMaterialCount() const;
 
//gets the bounding box of this node.
 
    virtual const core::aabbox3df& getBoundingBox() const;
    
//performs the onRegister callback.
 
    virtual void OnRegisterSceneNode();
 
//Performs an OnAnimate callback.
 
    virtual void OnAnimate(u32 timeMs);
 
//Renders the node.
 
    virtual void render();
 
//This method sets the current width of the trail, so the trail width's varies with the time.
 
    void setTrailWidth(f32 width = 10.0);
 
//This method provides a secondary node to the trail, so the trail is calculated using the position of the parent of this node and the position
//of this node.
 
    void setSecondaryNode(ISceneNode* n)
    {
        secondaryNode = n;
    }
 
//This method specifies how the trail segments will be aligned. It has no effect if a secondary node is provided, as the segments will align
//with regard to the parent and the secondary node. It may change dynamically.
 
    void setAlignmentMode(E_ALIGNMENT_MODE m)
    {
        mode = m;
    }
 
//This method makes the trail update on its own or not. It is useful, for instance, if the node is rendered in a multipass setup
//Also, the node is stored dynamically on the hardware, so if this node isn't updated, the data on the card remains static.
 
    void setAutoAnimate(bool animate)
    {
        autoAnimate = animate;
    }
 
//This method sets the direction of the wind on this trail. It is very sensitive, so it must be handled with care.
 
    void setWind(core::vector3df w = core::vector3df(0,-0.10f,0))
    {
        wind = w;
    }
 
//This method updates the normals of the trail, for lighting purposes. If it is not set, it only animates the positions of the trail
    void setUpdateNormals(bool update = false)
    {
        updateNormals = update;
    }
 
//This method forces the update of the trail. use with caution, as it may provide odd results if it is used too many times in a single render. 
//Generally, it should be used only once per render.
 
    void animate();
 
//This method serves to identify this kind of scene node, if this can have any use...
 
    scene::ESCENE_NODE_TYPE getType() const
    {
        return (scene::ESCENE_NODE_TYPE)MAKE_IRR_ID('t','a','i','l');
    }
 
private:
    u32 currentTime;
 
    u32 lastTime;
 
    void updateTrailData();
 
    void transferTrailData();
};
 
#endif
ghostTrailSceneNode.cpp

Code: Select all

 
#include "ghostTrailSceneNode.h"
 
ghostTrailSceneNode::ghostTrailSceneNode(scene::ISceneNode* parent, scene::ISceneManager* smgr, s32 id, f32 TWidth, s32 numberSegments)
:scene::ISceneNode(parent,smgr,id)
{
    trailMesh = new scene::SMesh();
    trailMeshBuffer = new scene::SMeshBuffer();
    trailMesh->addMeshBuffer(trailMeshBuffer);
    trailMeshBuffer->drop();
 
//number of vertices: as many segments plus 2, multiplied by 2; one initial, one final, n between
//number of indices: as many segments plus 1, multiplied by 6; 2 triangles each segment.
 
    numberOfSegments = (numberSegments + 2)*2 < 65536 ? numberSegments : 32766; //max 65536 vertices! we use 16 bit indices.
    
    numberOfSegments < 0 ? numberOfSegments = 2 : 0; //faster than an IF :)
 
    video::S3DVertex* vertexData = new video::S3DVertex[2*(numberOfSegments+2)];
    u16* indexData = new u16[6*(numberOfSegments+1)];
 
    //We have to startup the mapping coordinates...
 
    for(u32 i=0;i<numberOfSegments+2;i++)
    {
        f32 uData = i/(f32)(numberOfSegments+1);
 
        vertexData[2*i+0].TCoords = core::vector2df(uData,0.0f);
        vertexData[2*i+0].Color = video::SColor(255,255,255,255);
        vertexData[2*i+1].TCoords = core::vector2df(uData,1.0f);
        vertexData[2*i+1].Color = video::SColor(255,255,255,255);
    }
 
 
/*
    Now, we have to sew the indices...
 
    0-2
    |/| if 0 segments. indices: 0,1,2,2,1,3
    1-3
 
    0-2-4
    |/|/| if 1 segment; indices 0,1,2,2,1,3 ,2,3,4,4,3,5
    1-3-5
 
    0-2-4-6
    |/|/|/| if 2 segments; indices 0,1,2,2,1,3 ,2,3,4,4,3,5 ,4,5,6,6,5,7
    1-3-5-7
 
 
    if N segments... indices ... 2i, 2i+1, 2i+2, 2i+2, 2i+1, 2i+3 ...
*/
 
    for(u32 i=0;i<numberOfSegments+1;i++)
    {
        indexData[6*i+0] = 2*i+0;
        indexData[6*i+1] = 2*i+1;
        indexData[6*i+2] = 2*i+2;
        indexData[6*i+3] = 2*i+2;
        indexData[6*i+4] = 2*i+1;
        indexData[6*i+5] = 2*i+3;
    }
 
    //Finally, we add this soup of triangles to the mesh buffer
    trailMeshBuffer->append(vertexData,2*(numberOfSegments+2),indexData,6*(numberOfSegments+1));
 
    startingPoint = 0;//It must be an in range index!
 
    pointBuffer = new core::vector3df[2*(numberOfSegments+2)];
 
    //Having a single strip of triangles creates somewhat odd effects when this isn't set, so we help the 
    //user by setting this for him. Anyway, he can unset it again later if he wishes...
 
    for(u32 i=0;i<_IRR_MATERIAL_MAX_TEXTURES_;i++)
    {
        material.TextureLayer[i].TextureWrapU = material.TextureLayer[i].TextureWrapV = video::ETC_CLAMP_TO_EDGE;
    }
    material.BackfaceCulling = false;
 
    trailWidth = TWidth;
 
    updateNormals = false;//Just in case, this trail won't update its normals unless specified otherwise.
    
    autoAnimate = true;//If we have a multiple pass setup, maybe we DON'T want to let this happen...
    
    secondaryNode = 0;//for the moment, only the parent
    
    wind = core::vector3df(0,0,0);//no wind.
    
    mode = EALM_GLOBAL_Y_AXIS;//default, aligned with the Y world axis.
    
    trailMesh->setHardwareMappingHint(scene::EHM_DYNAMIC);//It may change, or it may not, still, the user may alter it...
}
 
ghostTrailSceneNode::~ghostTrailSceneNode()
{
    trailMesh->drop();
    delete pointBuffer;
}
 
video::SMaterial& ghostTrailSceneNode::getMaterial(u32 index)
{
    return material;
}
 
u32 ghostTrailSceneNode::getMaterialCount() const
{
    return 1;
}
 
const core::aabbox3df& ghostTrailSceneNode::getBoundingBox() const
{
    return boundingBox;
}
 
void ghostTrailSceneNode::OnRegisterSceneNode()
{
    if(getParent()){ //If there is no parent node, there is no need for rendering
        if(isVisible()){
            getSceneManager()->registerNodeForRendering(this);      
        }
        scene::ISceneNode::OnRegisterSceneNode();
    }
}
 
void ghostTrailSceneNode::OnAnimate(u32 timeMs)
{
    lastTime = currentTime;
    currentTime = timeMs;
 
    if(autoAnimate)
    {
        animate();
    }
    scene::ISceneNode::OnAnimate(timeMs);
}
 
void ghostTrailSceneNode::render()
{
    scene::ISceneManager* smgr = getSceneManager(); 
    video::IVideoDriver* drv = smgr->getVideoDriver(); 
    scene::ICameraSceneNode* cam = smgr->getActiveCamera();
 
    drv->setMaterial(material);
 
    drv->setTransform(video::ETS_WORLD,core::matrix4().makeIdentity());
    drv->setTransform(video::ETS_VIEW,cam->getViewMatrix());
    drv->setTransform(video::ETS_PROJECTION,cam->getProjectionMatrix());
 
    drv->drawMeshBuffer(trailMeshBuffer);
}
 
void ghostTrailSceneNode::setTrailWidth(f32 width)
{
    trailWidth = width; 
    //variable trail size, but only for the last pair of points,
    //this allows, for instance, for the matrix bullet trails effect :)
}
 
void ghostTrailSceneNode::updateTrailData()
{
    core::matrix4 parentMatrix = getParent()->getAbsoluteTransformation();
    core::vector3df alignmentVector,viewVector,upVector;
    core::vector3df parentPosition;
    parentMatrix.transformVect(parentPosition,getPosition()); //Posicion local
    core::vector3df secondaryPosition;
    s32 p;
 
    startingPoint < 0 ? startingPoint = numberOfSegments+1 : 0;
 
    if(!secondaryNode)
    {
        switch(mode){
            case EALM_CAMERA_X_AXIS:
                upVector = getSceneManager()->getActiveCamera()->getUpVector();
                upVector.normalize();
 
                viewVector = getSceneManager()->getActiveCamera()->getTarget() - getSceneManager()->getActiveCamera()->getAbsolutePosition();
                viewVector.normalize();
 
                alignmentVector = viewVector.crossProduct(upVector);
                alignmentVector.normalize();
                break;
 
            case EALM_CAMERA_Y_AXIS:
                alignmentVector = getSceneManager()->getActiveCamera()->getUpVector();
                alignmentVector.normalize();
                break;
 
            case EALM_GLOBAL_X_AXIS:
                alignmentVector = core::vector3df(1.0f,0,0);
                break;
 
            case EALM_GLOBAL_Y_AXIS:
                alignmentVector = core::vector3df(0,1.0f,0);
                break;
 
            case EALM_GLOBAL_Z_AXIS:
                alignmentVector = core::vector3df(0,0,1.0f);
                break;
 
            case EALM_PARENT_X_AXIS:
                parentMatrix.setTranslation(core::vector3df());//absolute transforms include a translation, hence we need this
                parentMatrix.transformVect(alignmentVector,core::vector3df(1,0,0));
                alignmentVector.normalize();
                break;
 
            case EALM_PARENT_Y_AXIS:
                parentMatrix.setTranslation(core::vector3df());
                parentMatrix.transformVect(alignmentVector,core::vector3df(0,1,0));
                alignmentVector.normalize();
                break;
 
            case EALM_PARENT_Z_AXIS:
                parentMatrix.setTranslation(core::vector3df());
                parentMatrix.transformVect(alignmentVector,core::vector3df(0,0,1));
                alignmentVector.normalize();
                break;
        }
        pointBuffer[2*startingPoint+0] = parentPosition-alignmentVector*trailWidth/2.0f;
        pointBuffer[2*startingPoint+1] = parentPosition+alignmentVector*trailWidth/2.0f;
    }
    else
    {//There is no need for alignment options with a secondary node
        parentPosition = getParent()->getAbsolutePosition();
        secondaryNode->updateAbsolutePosition();
        secondaryPosition = secondaryNode->getAbsolutePosition();
 
        pointBuffer[2*startingPoint+0] = parentPosition;
        pointBuffer[2*startingPoint+1] = secondaryPosition;
    }
 
    //Wind update... It is okay, in the end all the data is transferred from the app to the video card.
    //The good thing is that this affects all the points at the same time.
    if(wind.getLengthSQ() > 0.001f)
    {
        u32 timeIncrement = currentTime - lastTime;
 
        for(u32 i=0;i<numberOfSegments+2;i++)
        {
            s32 windStrength = i<startingPoint ? numberOfSegments+2-startingPoint+i : i-startingPoint;
            pointBuffer[2*i+0] += wind*(f32)windStrength/(f32)(numberOfSegments+2);
            pointBuffer[2*i+1] += wind*(f32)windStrength/(f32)(numberOfSegments+2);
        }
    }
 
    transferTrailData();
 
    startingPoint --;
}
 
void ghostTrailSceneNode::animate()
{
    if(getParent())
    {
        updateTrailData();
    }
}
 
void ghostTrailSceneNode::transferTrailData()
{
    video::S3DVertex* vertices = (video::S3DVertex*)trailMeshBuffer->getVertices();
    core::vector3df normal,vectorU,vectorV;
 
    boundingBox.reset(0,0,0);
    trailMeshBuffer->BoundingBox.reset(0,0,0);
    trailMesh->BoundingBox.reset(0,0,0);
 
    for(u32 i=0;i<numberOfSegments+2;i++)
    {
        s32 vertexIndex = i<startingPoint ? numberOfSegments+2-startingPoint+i : i-startingPoint;
        vertices[2*vertexIndex+0].Pos = pointBuffer[2*i+0];
        vertices[2*vertexIndex+1].Pos = pointBuffer[2*i+1];
        boundingBox.addInternalBox(core::aabbox3df(vertices[2*vertexIndex+0].Pos,vertices[2*vertexIndex+1].Pos));
    }
 
    trailMeshBuffer->BoundingBox.addInternalBox(boundingBox);
    trailMesh->BoundingBox.addInternalBox(boundingBox);
 
    if(updateNormals)
    {
        for(u32 i=0; i<numberOfSegments+1;i++)
        {
            vectorU = vertices[2*i+2].Pos - vertices[2*i].Pos;
            vectorV = vertices[2*i+1].Pos - vertices[2*i+2].Pos;
            normal = vectorU.crossProduct(vectorV);
            normal.normalize();
            vertices[2*i].Normal = normal;
            vertices[2*i+1].Normal = normal;
        }
 
        vertices[2*(numberOfSegments+1)+0].Normal = normal;
        vertices[2*(numberOfSegments+1)+1].Normal = normal;
    }
 
    trailMeshBuffer->setDirty(scene::EBT_VERTEX);
}
 
"There is nothing truly useless, it always serves as a bad example". Arthur A. Schmitt
chronologicaldot
Competition winner
Posts: 685
Joined: Mon Sep 10, 2012 8:51 am

Re: (Yet another?) motion trail scene node.

Post by chronologicaldot »

Sweet looking! Mind if I use it?

Also - Is the image from a game you're making or is that just a test world?

EDIT: Quick question: What's the difference between the node animating itself and not animating itself? Why would we want the latter?
Cube_
Posts: 1010
Joined: Mon Oct 24, 2011 10:03 pm
Location: 0x45 61 72 74 68 2c 20 69 6e 20 74 68 65 20 73 6f 6c 20 73 79 73 74 65 6d

Re: (Yet another?) motion trail scene node.

Post by Cube_ »

Very nice!
"this is not the bottleneck you are looking for"
Mel
Competition winner
Posts: 2292
Joined: Wed May 07, 2008 11:40 am
Location: Granada, Spain

Re: (Yet another?) motion trail scene node.

Post by Mel »

Because if you let the node animate itself each time you make a drawAll() call, the trail adds a segment based on the current position on the parent and discards the last one. This has no effect if the render is performed only once, but for scene setups with more than one render pass (shadowmaps, fur effects, etc...), this may be problematic, and thus, with this, the update can be performed when needed.

Yes, the image is from a game i am doing.
"There is nothing truly useless, it always serves as a bad example". Arthur A. Schmitt
fmx

Re: (Yet another?) motion trail scene node.

Post by fmx »

Nice! Reminds me of soul calibur
I can use this 8)
smso
Posts: 246
Joined: Fri Jun 04, 2010 3:28 pm
Location: Hong Kong

Re: (Yet another?) motion trail scene node.

Post by smso »

To make the color fade towards the end of the trail, I change some lines in the ctor of GhostTrailSceneNode:

Code: Select all

    //to make color fade towards the end of the trail
    u32 alpha = 0;
    
    for(u32 i=0;i<numberOfSegments+2;i++)
    {
        f32 uData = i/(f32)(numberOfSegments+1);
        alpha = 255-u32(i*255/f32(numberOfSegments+1));
        //printf("i=%i => alpha=%i\n", i, alpha);
        
        vertexData[2*i+0].TCoords = core::vector2df(uData,0.0f);
        //vertexData[2*i+0].Color = video::SColor(32,0,0,255);
        vertexData[2*i+0].Color = video::SColor(alpha,0,0,255);
        
        vertexData[2*i+1].TCoords = core::vector2df(uData,1.0f);
        //vertexData[2*i+1].Color = video::SColor(32,0,0,255);
        vertexData[2*i+1].Color = video::SColor(alpha,0,0,255);
    }
 

Thanks for the snippets.
Regards,
smso
Mel
Competition winner
Posts: 2292
Joined: Wed May 07, 2008 11:40 am
Location: Granada, Spain

Re: (Yet another?) motion trail scene node.

Post by Mel »

I just use textures to modulate the alpha value, and let the material do the rest, but that is a nice addition! :)

By the way, why don't you do alpha = ceil(255-uData*255)?

uData ranges from 0 to 1 as the segments get closer to the end of the trail
"There is nothing truly useless, it always serves as a bad example". Arthur A. Schmitt
smso
Posts: 246
Joined: Fri Jun 04, 2010 3:28 pm
Location: Hong Kong

Re: (Yet another?) motion trail scene node.

Post by smso »

Mel wrote:I just use textures to modulate the alpha value, and let the material do the rest, but that is a nice addition! :)

By the way, why don't you do alpha = ceil(255-uData*255)?

uData ranges from 0 to 1 as the segments get closer to the end of the trail
@Mel
You are right. My code is not optimized.

Regards,
smso
smso
Posts: 246
Joined: Fri Jun 04, 2010 3:28 pm
Location: Hong Kong

Re: (Yet another?) motion trail scene node.

Post by smso »

If texture is used, place the following in the ctor:

Code: Select all

        material.BackfaceCulling = false;
    material.MaterialType = video::EMT_ONETEXTURE_BLEND;
    material.setFlag(video::EMF_LIGHTING, false);
    material.setTexture(0, smgr->getVideoDriver()->getTexture("beamside.png"));
    //material.setTexture(0, smgr->getVideoDriver()->getTexture("media/wall.bmp"));
    
    //FIXME
    //material.BlendOperation = video::EBO_ADD;
    material.MaterialTypeParam = irr::video::pack_textureBlendFunc
    (
        video::EBF_SRC_ALPHA,
        video::EBF_ONE_MINUS_SRC_ALPHA,
        video::EMFN_MODULATE_1X,
        video::EAS_VERTEX_COLOR | video::EAS_TEXTURE
    );
 
Regards,
smso
Mel
Competition winner
Posts: 2292
Joined: Wed May 07, 2008 11:40 am
Location: Granada, Spain

Re: (Yet another?) motion trail scene node.

Post by Mel »

There is no need really, if something in the materials doesn't meet your needs, you always can access the material and change it.
"There is nothing truly useless, it always serves as a bad example". Arthur A. Schmitt
Post Reply