Download the source here. Intro:
Decals are flat planes that are applied to models, usually to denote
things like bullet holes, or footprints etc.
Theoretically they can be done simply enough by slapping a small plane
with the texture you want on the same location as the surface it's to
sit on, with the same normal. You can mimic this easily enough by using
modelsUnderLoc() or modelsUnderRay()
to get the intersection position with a model's surface (using the #detailed
version of these routines gives you the surface normal and the exact
collision location you need for this).
Unfortunately this, while technically correct, results in visual errors.
Why? Because drawing the decal over the surface causes Z buffer problems.
You may not see the decal, you may see it, or you may get bizarre stripes
of it. See the demo below with the decal offsetting disabled to visualise
what I mean here.
There is a way around this, however! The simplest way is to position
the decal, not on the surface, but slightly away from it. One simple
solution to this is to move it forward in the direction of the surface
normal. Some success results from this, but again it causes problems.
The main one is that the Z buffer is not linear - it doesn't divide
it's bits up into a linear sequence starting from the camera.hither
property to the camera.yon property. It's actually logarithmic. The
Z buffer stores more data about close objects than far away. So while
up close moving your decal (say) 1 unit 'up' from the surface it rests
on may be fine, moving away means the nasty Z buffer graphical glitch
returns. You can remove this completely by setting the decal so far
away from the surface it is on that when in the distance it renders
fine, but up close you will really really notice that the decal is standing
out from the wall. Trust me on this one, it don't work (cos I tried
to implement it, and it sucks!).
Method:
OK, so now what?
Well, as usual, there is a solution (well, probably lots, but this is
one of them). Sadly it is computationally fairly expensive. Every frame
you re-position each decal - moving it forward an amount based on the
distance of the decal from the camera. Using this method gains one benefit
- rather than just moving the decal 'upwards' from the surface it lies
on, we can move it directly towards the camera. This means it never
looks like it's floating above the surface below.
So, the nitty gritty. Each decal stores a bit of data about itself.
This data is where in an ideal world it should be positioned (flat on
the underlying surface). I'll refer to this as the decal's 'ideal position'.
Then, every frame, the vector from the camera to this location is calculated.
Let's call this the 'viewing vector'. The decal's new position is calculated
as it's ideal position, minus a proportion of the viewing vector's magnitude
(IE the distance from the camera to the decal's ideal position). This
is basically moving the decal from where it should be, to a point slightly
nearer the camera in a direct line.
And what is this proportion?
Well, I'm afraid it's a trial and error situation I'm afraid :(
You need to play around with this so that decals look fine up close
and far away - simple as that. It will differ from application to application,
although it should remain constant within a given project. This value
is based on the full Z buffer range - the camera.hither -> camera.yon
distance. Change this and you'll need to change the proportion scalar.
Also, note that using this, test your code in 16 BIT MODE! The reason
is that this uses only a 16 bit Z buffer, which is less accurate than
a 32 bit Z buffer. Decals that work on a 16 bit Z buffer will work identically
on a 32 bit Z buffer, but the reverse is not true. You have been warned....
In Practice:
I won't detail how I pick the actual point for the decal to be, that's
not the point of this tutorial. However, it may be of some interest,
so here's the brief breakdown:
Use modelsUnderLoc(#detailed)
to determine the hit point of the mouse click and the chosen model
Since this result
is in world coordinates, I convert it to model coordinates by inverting
the model's worldTransform, and passing the results from modelsUnderLoc()
through it. This tells me where on the model the click happened and
also it's surface normal.
Back to how the decals
are actually processed:
Get the camera
location (you don't need to know it's direction or anything).
Because of how decals work, it's best to manipulate the camera location
in model coordinates, not the world coordinates. To this end I get
the model's worldTransform, and invert it, with the following line:
tmWorldInvert = inverse(pObj.getWorldTransform())
This gives you a transform that will take a vector from world coordinates
to to model coordinates (for the model pObj, set up in the new()
handler of the decal script object.
Then I get the camera's location and using the above transform convert
it into model coordinates. For this you use the following line of
code:
Now we need to
scale this vector by a proportion. In the code below I simply chose
to divide it by 10.0 - it worked first time, so I'm happy! If you
divide the vector by too large a value here the decals may not be
offset enough and you'll get the Z buffer problem. Divide the vector
by too small a value here and the decals may be offset by so much
they appear through nearer objects. Trial and error I'm afraid :(
Anyway, the code used here is:
tvOffset = tvView / 10.0
Now to actually
offset the decal. We position it offset from it's ideal location with
the following code:
tDecalModel.transform.position
= tvPos - tvOffset
Repeat with process for all decals, and away you go!