Smooth decals on irregular surfaces
by David Barr · in Torque Game Engine · 08/08/2005 (12:58 pm) · 11 replies
I am doing some experimenting with using the TGE shadow code and the decal manager to produce decals that conform to irregular surfaces in interiors.
So far, its working fine using the surface normal of the point where I want a decal as the light direction but I now want to extend the code to use any light direction.
My problem is that my 3D maths needs some serious work :( and I am struggling to understand exactly what the setLightMatrices function (from shadow.cc) is doing to the plane where the shadow is being built (specifically, how does it relate back to the square shadow texture). The results so far vary depending on which 'light direction' I am using.
Can anyone recommend a link to some description of how this function (or the principle behind it) is actually working - all the searching I have tried has landed me in the middle of none too basic descriptions that assume you know what you are doing in the first place!
So far, its working fine using the surface normal of the point where I want a decal as the light direction but I now want to extend the code to use any light direction.
My problem is that my 3D maths needs some serious work :( and I am struggling to understand exactly what the setLightMatrices function (from shadow.cc) is doing to the plane where the shadow is being built (specifically, how does it relate back to the square shadow texture). The results so far vary depending on which 'light direction' I am using.
Can anyone recommend a link to some description of how this function (or the principle behind it) is actually working - all the searching I have tried has landed me in the middle of none too basic descriptions that assume you know what you are doing in the first place!
#2
This code here is creating basis vectors to generate a matrix from. You can build a matrix from your three basis vectors (they need to be orthogonal) and a position. So, what you see below is using the light direction as one of the vectors. If the Z component is larger than zero, then we swap Z and Y and invert Y and we cross that with the original light direction to get our "right" vector. What we want from this is an Up, Right, and Forward vector (Z, X, Y):
Once we have our basis vectors we can assign them to a matrix which will transform points from the shadow coordinates to the world coordinates. The mLightToWorld is a little deceiving - it would make more sense as mShadowToWorld...
As you can see above, after the shadow to world is calculated, the world to shadow is just a simple inversion. Now you can take a point in 3D space and multiply by the mWorldToLight matrix and that point will be in local shadow coordinates making calculations much easier.
Hope that was a little helpful... I really am not a math guy.
- Brett
08/08/2005 (5:13 pm)
I've spent numerous hours in that code.. in fact, it's exactly where I started. Let me see if I can help you to understand what setLightMatrices is doing. Understand, of course, that this is like the blind leading the blind.This code here is creating basis vectors to generate a matrix from. You can build a matrix from your three basis vectors (they need to be orthogonal) and a position. So, what you see below is using the light direction as one of the vectors. If the Z component is larger than zero, then we swap Z and Y and invert Y and we cross that with the original light direction to get our "right" vector. What we want from this is an Up, Right, and Forward vector (Z, X, Y):
// construct light matrix
Point3F x,z;
if (mFabs(lightDir.z)>0.001f)
{
// mCross(Point3F(1,0,0),lightDir,&z);
z.x = 0.0f;
z.y = lightDir.z;
z.z = -lightDir.y;
z.normalize();
mCross(lightDir,z,&x);
}
else
{
mCross(lightDir,Point3F(0,0,1),&x);
x.normalize();
mCross(x,lightDir,&z);
}Once we have our basis vectors we can assign them to a matrix which will transform points from the shadow coordinates to the world coordinates. The mLightToWorld is a little deceiving - it would make more sense as mShadowToWorld...
mLightToWorld.identity(); mLightToWorld.setColumn(0,x); mLightToWorld.setColumn(1,lightDir); mLightToWorld.setColumn(2,z); mLightToWorld.setColumn(3,pos); mWorldToLight = mLightToWorld; mWorldToLight.inverse(); }
As you can see above, after the shadow to world is calculated, the world to shadow is just a simple inversion. Now you can take a point in 3D space and multiply by the mWorldToLight matrix and that point will be in local shadow coordinates making calculations much easier.
Point3F playerPos = player.getPosition();
mWorldToLight.mulP(playerPos);
Con::printf("The player in shadow coordinates: %3.2f, %3.2f, 3.2f", playerPos.x, playerPos.y, playerPos.z);Hope that was a little helpful... I really am not a math guy.
- Brett
#3
- Brett
08/08/2005 (5:13 pm)
Additionally, let me post a great piece of wisdom that I received from Clark Fagot about this very topic:Quote:
2 things are important here -- make sure all the vectors are orthogonal, and make sure they are all normalized (the second won't keep the matrix from being invertible, but it should be true anyway). A x B is orthogonal to both A and B, unless A and B are parallel, in which case it is a zero vector. So if you are building a matrix out of part of a basis set (like, say, (0,0,1) and some direction vector -- call these Z and dir, respectively -- then if mDot(Z,dir)<0.9 (if the dot product were equal to 1 they'd be parallel) you can find a vector orthogonal to both by taking their cross product Z x dir. But z, dir, and z x dir do not form an orthogonal basis on their own because Z and dir are not necessarily orthogonal. So now you need to choose one of Z and dir (we'll choose dir) and find a new Z to be in our basis -- dir x (Z x dir) will be our third vector. Along the way, however, you'll want to normalize the vectors, so the end result is something like
input, Z, dir -- assume both normalized
X = mCross(dir,Z);
X.normalize();
dir = mCross(X,Z);
// don't need to normalize dir because X and Z are normalized and known to be perpendicular
Of course, if mDot(Z,dir)>0.9 then Z and dir are almost parallel and you'll want to start with another seed vector, other than Z = (0,0,1). Can be just about anything, say (0,1,0).
Hope that helps to at least wrap you mind around it.
- Brett
#4
@ Brett - thanks for that - I forced myself to read several chapters of a 3D Math Primer after reading through your posts and I get the idea what is going on now with basis vectors and matrices.
The problem I had was not actually where I though it was after all though - I just needed to move the decal application point away from the surface to be decaled rather than have it on the surface being decaled.
I now have nice smooth decals on corners, ramps and everywhere else I have tried sticking them whatever the angle.
It seems quite efficient too - its taking less than 1% of process time to render a set of 64 applied decals and there are lot of redundant gl calls I can remove from the render loop.
Thanks again :)
08/09/2005 (10:11 am)
@ Ben - yes it does ... just wasnt making sense to me what it was doing with the verts and texture.@ Brett - thanks for that - I forced myself to read several chapters of a 3D Math Primer after reading through your posts and I get the idea what is going on now with basis vectors and matrices.
The problem I had was not actually where I though it was after all though - I just needed to move the decal application point away from the surface to be decaled rather than have it on the surface being decaled.
I now have nice smooth decals on corners, ramps and everywhere else I have tried sticking them whatever the angle.
It seems quite efficient too - its taking less than 1% of process time to render a set of 64 applied decals and there are lot of redundant gl calls I can remove from the render loop.
Thanks again :)
#5
Thanks for any insight....:)
08/09/2005 (12:08 pm)
@David. I am very interested in how you accomplished this. I dug into something similar to this a while back but never got it to work. Can you possibly give us some more detail on how you did this?Thanks for any insight....:)
#6
I am in the middle of writing and testing stuff at the moment but (very) roughly speaking :-
1, I added the fxDecalManager resource (I wanted a separate decal manager for smooth decals and the resource has a nice guide to how to add another decalmanager).
2, I created a class within fxDecalManager called SmoothDecal which is mainly a copy of the shadow class and copied across the code I needed from shadow.
3, I added a pointer to a SmoothDecal to fxDecalInstance to hold each new decal.
4, I added a new copy of the addDecal function to fxDecalManager which takes a few more parameters (lightdir and object type)
5, Within the new addDecal, I create the SmoothDecal instance :-
6, I altered the renderloop in fxDecalManager::renderObject to call the render method on each SmoothDecal instead of the existing render call.
7, SmoothDecal::renderDecal() is roughly Shadow::render() with a few rearrangements of gl calls because its now a loop.
8, I call fxDecalManager::addDecal from projectile ::explode at the moment as it is a good way to test different points, angles etc etc and will be one of the uses I will be putting the new code to. I use the projectile velocity vector as the light direction (in a new member variable that gets sent to clients as ExplosionDir called pExDir in the function call), the projectile impact point and normal for the function call. I have added a set of fxDecalData to projectile (duplicated the normal decal code) so I have a separate set of SmoothDecals to the normal projectile ones. The bit of code in projectile::explode() looks like this (its a copy of the standard addDecal code just above it) :
Thats about it really - I said it was a bit of a mess at the moment but now its working I can get on with cleaning the code up and adding some decal manager functionality.
One other change I made (which I have made to the normal decalmanager too) is to change the sort method from instance pointer to alloctime since the sort puts your new decal at the front of the queue which is then the first one removed on the next add decal call if your queue is still full :-
Let me know if any of the above is less than clear.
08/09/2005 (1:04 pm)
Jackie (Edit - check your email!)I am in the middle of writing and testing stuff at the moment but (very) roughly speaking :-
1, I added the fxDecalManager resource (I wanted a separate decal manager for smooth decals and the resource has a nice guide to how to add another decalmanager).
2, I created a class within fxDecalManager called SmoothDecal which is mainly a copy of the shadow class and copied across the code I needed from shadow.
3, I added a pointer to a SmoothDecal to fxDecalInstance to hold each new decal.
4, I added a new copy of the addDecal function to fxDecalManager which takes a few more parameters (lightdir and object type)
5, Within the new addDecal, I create the SmoothDecal instance :-
...
//smooth decal code
newDecal->mSmoothDecal = new SmoothDecal();
F32 radius = 1.0f; //TO DO - build into fxDecalData
F32 shadowLen = 4.5f; //TO DO - build into fxDecalData
pos -= lightDir; //offset from surface TO DO - add angle test light to normal to limit skew
pos += normal; //offset from surface
newDecal->mSmoothDecal->buildPartition(pos,lightDir,radius,shadowLen); //pos is adjusted impact point, lightDir is projectile velocity
newDecal->mSmoothDecal->textureHandle = decalData->textureHandle; //hijack the decal texture for testing
newDecal->mSmoothDecal->mFade = 0.9f; //start off transparent
...6, I altered the renderloop in fxDecalManager::renderObject to call the render method on each SmoothDecal instead of the existing render call.
for (S32 x = 0; x < mDecalQueue.size(); x++)
{
mDecalQueue[x]->mSmoothDecal->renderDecal();
}7, SmoothDecal::renderDecal() is roughly Shadow::render() with a few rearrangements of gl calls because its now a loop.
8, I call fxDecalManager::addDecal from projectile ::explode at the moment as it is a good way to test different points, angles etc etc and will be one of the uses I will be putting the new code to. I use the projectile velocity vector as the light direction (in a new member variable that gets sent to clients as ExplosionDir called pExDir in the function call), the projectile impact point and normal for the function call. I have added a set of fxDecalData to projectile (duplicated the normal decal code) so I have a separate set of SmoothDecals to the normal projectile ones. The bit of code in projectile::explode() looks like this (its a copy of the standard addDecal code just above it) :
if(mDataBlock->bloodDecalCount > 0) //lets cat out of bag as to one intended use of smooth decalling :)
{
if(collideType & (InteriorObjectType)) //something to shoot at to test
{
// randomly choose a decal between 0 and (decal count - 1)
U32 idx = (U32)(mCeil(mDataBlock->bloodDecalCount * Platform::getRandom()) - 1.0f);
// this should never choose a NULL idx, but check anyway
if(mDataBlock->bloodDecals[idx] != NULL)
{
fxDecalManager *fxdecalMngr = gClientSceneGraph->getCurrentfxDecalManager();
if(fxdecalMngr)
fxdecalMngr->addDecal(p, n, pExDir, mDataBlock->bloodDecals[idx], collideType); //new addDecal call
}
}
}Thats about it really - I said it was a bit of a mess at the moment but now its working I can get on with cleaning the code up and adding some decal manager functionality.
One other change I made (which I have made to the normal decalmanager too) is to change the sort method from instance pointer to alloctime since the sort puts your new decal at the front of the queue which is then the first one removed on the next add decal call if your queue is still full :-
In cmpfxDecalInstance return S32((*pd1)->allocTime) - S32((*pd2)->allocTime); //sort on alloc time
Let me know if any of the above is less than clear.
#7
Thanks Man!!!!
08/09/2005 (1:16 pm)
Clear as mud....;) Not really!! :) This is very creative. I was looking at the fxDecalManager last night, believe it or not. I have been trying to figure out how to place and control decals at random. As in. an explosive detonates at the corner of a wall then be able to place scorch marks on both walls and the floor. I am definitely going to dig into this this evening. I really do appreciate you sharing this info with everyone!Thanks Man!!!!
#8
My intention is to extend the fxDecalManager to do what its name implies so I can just call addDecal(position, type, ...) from where I want in the code (or script) and add a decal to the world.
08/09/2005 (1:28 pm)
Jackie - not a problem - this is what the community is for :)My intention is to extend the fxDecalManager to do what its name implies so I can just call addDecal(position, type, ...) from where I want in the code (or script) and add a decal to the world.
#9
Thanks....
08/09/2005 (5:47 pm)
@David.. I noticed on your above post you told me to check my email. Did you send me something? If you did I did not receive anything. You can also send me email at jackie@igysoft.com. Please resend!Thanks....
#10
08/10/2005 (5:28 am)
@Jackie - I sent you a couple of files that may be of use - I have just resent them to the igysoft address.
#11
Very cool......:)
08/10/2005 (6:09 am)
Thanks David. I got it that time. Not sure why I did not receive it through Cyberbasin unless our Barracuda tagged it for some reason. I'll get it setup and tested hopefully today and give you some feedback. Your example png shows exactly what I want to do! Very cool......:)
Associate Kyle Carter