Hello everybody.
I was doing some stuff for a game (more something enjoy coding than a real game) and i made a class that can be usefull and that is not hard to understand (not too much at least).
I didn’t found anything like that and i read a lot of people asking for something similar.
So, here is my simple class to handle bullet holes.
[java]
import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.collision.CollisionResults;
import com.jme3.material.Material;
import com.jme3.math.Ray;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.shape.Quad;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
- A simple class to handle bullet holes in a simple way. It is likely not the
- best way to do it, but it is simple and not hard to put into your application.
-
@author Bubuche
*/
public class BulletHoleManager extends AbstractAppState
{
private int maxTime;
private int maxHole;
private float distanceToAvoidZFight;
private Application application;
private List<BulletHole> holes;
private Vector3f temp;
private Ray ray;
private CollisionResults results;
public BulletHoleManager(Application application)
{
holes = new ArrayList<>();
this.application = application;
temp = new Vector3f();
maxTime = -1;
maxHole = -1;
// 1cm. In a "normal" world, surfaces separated with 1cm shouldn't z-fight
distanceToAvoidZFight = 0.01f;
ray = new Ray();
results = new CollisionResults();
}
public void setMaxTime(int ms)
{
this.maxTime = ms;
}
public void setNbMax(int nb)
{
this.maxHole = nb;
}
public void setDistanceToAvoidZFight(float distance)
{
this.distanceToAvoidZFight = distance;
}
public void addBulletHole(
Node node,
Material material,
Vector3f place,
Vector3f normal,
float width,
float height)
{
long time;
time = application.getTimer().getTime();
BulletHole bulletHole = new BulletHole(time, material, width, height);
temp.set(normal);
temp.normalizeLocal();
temp.multLocal(distanceToAvoidZFight);
temp.addLocal(place);
node.worldToLocal(temp, temp);
bulletHole.geom.setLocalTranslation(temp);
bulletHole.geom.setQueueBucket(Bucket.Translucent);
node.attachChild(bulletHole.geom);
temp.set(normal);
temp.addLocal(place);
bulletHole.geom.lookAt(temp, Vector3f.UNIT_Y);
testCollisionWithOtherBulletsHoles(
bulletHole,
place,
normal);
holes.add(bulletHole);
}
@Override
public void update(float tpf)
{
removeTooOldHoles();
removeTooManyHoles();
}
private void removeTooOldHoles()
{
if (maxTime < 0)
{
return;
}
long time = application.getTimer().getTime();
Iterator<BulletHole> it = holes.iterator();
while (it.hasNext())
{
BulletHole bh = it.next();
if (bh.time >= time + maxTime)
{
removeHole(bh);
it.remove();
}
else
{
break;
}
}
}
private void removeTooManyHoles()
{
if (maxHole < 0)
{
return;
}
while (holes.size() > maxHole)
{
removeHole(holes.remove(0));
}
}
private void removeHole(BulletHole bh)
{
bh.geom.removeFromParent();
}
private void testCollisionWithOtherBulletsHoles(
BulletHole bulletHole,
Vector3f point,
Vector3f normal)
{
Vector3f working = null;
Vector3f vert = new Vector3f();
Transform t = bulletHole.geom.getWorldTransform();
FloatBuffer buf = bulletHole.geom.getMesh().getFloatBuffer(VertexBuffer.Type.Position);
buf.rewind();
temp.set(normal);
temp.negateLocal();
ray.setDirection(normal);
//"3*" because only 2 could lead to miss some case because of approximations
ray.setLimit(1 + (3 * distanceToAvoidZFight));
/* for each corner, we check if this corner insn't inside an other
* bullethole.
* To get the origine of the ray, we take the (x, y, z) of the vertex from
* the position buffer, then we transform it's position to the world's position
*/
for (int i = 0; i < 4; i++)
{
float x, y, z;
x = buf.get();
y = buf.get();
z = buf.get();
vert.set(x, y, z);
working = t.transformVector(vert, working);
Vector3f origin = ray.getOrigin();
origin.set(working);
origin.addLocal(temp);
ray.setOrigin(origin);
/* We prepared the raycast. Now, we try to intersect it with all others
* bullethole */
for (Iterator<BulletHole> it = holes.iterator(); it.hasNext();)
{
BulletHole other = it.next();
other.geom.collideWith(ray, results);
if (results.size() != 0)
{
removeHole(other);
it.remove();
}
results.clear();
}
}
/*FIXME*/
/* NOT SURE WE HAVE TO FLIP ! CAN BE A BUG */
buf.flip();
}
private class BulletHole
{
private long time;
private Geometry geom;
public BulletHole(long time, Material material, float width, float height)
{
Quad quad = new Quad(width, height);
geom = new Geometry("bullet hole", quad);
geom.setMaterial(material);
}
}
}
[/java]
By default, it enables 10 bullet hole at a time, with no life time limit. You can set a limit of time (so bullet holes will disappear by themself), by setting any positive value. You can disable the “10 bullet hole at a time” value by setting a negative value to it.
It’s NOT a perfect class for this job at all, as i think that a shader should do that. If you put a bullet hole in a corner, you’ll have an artifact with a partially floating bullet hole. I can’t adapt the form of the decal to fit the object behind it for 2 reason : this object can change it’s shape AND i have no information about the volume of your bullet hole (it’s juste an image, i don’t know if you put a hole on this image or not).
Be carefull : each bullet hole is a quad, meaning that they will be into the CollisionResults if you perform a raycast test (like i do to detect when the bullet hit )
If the object is moving (or even rolling) the bullet hole will follow it (and roll with it).
If 2 bullet holes are overlapping, the oldest will be removed (or it should be, i know that in some very specific case i’ll remove too much bullet holes - namely if you shoot on a ball, then put the resulting bullet hole near a wall then shoot on the wall, the bullet hole on the ball will be removed).
Anyway, to enable it, just add it with
getStateManager().attach(new BulletHoleManager(this));
To add a bullet hole, “just” do :
[java]
application.getStateManager().getState(BulletHoleManager.class).addBulletHole(
hitNode, //likely result.getGeometry().getParent()
bulletHoleMaterial,
result.getContactPoint(),
result.getContactNormal(),
widthOfTheBulletHole,
heightOfTheBulletHole);
[/java]
where result is a CollisionResult and the width and height are floats.
If you want to use it or even to put it into the core of jme (i don’t think it’s a good idea, however ) feel free to do it :).