BloodRayne@Posted: Sat Dec 29, 2012 1:36 pm :
I've implemented a new feature into Grimm:Quest for the Gatherer's key that I'm very excited about. It's simply called 'spray painting' and what it does is allow for mappers to run their map and spray paint the world with decals. No more tedious placing of single plane decals to create static blood, but 'real' painting of the world with decals. I can now run a map, think 'I'd like to have some blood spray there', paint the world with a single click, issue the command 'saveDecals' when I am happy with the result and the paint will be there permanently in the map.

To make this happen I have exposed a new script command via the SDK that calls 'projectDecalOntoWorld'.
I use this routine in a very simple entity called 'grimm_spray' which has several advantages over the regular func_spray.
The first advantage is that I can now use random paint decals, allowing for randomly looking worlds (which is what Grimm is all about).
The second is that players can turn the feature off, meaning that they can remove these decals for performance reasons, something that's not possible in a static map.

There's no real way to describe it unless you see it in action, so I've created a nice video to show off the effect!

Click image to play video
Image

When painting mode is not set, the spray entities spawn during mapload and paint the world, after which they are automatically removed, this will leave only the paint decal.
Players can set paint density, to have either more or less paint in the world.

I'm pretty excited about this, I've managed to go over 6 maps in a matter of several hours and completely detailed them to taste with decals. This would have normally taken more than a week of careful placing of single decals by hand in the editor, with the added disadvantage of blood not spraying onto nearby objects and such.



Tron@Posted: Sun Dec 30, 2012 2:23 am :
Nice! How many of these decals can you throw around until it starts impacting performance?



bladeghost@Posted: Sun Dec 30, 2012 3:21 am :
That's very interesting, can the same be done with grass,rocks and dirt?
you have some great talent there rayne.
good stuff!



BNA!@Posted: Sun Dec 30, 2012 4:48 am :
Oh that's nice!



The Happy Friar@Posted: Sun Dec 30, 2012 4:56 am :
That's VERY cool. I always wondered why id didn't have that built in to D3. Was there any special hurdles you had to jump over to get this working?



motorsep@Posted: Sun Dec 30, 2012 7:22 am :
Aren't those decals dynamic? (meaning you can only have so many of those on the level)



BloodRayne@Posted: Sun Dec 30, 2012 10:33 am :
Tron wrote:
Nice! How many of these decals can you throw around until it starts impacting performance?

motorsep wrote:
Aren't those decals dynamic? (meaning you can only have so many of those on the level)

Numbers I'm not sure about, it's just a sprite so there's many. Performance starts to hurt when the decals overlap intersecting lights, so the challenge there is to minimize overlap, try to see them as lights and how lights hurt performance. So you quickly learn what works and what doesn't.
There is no hard set limit in my version of the engine.

bladeghost wrote:
That's very interesting, can the same be done with grass,rocks and dirt?
you have some great talent there rayne.
good stuff!

Any texture. But you can do random grass, rock and dirt model using the func_clutter entity that I made for Grimm which does random models. :)

The Happy Friar wrote:
That's VERY cool. I always wondered why id didn't have that built in to D3. Was there any special hurdles you had to jump over to get this working?

Yes, I'm working on making the decals save persistent, which they aren't now. When I quicksave/load the decals disappear, but that's easily fixable. Another hurdle was the angle. It just didn't want to spray in the correct angles, giving lots of stretches and other issues. I've manage to solve most of that but flukes still happen, in that case I simply undo the spray. The eventual code looks really simple, but it took me some time to get there..:)

Code is work in progress, still some changes and cleanouts to be done.

Code:
/* grimm --> spraydecal
================
idEntity::Event_SprayDecal
================
*/
void idEntity::Event_SprayDecal( idVec3 *spray_origin, const char *mtr_decal, idVec3 *spray_angle, float size ) {

   //srpay something
   idVec3 newangle = *spray_angle;   
   //gameLocal.Printf( "%s <--current vector\n", spray_angle->ToString() );
   
   if ( spray_angle->x <= -45 ) {
      newangle.z = 90;
   }
   if ( spray_angle->x >= 45 ) {
      newangle.z = -90;
   }

   gameLocal.ProjectDecal( *spray_origin, newangle, 256.0f, true, size, mtr_decal, 0, true );    
}


And I changed gameLocal.ProjectDecal somewhat:
Code:

/*
===============
idGameLocal::ProjectDecal
===============
*/
void idGameLocal::ProjectDecal( const idVec3 &origin, const idVec3 &dir, float depth, bool parallel, float size, const char *material, float angle, bool forever ) {
   float s, c;
   idMat3 axis, axistemp;
   idFixedWinding winding;
   idVec3 windingOrigin, projectionOrigin;

   static idVec3 decalWinding[4] = {
      idVec3(  1.0f,  1.0f, 0.0f ),
      idVec3( -1.0f,  1.0f, 0.0f ),
      idVec3( -1.0f, -1.0f, 0.0f ),
      idVec3(  1.0f, -1.0f, 0.0f )
   };

   if ( !g_decals.GetBool() ) {
      return;
   }

   // randomly rotate the decal winding
   idMath::SinCos16( ( angle ) ? angle : random.RandomFloat() * idMath::TWO_PI, s, c );

   // winding orientation
   axis[2] = dir;
   axis[2].Normalize();
   axis[2].NormalVectors( axistemp[0], axistemp[1] );
   axis[0] = axistemp[ 0 ] * c + axistemp[ 1 ] * -s;
   axis[1] = axistemp[ 0 ] * -s + axistemp[ 1 ] * -c;

   windingOrigin = origin + depth * axis[2];
   if ( parallel ) {
      projectionOrigin = origin - depth * axis[2];
   } else {
      projectionOrigin = origin;
   }

   size *= 0.5f;

   winding.Clear();
   winding += idVec5( windingOrigin + ( axis * decalWinding[0] ) * size, idVec2( 1, 1 ) );
   winding += idVec5( windingOrigin + ( axis * decalWinding[1] ) * size, idVec2( 0, 1 ) );
   winding += idVec5( windingOrigin + ( axis * decalWinding[2] ) * size, idVec2( 0, 0 ) );
   winding += idVec5( windingOrigin + ( axis * decalWinding[3] ) * size, idVec2( 1, 0 ) );
   if ( forever ) {
      //27 hrs = 'forever' in game time..
      //gameLocal.Printf( "Project decal forever\n");
      gameRenderWorld->ProjectDecalOntoWorld( winding, projectionOrigin, parallel, depth * 0.5f, declManager->FindMaterial( material ), time + SEC2MS( 999999 ) );
   } else {
      //gameLocal.Printf( "Project decal for decalStay time\n");
      gameRenderWorld->ProjectDecalOntoWorld( winding, projectionOrigin, parallel, depth * 0.5f, declManager->FindMaterial( material ), time + SEC2MS( g_decalStay.GetFloat() ) );
   }
   
}



7318@Posted: Sun Dec 30, 2012 12:05 pm :
that's awesome!
but with this you could easily surpass the entity limit, isn't it? we have to find a way to get a much bigger entity limit without affecting anything else, just have the capacity to have more stuff in the maps.



BloodRayne@Posted: Sun Dec 30, 2012 12:16 pm :
7318 wrote:
that's awesome!
but with this you could easily surpass the entity limit, isn't it? we have to find a way to get a much bigger entity limit without affecting anything else, just have the capacity to have more stuff in the maps.

I currently have a 8192 entity limit. There's around 20-30 spray entities per map so that's no more than about 300 polygons max and 30 entities that are removed upon mapload. I could make it so that it doesn't use entities, but I want them to be available for tweaking afterwards in the editor, where you can give them specific sizes and paint materials etc..



7318@Posted: Sun Dec 30, 2012 8:49 pm :
what did you do in order to increase the max entity limit?



bkt@Posted: Mon Dec 31, 2012 6:13 am :
What would be great would be if when you 'savedecals' it would write the decals to the mapfile as func_statics (or regular brushes), which you could then finetune in the editor.



BloodRayne@Posted: Mon Dec 31, 2012 8:31 am :
7318 wrote:
what did you do in order to increase the max entity limit?

Simply raised it (it's a variable gameLocal), I searched D3World for it actually, there's a post on it somewhere. :)

bkt wrote:
What would be great would be if when you 'savedecals' it would write the decals to the mapfile as func_statics (or regular brushes), which you could then finetune in the editor.

I actually didn't want to do that because I want to be able to quickly change the decals (and randomly change them).
Also, one spray can have multiple decals as it sprays on different objects at once.



motorsep@Posted: Mon Dec 31, 2012 8:36 am :
How would you do such decaling in DarkRadiant (so decals end up projected on any kind of curvature in-game) ?

Do those decals work the same way as blood / burn marks in-game? If so, I encountered huge fps drop when I shop my player's model to turn it black (~5 decals will kill performance).



BloodRayne@Posted: Mon Dec 31, 2012 8:41 am :
motorsep wrote:
How would you do such decaling in DarkRadiant (so decals end up projected on any kind of curvature in-game) ?

I don't use DarkRadiant so I have no idea, I guess the same as I would do them in DoomEdit, e.g. place a square and texture it?

Quote:
Do those decals work the same way as blood / burn marks in-game? If so, I encountered huge fps drop when I shop my player's model to turn it black (~5 decals will kill performance).

They use ProjectDecal and there are the same performance issues there as with regular burn marks. This is a mapper's tool, to be used by a mapper that has knowledge of where to place these decals. The performance issues come from overlapping lights, creating draws and depend on which texture you use. As long as you stick with 1-2 decals per surface performance is fine.



motorsep@Posted: Mon Dec 31, 2012 8:46 am :
Ahh, gotcha :( I thought you are using something else to project decals. That's why maybe idSoftware did use that method because it's limiting. I wonder how to resolve this issue with performance (make decals not interact with lights?).



BloodRayne@Posted: Mon Dec 31, 2012 8:50 am :
motorsep wrote:
Ahh, gotcha :( I thought you are using something else to project decals. That's why maybe idSoftware did use that method because it's limiting. I wonder how to resolve this issue with performance (make decals not interact with lights?).

I am developing Grimm in two phases.

Phase 1 consists only of source-code fixes that are SDK related. ProjectDecalOntoWorld is a function that's available from inside the GPL code base, Phase 2 will consist of all those changes I couldn't make via the SDK. It's my plan to look into this again in phase 2 and perhaps create my own routine for it. My knowledge concerning the codebase is growing by the day and I'm nearly at the point where I can start feeling comfortable in calling myself an 'idtech 4' programmer.. Not there yet.. but getting there!



motorsep@Posted: Mon Dec 31, 2012 8:55 am :
cool stuff!



jmarshall23@Posted: Mon Dec 31, 2012 9:02 am :
Very nice now just put it in the editor : ).



nbohr1more@Posted: Mon Dec 31, 2012 8:56 pm :
Dark Radiant has an auto decal feature:


http://wiki.thedarkmod.com/index.php?title=Decals

Quote:
To create decals, select the surface you want to make a decal for (in select face mode or with SHIFT+CTRL+Click the left mouse button). Press the “create Decals for selected Faces” button, it will create a patch for each selected face, which will be exactly in the same plane. Another way to create a decal is create patch yourself and place it were you want. Assign a texture to it from the Media browser, the texture browser or with the surface inspector.


It doesn't look as fun as this though...



motorsep@Posted: Mon Dec 31, 2012 11:59 pm :
How does it get projected though? On runtime ?



Serpentine@Posted: Tue Jan 01, 2013 7:39 am :
motorsep wrote:
How does it get projected though? On runtime ?

This question makes very little sense. wat.



BloodRayne@Posted: Tue Jan 01, 2013 7:46 am :
Serpentine wrote:
motorsep wrote:
How does it get projected though? On runtime ?

This question makes very little sense. wat.

I think he missed the part where I actually posed source code. :mrgreen:

nbohr1more wrote:
Dark Radiant has an auto decal feature:


http://wiki.thedarkmod.com/index.php?title=Decals

Quote:
To create decals, select the surface you want to make a decal for (in select face mode or with SHIFT+CTRL+Click the left mouse button). Press the “create Decals for selected Faces” button, it will create a patch for each selected face, which will be exactly in the same plane. Another way to create a decal is create patch yourself and place it were you want. Assign a texture to it from the Media browser, the texture browser or with the surface inspector.


It doesn't look as fun as this though...


This does seem like a robust way to handle things as well. But it makes things static and give you much less control than in my current setup, which is basically like hanging spraycans in the world which spray their stuff upon mapload and restore, no editing of any kind, no offsetting or texturing needed.



motorsep@Posted: Tue Jan 01, 2013 7:52 am :
No, I was asking about DarkRadiant's method. How does flat patch with decal material get projected onto the world?



nbohr1more@Posted: Tue Jan 01, 2013 6:02 pm :
The patches are created as any other patches would be. They are map geometry rather than
generated entities. (Though you could use the SEED system to generate a bunch of patch models
randomly if you wanted.) The only thing special is that you don't need to meticulously create and align
the patches (and move them into position) yourself.
You just select the surfaces and Dark Radiant creates the patches for you with the right size and alignment.

As far as entity usage. One trick that we tend to advise for decorative grime patches is to merge them into
a single func_static entity. You could also do this with BloodRayne's method since it looks like you are saving
the decals to the map via a console action. You'd just need to crack open the editor, find the decal patches and mark them
as part of the same func_static entity. Though it would kinda defeat the purpose of having an easy free-form
workflow...



BloodRayne@Posted: Tue Jan 01, 2013 6:45 pm :
nbohr1more wrote:
As far as entity usage. One trick that we tend to advise for decorative grime patches is to merge them into
a single func_static entity. You could also do this with BloodRayne's method since it looks like you are saving
the decals to the map via a console action. You'd just need to crack open the editor, find the decal patches and mark them
as part of the same func_static entity. Though it would kinda defeat the purpose of having an easy free-form
workflow...

Actually, they are func_static entities that project a decal, nothing more.
They do it upon map load/restore. (Meaning that if you do a 'reloadModels' or 'vid_restart' the decals disappear), but will fix that soon as well. There's nothing in the editor but these entities to see/edit. You can give their projected decal an angle, set a mtr_decal key (or have it choose a random decal for you) and set spray size. These entities don't create map geometry other than the decals needed.



nbohr1more@Posted: Tue Jan 01, 2013 8:53 pm :
OK. So they are like SEED entities or Particle emitters then.

This might come in handy for TDM after all... Especially since distributing SEED entities on anything other than
a gravity-based orientation has proven a challenge. If these emitters project spherically then this would solve
issues like spawning rocks or vegetation in a cylindrical cave area.



BloodRayne@Posted: Tue Jan 01, 2013 8:59 pm :
nbohr1more wrote:
OK. So they are like SEED entities or Particle emitters then.

This might come in handy for TDM after all... Especially since distributing SEED entities on anything other than
a gravity-based orientation has proven a challenge. If these emitters project spherically then this would solve
issues like spawning rocks or vegetation in a cylindrical cave area.

The engine can do that, look into ProjectDecalOntoWorld in RenderWorld.cpp and modelDecal.cpp.
Mind you, that's just a starting point, I haven't done anything with these. My changes were made solely in entity.cpp. :)

Code:
/*
================
idRenderWorldLocal::ProjectDecalOntoWorld
================
*/
void idRenderWorldLocal::ProjectDecalOntoWorld(const idFixedWinding &winding, const idVec3 &projectionOrigin, const bool parallel, const float fadeDepth, const idMaterial *material, const int startTime)
{
    int i, areas[10], numAreas;
    const areaReference_t *ref;
    const portalArea_t *area;
    const idRenderModel *model;
    idRenderEntityLocal *def;
    decalProjectionInfo_t info, localInfo;

    if (!idRenderModelDecal::CreateProjectionInfo(info, winding, projectionOrigin, parallel, fadeDepth, material, startTime))
    {
        return;
    }

    // get the world areas touched by the projection volume
    numAreas = BoundsInAreas(info.projectionBounds, areas, 10);

    // check all areas for models
    for (i = 0; i < numAreas; i++)
    {

        area = &portalAreas[ areas[i] ];

        // check all models in this area
        for (ref = area->entityRefs.areaNext; ref != &area->entityRefs; ref = ref->areaNext)
        {
            def = ref->entity;

            // completely ignore any dynamic or callback models
            model = def->parms.hModel;

            if (model == NULL || model->IsDynamicModel() != DM_STATIC || def->parms.callback)
            {
                continue;
            }

            if (def->parms.customShader != NULL && !def->parms.customShader->AllowOverlays())
            {
                continue;
            }

            idBounds bounds;
            bounds.FromTransformedBounds(model->Bounds(&def->parms), def->parms.origin, def->parms.axis);

            // if the model bounds do not overlap with the projection bounds
            if (!info.projectionBounds.IntersectsBounds(bounds))
            {
                continue;
            }

            // transform the bounding planes, fade planes and texture axis into local space
            idRenderModelDecal::GlobalProjectionInfoToLocal(localInfo, info, def->parms.origin, def->parms.axis);
            localInfo.force = (def->parms.customShader != NULL);

            if (!def->decals)
            {
                def->decals = idRenderModelDecal::Alloc();
            }

            def->decals->CreateDecal(model, localInfo);
        }
    }
}

/*
====================
idRenderWorldLocal::ProjectDecal
====================
*/
void idRenderWorldLocal::ProjectDecal(qhandle_t entityHandle, const idFixedWinding &winding, const idVec3 &projectionOrigin, const bool parallel, const float fadeDepth, const idMaterial *material, const int startTime)
{
    decalProjectionInfo_t info, localInfo;

    if (entityHandle < 0 || entityHandle >= entityDefs.Num())
    {
        common->Error("idRenderWorld::ProjectOverlay: index = %i", entityHandle);
        return;
    }

    idRenderEntityLocal   *def = entityDefs[ entityHandle ];

    if (!def)
    {
        return;
    }

    const idRenderModel *model = def->parms.hModel;

    if (model == NULL || model->IsDynamicModel() != DM_STATIC || def->parms.callback)
    {
        return;
    }

    if (!idRenderModelDecal::CreateProjectionInfo(info, winding, projectionOrigin, parallel, fadeDepth, material, startTime))
    {
        return;
    }

    idBounds bounds;
    bounds.FromTransformedBounds(model->Bounds(&def->parms), def->parms.origin, def->parms.axis);

    // if the model bounds do not overlap with the projection bounds
    if (!info.projectionBounds.IntersectsBounds(bounds))
    {
        return;
    }

    // transform the bounding planes, fade planes and texture axis into local space
    idRenderModelDecal::GlobalProjectionInfoToLocal(localInfo, info, def->parms.origin, def->parms.axis);
    localInfo.force = (def->parms.customShader != NULL);

    if (def->decals == NULL)
    {
        def->decals = idRenderModelDecal::Alloc();
    }

    def->decals->CreateDecal(model, localInfo);
}

/*
====================
idRenderWorldLocal::ProjectOverlay
====================
*/
void idRenderWorldLocal::ProjectOverlay(qhandle_t entityHandle, const idPlane localTextureAxis[2], const idMaterial *material)
{

    if (entityHandle < 0 || entityHandle >= entityDefs.Num())
    {
        common->Error("idRenderWorld::ProjectOverlay: index = %i", entityHandle);
        return;
    }

    idRenderEntityLocal   *def = entityDefs[ entityHandle ];

    if (!def)
    {
        return;
    }

    const renderEntity_t *refEnt = &def->parms;

    idRenderModel *model = refEnt->hModel;

    if (model->IsDynamicModel() != DM_CACHED)     // FIXME: probably should be MD5 only
    {
        return;
    }

    model = R_EntityDefDynamicModel(def);

    if (def->overlay == NULL)
    {
        def->overlay = idRenderModelOverlay::Alloc();
    }

    def->overlay->CreateOverlay(model, localTextureAxis, material);
}


And ModelDecl.cpp
Code:
/*
=================
idRenderModelDecal::CreateProjectionInfo
=================
*/
bool idRenderModelDecal::CreateProjectionInfo(decalProjectionInfo_t &info, const idFixedWinding &winding, const idVec3 &projectionOrigin, const bool parallel, const float fadeDepth, const idMaterial *material, const int startTime)
{

    if (winding.GetNumPoints() != NUM_DECAL_BOUNDING_PLANES - 2)
    {
        common->Printf("idRenderModelDecal::CreateProjectionInfo: winding must have %d points\n", NUM_DECAL_BOUNDING_PLANES - 2);
        return false;
    }

    assert(material != NULL);

    info.projectionOrigin = projectionOrigin;
    info.material = material;
    info.parallel = parallel;
    info.fadeDepth = fadeDepth;
    info.startTime = startTime;
    info.force = false;

    // get the winding plane and the depth of the projection volume
    idPlane windingPlane;
    winding.GetPlane(windingPlane);
    float depth = windingPlane.Distance(projectionOrigin);

    // find the bounds for the projection
    winding.GetBounds(info.projectionBounds);

    if (parallel)
    {
        info.projectionBounds.ExpandSelf(depth);
    }
    else
    {
        info.projectionBounds.AddPoint(projectionOrigin);
    }

    // calculate the world space projection volume bounding planes, positive sides face outside the decal
    if (parallel)
    {
        for (int i = 0; i < winding.GetNumPoints(); i++)
        {
            idVec3 edge = winding[(i + 1) % winding.GetNumPoints()].ToVec3() - winding[i].ToVec3();
            info.boundingPlanes[i].Normal().Cross(windingPlane.Normal(), edge);
            info.boundingPlanes[i].Normalize();
            info.boundingPlanes[i].FitThroughPoint(winding[i].ToVec3());
        }
    }
    else
    {
        for (int i = 0; i < winding.GetNumPoints(); i++)
        {
            info.boundingPlanes[i].FromPoints(projectionOrigin, winding[i].ToVec3(), winding[(i + 1) % winding.GetNumPoints()].ToVec3());
        }
    }

    info.boundingPlanes[NUM_DECAL_BOUNDING_PLANES - 2] = windingPlane;
    info.boundingPlanes[NUM_DECAL_BOUNDING_PLANES - 2][3] -= depth;
    info.boundingPlanes[NUM_DECAL_BOUNDING_PLANES - 1] = -windingPlane;

    // fades will be from these plane
    info.fadePlanes[0] = windingPlane;
    info.fadePlanes[0][3] -= fadeDepth;
    info.fadePlanes[1] = -windingPlane;
    info.fadePlanes[1][3] += depth - fadeDepth;

    // calculate the texture vectors for the winding
    float   len, texArea, inva;
    idVec3   temp;
    idVec5   d0, d1;

    const idVec5 &a = winding[0];
    const idVec5 &b = winding[1];
    const idVec5 &c = winding[2];

    d0 = b.ToVec3() - a.ToVec3();
    d0.s = b.s - a.s;
    d0.t = b.t - a.t;
    d1 = c.ToVec3() - a.ToVec3();
    d1.s = c.s - a.s;
    d1.t = c.t - a.t;

    texArea = (d0[3] * d1[4]) - (d0[4] * d1[3]);
    inva = 1.0f / texArea;

    temp[0] = (d0[0] * d1[4] - d0[4] * d1[0]) * inva;
    temp[1] = (d0[1] * d1[4] - d0[4] * d1[1]) * inva;
    temp[2] = (d0[2] * d1[4] - d0[4] * d1[2]) * inva;
    len = temp.Normalize();
    info.textureAxis[0].Normal() = temp * (1.0f / len);
    info.textureAxis[0][3] = winding[0].s - (winding[0].ToVec3() * info.textureAxis[0].Normal());

    temp[0] = (d0[3] * d1[0] - d0[0] * d1[3]) * inva;
    temp[1] = (d0[3] * d1[1] - d0[1] * d1[3]) * inva;
    temp[2] = (d0[3] * d1[2] - d0[2] * d1[3]) * inva;
    len = temp.Normalize();
    info.textureAxis[1].Normal() = temp * (1.0f / len);
    info.textureAxis[1][3] = winding[0].t - (winding[0].ToVec3() * info.textureAxis[1].Normal());

    return true;
}

/*
=================
idRenderModelDecal::CreateProjectionInfo
=================
*/
void idRenderModelDecal::GlobalProjectionInfoToLocal(decalProjectionInfo_t &localInfo, const decalProjectionInfo_t &info, const idVec3 &origin, const idMat3 &axis)
{
    float modelMatrix[16];

    R_AxisToModelMatrix(axis, origin, modelMatrix);

    for (int j = 0; j < NUM_DECAL_BOUNDING_PLANES; j++)
    {
        R_GlobalPlaneToLocal(modelMatrix, info.boundingPlanes[j], localInfo.boundingPlanes[j]);
    }

    R_GlobalPlaneToLocal(modelMatrix, info.fadePlanes[0], localInfo.fadePlanes[0]);
    R_GlobalPlaneToLocal(modelMatrix, info.fadePlanes[1], localInfo.fadePlanes[1]);
    R_GlobalPlaneToLocal(modelMatrix, info.textureAxis[0], localInfo.textureAxis[0]);
    R_GlobalPlaneToLocal(modelMatrix, info.textureAxis[1], localInfo.textureAxis[1]);
    R_GlobalPointToLocal(modelMatrix, info.projectionOrigin, localInfo.projectionOrigin);
    localInfo.projectionBounds = info.projectionBounds;
    localInfo.projectionBounds.TranslateSelf(-origin);
    localInfo.projectionBounds.RotateSelf(axis.Transpose());
    localInfo.material = info.material;
    localInfo.parallel = info.parallel;
    localInfo.fadeDepth = info.fadeDepth;
    localInfo.startTime = info.startTime;
    localInfo.force = info.force;
}

/*
=================
idRenderModelDecal::AddWinding
=================
*/
void idRenderModelDecal::AddWinding(const idWinding &w, const idMaterial *decalMaterial, const idPlane fadePlanes[2], float fadeDepth, int startTime)
{
    int i;
    float invFadeDepth, fade;
    decalInfo_t   decalInfo;

    if ((material == NULL || material == decalMaterial) &&
        tri.numVerts + w.GetNumPoints() < MAX_DECAL_VERTS &&
        tri.numIndexes + (w.GetNumPoints() - 2) * 3 < MAX_DECAL_INDEXES)
    {

        material = decalMaterial;

        // add to this decal
        decalInfo = material->GetDecalInfo();
        invFadeDepth = -1.0f / fadeDepth;

        for (i = 0; i < w.GetNumPoints(); i++)
        {
            fade = fadePlanes[0].Distance(w[i].ToVec3()) * invFadeDepth;

            if (fade < 0.0f)
            {
                fade = fadePlanes[1].Distance(w[i].ToVec3()) * invFadeDepth;
            }

            if (fade < 0.0f)
            {
                fade = 0.0f;
            }
            else if (fade > 0.99f)
            {
                fade = 1.0f;
            }

            fade = 1.0f - fade;
            vertDepthFade[tri.numVerts + i] = fade;
            tri.verts[tri.numVerts + i].xyz = w[i].ToVec3();
            tri.verts[tri.numVerts + i].st[0] = w[i].s;
            tri.verts[tri.numVerts + i].st[1] = w[i].t;

            for (int k = 0; k < 4; k++)
            {
                int icolor = idMath::FtoiFast(decalInfo.start[k] * fade * 255.0f);

                if (icolor < 0)
                {
                    icolor = 0;
                }
                else if (icolor > 255)
                {
                    icolor = 255;
                }

                tri.verts[tri.numVerts + i].color[k] = icolor;
            }
        }

        for (i = 2; i < w.GetNumPoints(); i++)
        {
            tri.indexes[tri.numIndexes + 0] = tri.numVerts;
            tri.indexes[tri.numIndexes + 1] = tri.numVerts + i - 1;
            tri.indexes[tri.numIndexes + 2] = tri.numVerts + i;
            indexStartTime[tri.numIndexes] =
                indexStartTime[tri.numIndexes + 1] =
                    indexStartTime[tri.numIndexes + 2] = startTime;
            tri.numIndexes += 3;
        }

        tri.numVerts += w.GetNumPoints();
        return;
    }

    // if we are at the end of the list, create a new decal
    if (!nextDecal)
    {
        nextDecal = idRenderModelDecal::Alloc();
    }

    // let the next decal on the chain take a look
    nextDecal->AddWinding(w, decalMaterial, fadePlanes, fadeDepth, startTime);
}

/*
=================
idRenderModelDecal::AddDepthFadedWinding
=================
*/
void idRenderModelDecal::AddDepthFadedWinding(const idWinding &w, const idMaterial *decalMaterial, const idPlane fadePlanes[2], float fadeDepth, int startTime)
{
    idFixedWinding front, back;

    front = w;

    if (front.Split(&back, fadePlanes[0], 0.1f) == SIDE_CROSS)
    {
        AddWinding(back, decalMaterial, fadePlanes, fadeDepth, startTime);
    }

    if (front.Split(&back, fadePlanes[1], 0.1f) == SIDE_CROSS)
    {
        AddWinding(back, decalMaterial, fadePlanes, fadeDepth, startTime);
    }

    AddWinding(front, decalMaterial, fadePlanes, fadeDepth, startTime);
}

/*
=================
idRenderModelDecal::CreateDecal
=================
*/
void idRenderModelDecal::CreateDecal(const idRenderModel *model, const decalProjectionInfo_t &localInfo)
{

    // check all model surfaces
    for (int surfNum = 0; surfNum < model->NumSurfaces(); surfNum++)
    {
        const modelSurface_t *surf = model->Surface(surfNum);

        // if no geometry or no shader
        if (!surf->geometry || !surf->shader)
        {
            continue;
        }

        // decals and overlays use the same rules
        if (!localInfo.force && !surf->shader->AllowOverlays())
        {
            continue;
        }

        srfTriangles_t *stri = surf->geometry;

        // if the triangle bounds do not overlap with projection bounds
        if (!localInfo.projectionBounds.IntersectsBounds(stri->bounds))
        {
            continue;
        }

        // allocate memory for the cull bits
        byte *cullBits = (byte *) _alloca16(stri->numVerts * sizeof(cullBits[0]));

        // catagorize all points by the planes
        SIMDProcessor->DecalPointCull(cullBits, localInfo.boundingPlanes, stri->verts, stri->numVerts);

        // find triangles inside the projection volume
        for (int triNum = 0, index = 0; index < stri->numIndexes; index += 3, triNum++)
        {
            int v1 = stri->indexes[index + 0];
            int v2 = stri->indexes[index + 1];
            int v3 = stri->indexes[index + 2];

            // skip triangles completely off one side
            if (cullBits[v1] & cullBits[v2] & cullBits[v3])
            {
                continue;
            }

            // skip back facing triangles
            if (stri->facePlanes && stri->facePlanesCalculated &&
                stri->facePlanes[triNum].Normal() * localInfo.boundingPlanes[NUM_DECAL_BOUNDING_PLANES - 2].Normal() < -0.1f)
            {
                continue;
            }

            // create a winding with texture coordinates for the triangle
            idFixedWinding fw;
            fw.SetNumPoints(3);

            if (localInfo.parallel)
            {
                for (int j = 0; j < 3; j++)
                {
                    fw[j] = stri->verts[stri->indexes[index + j]].xyz;
                    fw[j].s = localInfo.textureAxis[0].Distance(fw[j].ToVec3());
                    fw[j].t = localInfo.textureAxis[1].Distance(fw[j].ToVec3());
                }
            }
            else
            {
                for (int j = 0; j < 3; j++)
                {
                    idVec3 dir;
                    float scale;

                    fw[j] = stri->verts[stri->indexes[index + j]].xyz;
                    dir = fw[j].ToVec3() - localInfo.projectionOrigin;
                    localInfo.boundingPlanes[NUM_DECAL_BOUNDING_PLANES - 1].RayIntersection(fw[j].ToVec3(), dir, scale);
                    dir = fw[j].ToVec3() + scale * dir;
                    fw[j].s = localInfo.textureAxis[0].Distance(dir);
                    fw[j].t = localInfo.textureAxis[1].Distance(dir);
                }
            }

            int orBits = cullBits[v1] | cullBits[v2] | cullBits[v3];

            // clip the exact surface triangle to the projection volume
            for (int j = 0; j < NUM_DECAL_BOUNDING_PLANES; j++)
            {
                if (orBits & (1 << j))
                {
                    if (!fw.ClipInPlace(-localInfo.boundingPlanes[j]))
                    {
                        break;
                    }
                }
            }

            if (fw.GetNumPoints() == 0)
            {
                continue;
            }

            AddDepthFadedWinding(fw, localInfo.material, localInfo.fadePlanes, localInfo.fadeDepth, localInfo.startTime);
        }
    }
}



BloodRayne@Posted: Sat Dec 29, 2012 1:36 pm :
I've implemented a new feature into Grimm:Quest for the Gatherer's key that I'm very excited about. It's simply called 'spray painting' and what it does is allow for mappers to run their map and spray paint the world with decals. No more tedious placing of single plane decals to create static blood, but 'real' painting of the world with decals. I can now run a map, think 'I'd like to have some blood spray there', paint the world with a single click, issue the command 'saveDecals' when I am happy with the result and the paint will be there permanently in the map.

To make this happen I have exposed a new script command via the SDK that calls 'projectDecalOntoWorld'.
I use this routine in a very simple entity called 'grimm_spray' which has several advantages over the regular func_spray.
The first advantage is that I can now use random paint decals, allowing for randomly looking worlds (which is what Grimm is all about).
The second is that players can turn the feature off, meaning that they can remove these decals for performance reasons, something that's not possible in a static map.

There's no real way to describe it unless you see it in action, so I've created a nice video to show off the effect!

Click image to play video
Image

When painting mode is not set, the spray entities spawn during mapload and paint the world, after which they are automatically removed, this will leave only the paint decal.
Players can set paint density, to have either more or less paint in the world.

I'm pretty excited about this, I've managed to go over 6 maps in a matter of several hours and completely detailed them to taste with decals. This would have normally taken more than a week of careful placing of single decals by hand in the editor, with the added disadvantage of blood not spraying onto nearby objects and such.



Tron@Posted: Sun Dec 30, 2012 2:23 am :
Nice! How many of these decals can you throw around until it starts impacting performance?



bladeghost@Posted: Sun Dec 30, 2012 3:21 am :
That's very interesting, can the same be done with grass,rocks and dirt?
you have some great talent there rayne.
good stuff!



BNA!@Posted: Sun Dec 30, 2012 4:48 am :
Oh that's nice!



The Happy Friar@Posted: Sun Dec 30, 2012 4:56 am :
That's VERY cool. I always wondered why id didn't have that built in to D3. Was there any special hurdles you had to jump over to get this working?



motorsep@Posted: Sun Dec 30, 2012 7:22 am :
Aren't those decals dynamic? (meaning you can only have so many of those on the level)



BloodRayne@Posted: Sun Dec 30, 2012 10:33 am :
Tron wrote:
Nice! How many of these decals can you throw around until it starts impacting performance?

motorsep wrote:
Aren't those decals dynamic? (meaning you can only have so many of those on the level)

Numbers I'm not sure about, it's just a sprite so there's many. Performance starts to hurt when the decals overlap intersecting lights, so the challenge there is to minimize overlap, try to see them as lights and how lights hurt performance. So you quickly learn what works and what doesn't.
There is no hard set limit in my version of the engine.

bladeghost wrote:
That's very interesting, can the same be done with grass,rocks and dirt?
you have some great talent there rayne.
good stuff!

Any texture. But you can do random grass, rock and dirt model using the func_clutter entity that I made for Grimm which does random models. :)

The Happy Friar wrote:
That's VERY cool. I always wondered why id didn't have that built in to D3. Was there any special hurdles you had to jump over to get this working?

Yes, I'm working on making the decals save persistent, which they aren't now. When I quicksave/load the decals disappear, but that's easily fixable. Another hurdle was the angle. It just didn't want to spray in the correct angles, giving lots of stretches and other issues. I've manage to solve most of that but flukes still happen, in that case I simply undo the spray. The eventual code looks really simple, but it took me some time to get there..:)

Code is work in progress, still some changes and cleanouts to be done.

Code:
/* grimm --> spraydecal
================
idEntity::Event_SprayDecal
================
*/
void idEntity::Event_SprayDecal( idVec3 *spray_origin, const char *mtr_decal, idVec3 *spray_angle, float size ) {

   //srpay something
   idVec3 newangle = *spray_angle;   
   //gameLocal.Printf( "%s <--current vector\n", spray_angle->ToString() );
   
   if ( spray_angle->x <= -45 ) {
      newangle.z = 90;
   }
   if ( spray_angle->x >= 45 ) {
      newangle.z = -90;
   }

   gameLocal.ProjectDecal( *spray_origin, newangle, 256.0f, true, size, mtr_decal, 0, true );    
}


And I changed gameLocal.ProjectDecal somewhat:
Code:

/*
===============
idGameLocal::ProjectDecal
===============
*/
void idGameLocal::ProjectDecal( const idVec3 &origin, const idVec3 &dir, float depth, bool parallel, float size, const char *material, float angle, bool forever ) {
   float s, c;
   idMat3 axis, axistemp;
   idFixedWinding winding;
   idVec3 windingOrigin, projectionOrigin;

   static idVec3 decalWinding[4] = {
      idVec3(  1.0f,  1.0f, 0.0f ),
      idVec3( -1.0f,  1.0f, 0.0f ),
      idVec3( -1.0f, -1.0f, 0.0f ),
      idVec3(  1.0f, -1.0f, 0.0f )
   };

   if ( !g_decals.GetBool() ) {
      return;
   }

   // randomly rotate the decal winding
   idMath::SinCos16( ( angle ) ? angle : random.RandomFloat() * idMath::TWO_PI, s, c );

   // winding orientation
   axis[2] = dir;
   axis[2].Normalize();
   axis[2].NormalVectors( axistemp[0], axistemp[1] );
   axis[0] = axistemp[ 0 ] * c + axistemp[ 1 ] * -s;
   axis[1] = axistemp[ 0 ] * -s + axistemp[ 1 ] * -c;

   windingOrigin = origin + depth * axis[2];
   if ( parallel ) {
      projectionOrigin = origin - depth * axis[2];
   } else {
      projectionOrigin = origin;
   }

   size *= 0.5f;

   winding.Clear();
   winding += idVec5( windingOrigin + ( axis * decalWinding[0] ) * size, idVec2( 1, 1 ) );
   winding += idVec5( windingOrigin + ( axis * decalWinding[1] ) * size, idVec2( 0, 1 ) );
   winding += idVec5( windingOrigin + ( axis * decalWinding[2] ) * size, idVec2( 0, 0 ) );
   winding += idVec5( windingOrigin + ( axis * decalWinding[3] ) * size, idVec2( 1, 0 ) );
   if ( forever ) {
      //27 hrs = 'forever' in game time..
      //gameLocal.Printf( "Project decal forever\n");
      gameRenderWorld->ProjectDecalOntoWorld( winding, projectionOrigin, parallel, depth * 0.5f, declManager->FindMaterial( material ), time + SEC2MS( 999999 ) );
   } else {
      //gameLocal.Printf( "Project decal for decalStay time\n");
      gameRenderWorld->ProjectDecalOntoWorld( winding, projectionOrigin, parallel, depth * 0.5f, declManager->FindMaterial( material ), time + SEC2MS( g_decalStay.GetFloat() ) );
   }
   
}



7318@Posted: Sun Dec 30, 2012 12:05 pm :
that's awesome!
but with this you could easily surpass the entity limit, isn't it? we have to find a way to get a much bigger entity limit without affecting anything else, just have the capacity to have more stuff in the maps.



BloodRayne@Posted: Sun Dec 30, 2012 12:16 pm :
7318 wrote:
that's awesome!
but with this you could easily surpass the entity limit, isn't it? we have to find a way to get a much bigger entity limit without affecting anything else, just have the capacity to have more stuff in the maps.

I currently have a 8192 entity limit. There's around 20-30 spray entities per map so that's no more than about 300 polygons max and 30 entities that are removed upon mapload. I could make it so that it doesn't use entities, but I want them to be available for tweaking afterwards in the editor, where you can give them specific sizes and paint materials etc..



7318@Posted: Sun Dec 30, 2012 8:49 pm :
what did you do in order to increase the max entity limit?



bkt@Posted: Mon Dec 31, 2012 6:13 am :
What would be great would be if when you 'savedecals' it would write the decals to the mapfile as func_statics (or regular brushes), which you could then finetune in the editor.



BloodRayne@Posted: Mon Dec 31, 2012 8:31 am :
7318 wrote:
what did you do in order to increase the max entity limit?

Simply raised it (it's a variable gameLocal), I searched D3World for it actually, there's a post on it somewhere. :)

bkt wrote:
What would be great would be if when you 'savedecals' it would write the decals to the mapfile as func_statics (or regular brushes), which you could then finetune in the editor.

I actually didn't want to do that because I want to be able to quickly change the decals (and randomly change them).
Also, one spray can have multiple decals as it sprays on different objects at once.



motorsep@Posted: Mon Dec 31, 2012 8:36 am :
How would you do such decaling in DarkRadiant (so decals end up projected on any kind of curvature in-game) ?

Do those decals work the same way as blood / burn marks in-game? If so, I encountered huge fps drop when I shop my player's model to turn it black (~5 decals will kill performance).



BloodRayne@Posted: Mon Dec 31, 2012 8:41 am :
motorsep wrote:
How would you do such decaling in DarkRadiant (so decals end up projected on any kind of curvature in-game) ?

I don't use DarkRadiant so I have no idea, I guess the same as I would do them in DoomEdit, e.g. place a square and texture it?

Quote:
Do those decals work the same way as blood / burn marks in-game? If so, I encountered huge fps drop when I shop my player's model to turn it black (~5 decals will kill performance).

They use ProjectDecal and there are the same performance issues there as with regular burn marks. This is a mapper's tool, to be used by a mapper that has knowledge of where to place these decals. The performance issues come from overlapping lights, creating draws and depend on which texture you use. As long as you stick with 1-2 decals per surface performance is fine.



motorsep@Posted: Mon Dec 31, 2012 8:46 am :
Ahh, gotcha :( I thought you are using something else to project decals. That's why maybe idSoftware did use that method because it's limiting. I wonder how to resolve this issue with performance (make decals not interact with lights?).



BloodRayne@Posted: Mon Dec 31, 2012 8:50 am :
motorsep wrote:
Ahh, gotcha :( I thought you are using something else to project decals. That's why maybe idSoftware did use that method because it's limiting. I wonder how to resolve this issue with performance (make decals not interact with lights?).

I am developing Grimm in two phases.

Phase 1 consists only of source-code fixes that are SDK related. ProjectDecalOntoWorld is a function that's available from inside the GPL code base, Phase 2 will consist of all those changes I couldn't make via the SDK. It's my plan to look into this again in phase 2 and perhaps create my own routine for it. My knowledge concerning the codebase is growing by the day and I'm nearly at the point where I can start feeling comfortable in calling myself an 'idtech 4' programmer.. Not there yet.. but getting there!



motorsep@Posted: Mon Dec 31, 2012 8:55 am :
cool stuff!



jmarshall23@Posted: Mon Dec 31, 2012 9:02 am :
Very nice now just put it in the editor : ).



nbohr1more@Posted: Mon Dec 31, 2012 8:56 pm :
Dark Radiant has an auto decal feature:


http://wiki.thedarkmod.com/index.php?title=Decals

Quote:
To create decals, select the surface you want to make a decal for (in select face mode or with SHIFT+CTRL+Click the left mouse button). Press the “create Decals for selected Faces” button, it will create a patch for each selected face, which will be exactly in the same plane. Another way to create a decal is create patch yourself and place it were you want. Assign a texture to it from the Media browser, the texture browser or with the surface inspector.


It doesn't look as fun as this though...



motorsep@Posted: Mon Dec 31, 2012 11:59 pm :
How does it get projected though? On runtime ?



Serpentine@Posted: Tue Jan 01, 2013 7:39 am :
motorsep wrote:
How does it get projected though? On runtime ?

This question makes very little sense. wat.



BloodRayne@Posted: Tue Jan 01, 2013 7:46 am :
Serpentine wrote:
motorsep wrote:
How does it get projected though? On runtime ?

This question makes very little sense. wat.

I think he missed the part where I actually posed source code. :mrgreen:

nbohr1more wrote:
Dark Radiant has an auto decal feature:


http://wiki.thedarkmod.com/index.php?title=Decals

Quote:
To create decals, select the surface you want to make a decal for (in select face mode or with SHIFT+CTRL+Click the left mouse button). Press the “create Decals for selected Faces” button, it will create a patch for each selected face, which will be exactly in the same plane. Another way to create a decal is create patch yourself and place it were you want. Assign a texture to it from the Media browser, the texture browser or with the surface inspector.


It doesn't look as fun as this though...


This does seem like a robust way to handle things as well. But it makes things static and give you much less control than in my current setup, which is basically like hanging spraycans in the world which spray their stuff upon mapload and restore, no editing of any kind, no offsetting or texturing needed.



motorsep@Posted: Tue Jan 01, 2013 7:52 am :
No, I was asking about DarkRadiant's method. How does flat patch with decal material get projected onto the world?



nbohr1more@Posted: Tue Jan 01, 2013 6:02 pm :
The patches are created as any other patches would be. They are map geometry rather than
generated entities. (Though you could use the SEED system to generate a bunch of patch models
randomly if you wanted.) The only thing special is that you don't need to meticulously create and align
the patches (and move them into position) yourself.
You just select the surfaces and Dark Radiant creates the patches for you with the right size and alignment.

As far as entity usage. One trick that we tend to advise for decorative grime patches is to merge them into
a single func_static entity. You could also do this with BloodRayne's method since it looks like you are saving
the decals to the map via a console action. You'd just need to crack open the editor, find the decal patches and mark them
as part of the same func_static entity. Though it would kinda defeat the purpose of having an easy free-form
workflow...



BloodRayne@Posted: Tue Jan 01, 2013 6:45 pm :
nbohr1more wrote:
As far as entity usage. One trick that we tend to advise for decorative grime patches is to merge them into
a single func_static entity. You could also do this with BloodRayne's method since it looks like you are saving
the decals to the map via a console action. You'd just need to crack open the editor, find the decal patches and mark them
as part of the same func_static entity. Though it would kinda defeat the purpose of having an easy free-form
workflow...

Actually, they are func_static entities that project a decal, nothing more.
They do it upon map load/restore. (Meaning that if you do a 'reloadModels' or 'vid_restart' the decals disappear), but will fix that soon as well. There's nothing in the editor but these entities to see/edit. You can give their projected decal an angle, set a mtr_decal key (or have it choose a random decal for you) and set spray size. These entities don't create map geometry other than the decals needed.



nbohr1more@Posted: Tue Jan 01, 2013 8:53 pm :
OK. So they are like SEED entities or Particle emitters then.

This might come in handy for TDM after all... Especially since distributing SEED entities on anything other than
a gravity-based orientation has proven a challenge. If these emitters project spherically then this would solve
issues like spawning rocks or vegetation in a cylindrical cave area.



BloodRayne@Posted: Tue Jan 01, 2013 8:59 pm :
nbohr1more wrote:
OK. So they are like SEED entities or Particle emitters then.

This might come in handy for TDM after all... Especially since distributing SEED entities on anything other than
a gravity-based orientation has proven a challenge. If these emitters project spherically then this would solve
issues like spawning rocks or vegetation in a cylindrical cave area.

The engine can do that, look into ProjectDecalOntoWorld in RenderWorld.cpp and modelDecal.cpp.
Mind you, that's just a starting point, I haven't done anything with these. My changes were made solely in entity.cpp. :)

Code:
/*
================
idRenderWorldLocal::ProjectDecalOntoWorld
================
*/
void idRenderWorldLocal::ProjectDecalOntoWorld(const idFixedWinding &winding, const idVec3 &projectionOrigin, const bool parallel, const float fadeDepth, const idMaterial *material, const int startTime)
{
    int i, areas[10], numAreas;
    const areaReference_t *ref;
    const portalArea_t *area;
    const idRenderModel *model;
    idRenderEntityLocal *def;
    decalProjectionInfo_t info, localInfo;

    if (!idRenderModelDecal::CreateProjectionInfo(info, winding, projectionOrigin, parallel, fadeDepth, material, startTime))
    {
        return;
    }

    // get the world areas touched by the projection volume
    numAreas = BoundsInAreas(info.projectionBounds, areas, 10);

    // check all areas for models
    for (i = 0; i < numAreas; i++)
    {

        area = &portalAreas[ areas[i] ];

        // check all models in this area
        for (ref = area->entityRefs.areaNext; ref != &area->entityRefs; ref = ref->areaNext)
        {
            def = ref->entity;

            // completely ignore any dynamic or callback models
            model = def->parms.hModel;

            if (model == NULL || model->IsDynamicModel() != DM_STATIC || def->parms.callback)
            {
                continue;
            }

            if (def->parms.customShader != NULL && !def->parms.customShader->AllowOverlays())
            {
                continue;
            }

            idBounds bounds;
            bounds.FromTransformedBounds(model->Bounds(&def->parms), def->parms.origin, def->parms.axis);

            // if the model bounds do not overlap with the projection bounds
            if (!info.projectionBounds.IntersectsBounds(bounds))
            {
                continue;
            }

            // transform the bounding planes, fade planes and texture axis into local space
            idRenderModelDecal::GlobalProjectionInfoToLocal(localInfo, info, def->parms.origin, def->parms.axis);
            localInfo.force = (def->parms.customShader != NULL);

            if (!def->decals)
            {
                def->decals = idRenderModelDecal::Alloc();
            }

            def->decals->CreateDecal(model, localInfo);
        }
    }
}

/*
====================
idRenderWorldLocal::ProjectDecal
====================
*/
void idRenderWorldLocal::ProjectDecal(qhandle_t entityHandle, const idFixedWinding &winding, const idVec3 &projectionOrigin, const bool parallel, const float fadeDepth, const idMaterial *material, const int startTime)
{
    decalProjectionInfo_t info, localInfo;

    if (entityHandle < 0 || entityHandle >= entityDefs.Num())
    {
        common->Error("idRenderWorld::ProjectOverlay: index = %i", entityHandle);
        return;
    }

    idRenderEntityLocal   *def = entityDefs[ entityHandle ];

    if (!def)
    {
        return;
    }

    const idRenderModel *model = def->parms.hModel;

    if (model == NULL || model->IsDynamicModel() != DM_STATIC || def->parms.callback)
    {
        return;
    }

    if (!idRenderModelDecal::CreateProjectionInfo(info, winding, projectionOrigin, parallel, fadeDepth, material, startTime))
    {
        return;
    }

    idBounds bounds;
    bounds.FromTransformedBounds(model->Bounds(&def->parms), def->parms.origin, def->parms.axis);

    // if the model bounds do not overlap with the projection bounds
    if (!info.projectionBounds.IntersectsBounds(bounds))
    {
        return;
    }

    // transform the bounding planes, fade planes and texture axis into local space
    idRenderModelDecal::GlobalProjectionInfoToLocal(localInfo, info, def->parms.origin, def->parms.axis);
    localInfo.force = (def->parms.customShader != NULL);

    if (def->decals == NULL)
    {
        def->decals = idRenderModelDecal::Alloc();
    }

    def->decals->CreateDecal(model, localInfo);
}

/*
====================
idRenderWorldLocal::ProjectOverlay
====================
*/
void idRenderWorldLocal::ProjectOverlay(qhandle_t entityHandle, const idPlane localTextureAxis[2], const idMaterial *material)
{

    if (entityHandle < 0 || entityHandle >= entityDefs.Num())
    {
        common->Error("idRenderWorld::ProjectOverlay: index = %i", entityHandle);
        return;
    }

    idRenderEntityLocal   *def = entityDefs[ entityHandle ];

    if (!def)
    {
        return;
    }

    const renderEntity_t *refEnt = &def->parms;

    idRenderModel *model = refEnt->hModel;

    if (model->IsDynamicModel() != DM_CACHED)     // FIXME: probably should be MD5 only
    {
        return;
    }

    model = R_EntityDefDynamicModel(def);

    if (def->overlay == NULL)
    {
        def->overlay = idRenderModelOverlay::Alloc();
    }

    def->overlay->CreateOverlay(model, localTextureAxis, material);
}


And ModelDecl.cpp
Code:
/*
=================
idRenderModelDecal::CreateProjectionInfo
=================
*/
bool idRenderModelDecal::CreateProjectionInfo(decalProjectionInfo_t &info, const idFixedWinding &winding, const idVec3 &projectionOrigin, const bool parallel, const float fadeDepth, const idMaterial *material, const int startTime)
{

    if (winding.GetNumPoints() != NUM_DECAL_BOUNDING_PLANES - 2)
    {
        common->Printf("idRenderModelDecal::CreateProjectionInfo: winding must have %d points\n", NUM_DECAL_BOUNDING_PLANES - 2);
        return false;
    }

    assert(material != NULL);

    info.projectionOrigin = projectionOrigin;
    info.material = material;
    info.parallel = parallel;
    info.fadeDepth = fadeDepth;
    info.startTime = startTime;
    info.force = false;

    // get the winding plane and the depth of the projection volume
    idPlane windingPlane;
    winding.GetPlane(windingPlane);
    float depth = windingPlane.Distance(projectionOrigin);

    // find the bounds for the projection
    winding.GetBounds(info.projectionBounds);

    if (parallel)
    {
        info.projectionBounds.ExpandSelf(depth);
    }
    else
    {
        info.projectionBounds.AddPoint(projectionOrigin);
    }

    // calculate the world space projection volume bounding planes, positive sides face outside the decal
    if (parallel)
    {
        for (int i = 0; i < winding.GetNumPoints(); i++)
        {
            idVec3 edge = winding[(i + 1) % winding.GetNumPoints()].ToVec3() - winding[i].ToVec3();
            info.boundingPlanes[i].Normal().Cross(windingPlane.Normal(), edge);
            info.boundingPlanes[i].Normalize();
            info.boundingPlanes[i].FitThroughPoint(winding[i].ToVec3());
        }
    }
    else
    {
        for (int i = 0; i < winding.GetNumPoints(); i++)
        {
            info.boundingPlanes[i].FromPoints(projectionOrigin, winding[i].ToVec3(), winding[(i + 1) % winding.GetNumPoints()].ToVec3());
        }
    }

    info.boundingPlanes[NUM_DECAL_BOUNDING_PLANES - 2] = windingPlane;
    info.boundingPlanes[NUM_DECAL_BOUNDING_PLANES - 2][3] -= depth;
    info.boundingPlanes[NUM_DECAL_BOUNDING_PLANES - 1] = -windingPlane;

    // fades will be from these plane
    info.fadePlanes[0] = windingPlane;
    info.fadePlanes[0][3] -= fadeDepth;
    info.fadePlanes[1] = -windingPlane;
    info.fadePlanes[1][3] += depth - fadeDepth;

    // calculate the texture vectors for the winding
    float   len, texArea, inva;
    idVec3   temp;
    idVec5   d0, d1;

    const idVec5 &a = winding[0];
    const idVec5 &b = winding[1];
    const idVec5 &c = winding[2];

    d0 = b.ToVec3() - a.ToVec3();
    d0.s = b.s - a.s;
    d0.t = b.t - a.t;
    d1 = c.ToVec3() - a.ToVec3();
    d1.s = c.s - a.s;
    d1.t = c.t - a.t;

    texArea = (d0[3] * d1[4]) - (d0[4] * d1[3]);
    inva = 1.0f / texArea;

    temp[0] = (d0[0] * d1[4] - d0[4] * d1[0]) * inva;
    temp[1] = (d0[1] * d1[4] - d0[4] * d1[1]) * inva;
    temp[2] = (d0[2] * d1[4] - d0[4] * d1[2]) * inva;
    len = temp.Normalize();
    info.textureAxis[0].Normal() = temp * (1.0f / len);
    info.textureAxis[0][3] = winding[0].s - (winding[0].ToVec3() * info.textureAxis[0].Normal());

    temp[0] = (d0[3] * d1[0] - d0[0] * d1[3]) * inva;
    temp[1] = (d0[3] * d1[1] - d0[1] * d1[3]) * inva;
    temp[2] = (d0[3] * d1[2] - d0[2] * d1[3]) * inva;
    len = temp.Normalize();
    info.textureAxis[1].Normal() = temp * (1.0f / len);
    info.textureAxis[1][3] = winding[0].t - (winding[0].ToVec3() * info.textureAxis[1].Normal());

    return true;
}

/*
=================
idRenderModelDecal::CreateProjectionInfo
=================
*/
void idRenderModelDecal::GlobalProjectionInfoToLocal(decalProjectionInfo_t &localInfo, const decalProjectionInfo_t &info, const idVec3 &origin, const idMat3 &axis)
{
    float modelMatrix[16];

    R_AxisToModelMatrix(axis, origin, modelMatrix);

    for (int j = 0; j < NUM_DECAL_BOUNDING_PLANES; j++)
    {
        R_GlobalPlaneToLocal(modelMatrix, info.boundingPlanes[j], localInfo.boundingPlanes[j]);
    }

    R_GlobalPlaneToLocal(modelMatrix, info.fadePlanes[0], localInfo.fadePlanes[0]);
    R_GlobalPlaneToLocal(modelMatrix, info.fadePlanes[1], localInfo.fadePlanes[1]);
    R_GlobalPlaneToLocal(modelMatrix, info.textureAxis[0], localInfo.textureAxis[0]);
    R_GlobalPlaneToLocal(modelMatrix, info.textureAxis[1], localInfo.textureAxis[1]);
    R_GlobalPointToLocal(modelMatrix, info.projectionOrigin, localInfo.projectionOrigin);
    localInfo.projectionBounds = info.projectionBounds;
    localInfo.projectionBounds.TranslateSelf(-origin);
    localInfo.projectionBounds.RotateSelf(axis.Transpose());
    localInfo.material = info.material;
    localInfo.parallel = info.parallel;
    localInfo.fadeDepth = info.fadeDepth;
    localInfo.startTime = info.startTime;
    localInfo.force = info.force;
}

/*
=================
idRenderModelDecal::AddWinding
=================
*/
void idRenderModelDecal::AddWinding(const idWinding &w, const idMaterial *decalMaterial, const idPlane fadePlanes[2], float fadeDepth, int startTime)
{
    int i;
    float invFadeDepth, fade;
    decalInfo_t   decalInfo;

    if ((material == NULL || material == decalMaterial) &&
        tri.numVerts + w.GetNumPoints() < MAX_DECAL_VERTS &&
        tri.numIndexes + (w.GetNumPoints() - 2) * 3 < MAX_DECAL_INDEXES)
    {

        material = decalMaterial;

        // add to this decal
        decalInfo = material->GetDecalInfo();
        invFadeDepth = -1.0f / fadeDepth;

        for (i = 0; i < w.GetNumPoints(); i++)
        {
            fade = fadePlanes[0].Distance(w[i].ToVec3()) * invFadeDepth;

            if (fade < 0.0f)
            {
                fade = fadePlanes[1].Distance(w[i].ToVec3()) * invFadeDepth;
            }

            if (fade < 0.0f)
            {
                fade = 0.0f;
            }
            else if (fade > 0.99f)
            {
                fade = 1.0f;
            }

            fade = 1.0f - fade;
            vertDepthFade[tri.numVerts + i] = fade;
            tri.verts[tri.numVerts + i].xyz = w[i].ToVec3();
            tri.verts[tri.numVerts + i].st[0] = w[i].s;
            tri.verts[tri.numVerts + i].st[1] = w[i].t;

            for (int k = 0; k < 4; k++)
            {
                int icolor = idMath::FtoiFast(decalInfo.start[k] * fade * 255.0f);

                if (icolor < 0)
                {
                    icolor = 0;
                }
                else if (icolor > 255)
                {
                    icolor = 255;
                }

                tri.verts[tri.numVerts + i].color[k] = icolor;
            }
        }

        for (i = 2; i < w.GetNumPoints(); i++)
        {
            tri.indexes[tri.numIndexes + 0] = tri.numVerts;
            tri.indexes[tri.numIndexes + 1] = tri.numVerts + i - 1;
            tri.indexes[tri.numIndexes + 2] = tri.numVerts + i;
            indexStartTime[tri.numIndexes] =
                indexStartTime[tri.numIndexes + 1] =
                    indexStartTime[tri.numIndexes + 2] = startTime;
            tri.numIndexes += 3;
        }

        tri.numVerts += w.GetNumPoints();
        return;
    }

    // if we are at the end of the list, create a new decal
    if (!nextDecal)
    {
        nextDecal = idRenderModelDecal::Alloc();
    }

    // let the next decal on the chain take a look
    nextDecal->AddWinding(w, decalMaterial, fadePlanes, fadeDepth, startTime);
}

/*
=================
idRenderModelDecal::AddDepthFadedWinding
=================
*/
void idRenderModelDecal::AddDepthFadedWinding(const idWinding &w, const idMaterial *decalMaterial, const idPlane fadePlanes[2], float fadeDepth, int startTime)
{
    idFixedWinding front, back;

    front = w;

    if (front.Split(&back, fadePlanes[0], 0.1f) == SIDE_CROSS)
    {
        AddWinding(back, decalMaterial, fadePlanes, fadeDepth, startTime);
    }

    if (front.Split(&back, fadePlanes[1], 0.1f) == SIDE_CROSS)
    {
        AddWinding(back, decalMaterial, fadePlanes, fadeDepth, startTime);
    }

    AddWinding(front, decalMaterial, fadePlanes, fadeDepth, startTime);
}

/*
=================
idRenderModelDecal::CreateDecal
=================
*/
void idRenderModelDecal::CreateDecal(const idRenderModel *model, const decalProjectionInfo_t &localInfo)
{

    // check all model surfaces
    for (int surfNum = 0; surfNum < model->NumSurfaces(); surfNum++)
    {
        const modelSurface_t *surf = model->Surface(surfNum);

        // if no geometry or no shader
        if (!surf->geometry || !surf->shader)
        {
            continue;
        }

        // decals and overlays use the same rules
        if (!localInfo.force && !surf->shader->AllowOverlays())
        {
            continue;
        }

        srfTriangles_t *stri = surf->geometry;

        // if the triangle bounds do not overlap with projection bounds
        if (!localInfo.projectionBounds.IntersectsBounds(stri->bounds))
        {
            continue;
        }

        // allocate memory for the cull bits
        byte *cullBits = (byte *) _alloca16(stri->numVerts * sizeof(cullBits[0]));

        // catagorize all points by the planes
        SIMDProcessor->DecalPointCull(cullBits, localInfo.boundingPlanes, stri->verts, stri->numVerts);

        // find triangles inside the projection volume
        for (int triNum = 0, index = 0; index < stri->numIndexes; index += 3, triNum++)
        {
            int v1 = stri->indexes[index + 0];
            int v2 = stri->indexes[index + 1];
            int v3 = stri->indexes[index + 2];

            // skip triangles completely off one side
            if (cullBits[v1] & cullBits[v2] & cullBits[v3])
            {
                continue;
            }

            // skip back facing triangles
            if (stri->facePlanes && stri->facePlanesCalculated &&
                stri->facePlanes[triNum].Normal() * localInfo.boundingPlanes[NUM_DECAL_BOUNDING_PLANES - 2].Normal() < -0.1f)
            {
                continue;
            }

            // create a winding with texture coordinates for the triangle
            idFixedWinding fw;
            fw.SetNumPoints(3);

            if (localInfo.parallel)
            {
                for (int j = 0; j < 3; j++)
                {
                    fw[j] = stri->verts[stri->indexes[index + j]].xyz;
                    fw[j].s = localInfo.textureAxis[0].Distance(fw[j].ToVec3());
                    fw[j].t = localInfo.textureAxis[1].Distance(fw[j].ToVec3());
                }
            }
            else
            {
                for (int j = 0; j < 3; j++)
                {
                    idVec3 dir;
                    float scale;

                    fw[j] = stri->verts[stri->indexes[index + j]].xyz;
                    dir = fw[j].ToVec3() - localInfo.projectionOrigin;
                    localInfo.boundingPlanes[NUM_DECAL_BOUNDING_PLANES - 1].RayIntersection(fw[j].ToVec3(), dir, scale);
                    dir = fw[j].ToVec3() + scale * dir;
                    fw[j].s = localInfo.textureAxis[0].Distance(dir);
                    fw[j].t = localInfo.textureAxis[1].Distance(dir);
                }
            }

            int orBits = cullBits[v1] | cullBits[v2] | cullBits[v3];

            // clip the exact surface triangle to the projection volume
            for (int j = 0; j < NUM_DECAL_BOUNDING_PLANES; j++)
            {
                if (orBits & (1 << j))
                {
                    if (!fw.ClipInPlace(-localInfo.boundingPlanes[j]))
                    {
                        break;
                    }
                }
            }

            if (fw.GetNumPoints() == 0)
            {
                continue;
            }

            AddDepthFadedWinding(fw, localInfo.material, localInfo.fadePlanes, localInfo.fadeDepth, localInfo.startTime);
        }
    }
}