Lines and trails

Hi,



my first jmonkey project and at the same time my first contribution. Lines and Trails which are pretty much like the LineRenderer and TrailRenderer from Unity3d. I made a video, but the quality is rather poor, I don’t know why and am too tired to try and figure out what settings were wrong (it’s 4am over here).



http://www.youtube.com/watch?v=KI31waXll3k



[java]package mygame;

import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;
import com.jme3.util.BufferUtils;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
* 3d Floating Billboarded Lines
* Attach this to a Geometry Node, which has a Material.
* The Material should have FaceCulling set Off!
* The Mesh is procedurally created, texture coordinates are computed using the line segment lengths.
* Normals are not computed, since it's billboarded anyway.
* Can be used for trails. In that case set usingLinkedList true. If you do this, LinkedLists are used,
* which make adding/removing points more efficient.
* Note however, that of course random access to points suffer from using LinkedLists.
* If you need to access points often, for example to create lightning bolts, set usedAsTrails false. In this
* case ArrayLists are used to make accessing points more efficient.
* Note: Calling setPoint(Vector3f) will not only set the points position, but also recompute the texture coordinates
* for all points. If you want to access Points and modify their positions without costly recomputing all the texture
* coordinates, then use getPoint(int index) and modify that vector directly or use getPoints(). This way you can avoid recomputing
* texture coordinates, however if you want to have correctly computed texture coordinates after modifying a
* bunch of points this way, then call recomputeTextureCoordinates() after moving the vertices this way yourself.
* @author cvlad
*/
public class LineControl extends AbstractControl {
static final int EXPECTED_POINTS = 32;
static final int MINIMUM_POS_BUFFER_SIZE = EXPECTED_POINTS * 6; // 2 verts per point, 3 floats per pos
static final int MINIMUM_TEXCOORD_BUFFER_SIZE = EXPECTED_POINTS * 4; // 2 verts per point, 2 floats per texcoord
static final int MINIMUM_INDEX_BUFFER_SIZE = EXPECTED_POINTS * 2; // 2 verts per point, 1 int per index
List<Vector3f> points;
List<Float> halfWidths;
List<FloatWrapper> lengths;
Mesh mesh;
float totalLength = 0;
boolean usingLinkedList = true;
LineBehaviour behaviour;

/**
* Constructor
* Standard line behaviour: Uses Algo1CamDirBB and usingLinkedList is set false.
*/
public LineControl(){
this(new Algo1CamDirBB(), false);
}

/**
* Constructor
* @param behaviour There are different algorithms available to billboard the line. Inject one of those here.
* @param usingLinkedList When set to true, LinkedLists are used to make adding/removing points more efficient.
* False means ArrayLists will be used, which makes manipulating existing points more efficient.
*/
public LineControl(LineBehaviour behaviour, boolean usingLinkedList){
this.behaviour = behaviour;
this.usingLinkedList = usingLinkedList;
if (usingLinkedList){
this.points = new LinkedList<Vector3f>();
this.halfWidths = new LinkedList<Float>();
this.lengths = new LinkedList<FloatWrapper>();
} else{
this.points = new ArrayList<Vector3f>();
this.halfWidths = new ArrayList<Float>();
this.lengths = new ArrayList<FloatWrapper>();
}
}

@Override
public void setSpatial(Spatial spatial){
if (!(spatial instanceof Geometry))
throw new ClassCastException("LineControl can only be attached to Geometry Nodes");
super.setSpatial(spatial);

mesh = new Mesh();
mesh.setDynamic();

// Setting buffers
FloatBuffer positions = BufferUtils.createFloatBuffer(MINIMUM_POS_BUFFER_SIZE);
positions.limit(0);
mesh.setBuffer(Type.Position, 3, positions);
FloatBuffer texCoord = BufferUtils.createFloatBuffer(MINIMUM_TEXCOORD_BUFFER_SIZE);
texCoord.limit(0);
mesh.setBuffer(Type.TexCoord, 2, texCoord);
IntBuffer indexes = BufferUtils.createIntBuffer(MINIMUM_INDEX_BUFFER_SIZE);
indexes.limit(0);
mesh.setBuffer(Type.Index, 3, indexes);
mesh.updateBound();
mesh.setMode(Mesh.Mode.TriangleStrip);
((Geometry)spatial).setMesh(mesh);
}

@Override
protected void controlUpdate(float f) {}

@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
behaviour.update(this, rm, vp);
}

/**
* Returns line's billboarding behaviour.
* @return line's billboarding behaviour.
*/
public LineBehaviour getBehaviour(){
return this.behaviour;
}

@Override
public Control cloneForSpatial(Spatial sptl) {
LineControl clone = new LineControl(this.behaviour, this.usingLinkedList);
clone.set(this.points, this.halfWidths);
return clone;
}

/**
* Clears this line's points and halfWidths and copys(!) points and halfwidths into this line.
* @param Points through which the line will be drawn.
* @param halfWidths Half of the width for each point.
*/
public void set(List<Vector3f> points, List<Float> halfWidths){
if (points == null || halfWidths == null)
throw new IllegalArgumentException("LineNodes.set: points and halfWidths may not be null");
if (points.size() != halfWidths.size())
throw new IllegalArgumentException("LineNodes.set: points.size() has to be equal to halfWidths.size()");
this.points.clear();
this.halfWidths.clear();
for (Vector3f point : points){
if (point == null)
throw new IllegalArgumentException("LineNodes.set: One of the vectors in the points parameter was null");
this.points.add(new Vector3f(point.x, point.y, point.z));
}
for (float halfWidth : halfWidths){
this.halfWidths.add(halfWidth);
}


int p = 2;
while (p < points.size()){
p *= 2;
}

// positions
FloatBuffer positions = BufferUtils.createFloatBuffer(p*6);
positions.limit(points.size()*6);
mesh.setBuffer(Type.Position, 3, positions);

// indexes
IntBuffer indexes = BufferUtils.createIntBuffer(p*2);
for (int i = 0; i < points.size()*2; i++){
indexes.put(i);
}
indexes.flip();
mesh.setBuffer(Type.Index, 3, indexes);

// texcoord
totalLength = 0;
lengths.clear();
if (points.size() > 1){
Vector3f distance = new Vector3f();
Iterator<Vector3f> itPoint = points.iterator();
Vector3f point = itPoint.next();
while (itPoint.hasNext()){
Vector3f nextPoint = itPoint.next();
totalLength += nextPoint.subtract(point, distance).length();
lengths.add(new FloatWrapper(totalLength));
point = nextPoint;
}
}

FloatBuffer texCoord = BufferUtils.createFloatBuffer(p*4);
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(1.0f);
if (totalLength == 0){
for (FloatWrapper l : lengths){
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(1.0f);
}
}else{
for (FloatWrapper l : lengths){
float length = l.getValue() / totalLength;
texCoord.put(length);
texCoord.put(0.0f);
texCoord.put(length);
texCoord.put(1.0f);
}
}
texCoord.flip();
mesh.setBuffer(Type.TexCoord, 2, texCoord);
behaviour.set();
}

/**
* Sets the width at the point whom index you provide.
* @param index Point's (ergo halfWidths) index in line's list.
* @param width How width the line shall be at that point.
*/
public void setWidth(int index, float width){
this.halfWidths.set(index, width/2);
}

/**
* Gets the width at the point whom index you provide.
* @param index Point's (ergo halfWidths) index in line's list.
* @return width How width the line is at that point.
*/
public float getWidth(int index){
return this.halfWidths.get(index) * 2;
}

/**
* Sets the width at the point whom index you provide.
* @param index Point's (ergo halfWidths) index in line's list.
* @param halfWidth Half of the desired width
*/
public void setHalfWidth(int index, float halfWidth){
this.halfWidths.set(index, halfWidth);
}

/**
* Gets the half of the width at the point whom index you provide.
* @param index Point's (ergo halfWidths) index in line's list.
* @return Half of the width at that point
*/
public float getHalfWidth(int index){
return this.halfWidths.get(index);
}

/**
* Returns the list of the points which make up this line. Do NOT add or remove points.
* You can however set the points positions. After doing so you should call recomputeTextureCoordinates
* if you want the texture coordinates to be correct.
*/
public List<Vector3f> getPoints(){
return this.points;
}

/**
* Returns the list of the widths of the points which make up this line. Do NOT add or remove items.
* You can however modify the existing widths
* The widths divided by two.
* @return Returns the list of the widths of the points which make up this line. The widths divided by two.
*/
public List<Float> getHalfWidths(){
return this.halfWidths;
}

/**
* Copys the point's coordinates into the point whom index you provide.
* @param index Index of the point which shall be modified.
* @param point Point who's coordinates are copied into the point whom index you provide
*/
public void setPoint(int index, Vector3f point){
this.getPoint(index).set(point);
this.recomputeTextureCoordinates();
}

/**
* Returns a point whom index you provide. Modifying that point will alter the line. However, after you do this,
* you should call recomputeTextureCoordinates if you want the texture coordinates to be correct.
* @param index Index of the point you want to get
* @return Point who's index you provided
*/
public Vector3f getPoint(int index){
if (usingLinkedList){
if (index == 0)
return ((LinkedList<Vector3f>) this.points).getFirst();
if (index == points.size() - 1)
return ((LinkedList<Vector3f>) this.points).getLast();
}
return this.points.get(index);
}

/**
* Returns the number of points in this line.
* @return number of points in this line
*/
public int getNumPoints(){
return this.points.size();
}

private class FloatWrapper{
float value;
public FloatWrapper(float value){
this.value = value;
}
public void setValue(float value){
this.value = value;
}
public float getValue(){
return this.value;
}
}

/**
* Removes the point who's index you provide. Returns a handle of that point, so you can recycle it if you want to.
* @param index Index of the point you want to remove.
* @return Point which was removed.
*/
public Vector3f removePoint(int index){
if (index < 0 || index >= points.size())
throw new IllegalArgumentException("LineNodes.removePoint: There is no point with such an index");
Vector3f oldPoint = null;
if (usingLinkedList){
if (index == 0){
((LinkedList<Float>) halfWidths).removeFirst();
oldPoint = ((LinkedList<Vector3f>) points).removeFirst();
}
else if (index == points.size() - 1){
((LinkedList<Float>) halfWidths).removeLast();
oldPoint = ((LinkedList<Vector3f>) points).removeLast();
}else{
halfWidths.remove(index);
oldPoint = points.remove(index);
}
} else{
halfWidths.remove(index);
oldPoint = points.remove(index);
}

// positions
VertexBuffer pvb = mesh.getBuffer(Type.Position);
FloatBuffer positions = (FloatBuffer) pvb.getData();
positions.limit(positions.limit() - 6);
if (positions.capacity() / 4 >= MINIMUM_POS_BUFFER_SIZE && positions.limit() <= positions.capacity() / 4){
FloatBuffer newBuffer = BufferUtils.createFloatBuffer(positions.capacity() / 2);
newBuffer.limit(positions.limit());
mesh.setBuffer(Type.Position, 3, newBuffer); // TODO: is this necessary???
}else{
pvb.updateData(positions); // TODO: is this necessary???
//mesh.setBuffer(Type.Position, 3, positions); // TODO: shouldn't it work without this???
//mesh.updateCounts();
}

// indexes
pvb = mesh.getBuffer(Type.Index);
IntBuffer indexes = (IntBuffer) pvb.getData();
indexes.limit(indexes.limit() - 2);
if (indexes.capacity() / 4 >= MINIMUM_INDEX_BUFFER_SIZE && indexes.limit() <= indexes.capacity() / 4){
IntBuffer newBuffer = BufferUtils.createIntBuffer(indexes.capacity() / 2);
indexes.rewind();
newBuffer.put(indexes);
newBuffer.flip();
mesh.setBuffer(Type.Index, 3, newBuffer); // TODO: is this necessary???
}else{
pvb.updateData(indexes); // TODO: is this necessary???
//mesh.setBuffer(Type.Index, 3, indexes); // TODO: shouldn't it work without this???
//mesh.updateCounts();
}

// texcoord
if (points.size() > 1){
if (index == 0){
float removedLength;
if (usingLinkedList){
removedLength = ((LinkedList<FloatWrapper>) lengths).remove().getValue();
} else{
removedLength = lengths.remove(0).getValue();
}
totalLength -= removedLength;

Iterator<FloatWrapper> itLengths = lengths.iterator();
while (itLengths.hasNext()){
FloatWrapper l = itLengths.next();
l.setValue(l.getValue() - removedLength);
}

} else if (index == lengths.size()){ // this should mean that the last point is removed, hence index == lengths.size()
float removedLength;
if (usingLinkedList){
removedLength = ((LinkedList<FloatWrapper>) lengths).removeLast().getValue();
totalLength += ((LinkedList<FloatWrapper>) lengths).getLast().getValue() - removedLength;
} else{
removedLength = lengths.remove(lengths.size() - 1).getValue();
totalLength += lengths.get(lengths.size() - 1).getValue() - removedLength;
}
} else {
float newSegmentLength = points.get(index - 1).subtract(points.get(index)).length();
float lengthBeforeSegment = 0;
if (index > 1)
lengthBeforeSegment = lengths.get(index-2).getValue();
float removedSegmentsLengths = lengths.remove(index).getValue() - lengthBeforeSegment;
float deltaLength = newSegmentLength - removedSegmentsLengths;
totalLength += deltaLength;
lengths.get(index-1).setValue(newSegmentLength + lengthBeforeSegment);

if (usingLinkedList){
Iterator<FloatWrapper> itLengths = lengths.iterator();
for (int i = 0; i < index; i++){
itLengths.next();
}
for (int i = index; i < lengths.size(); i++){
FloatWrapper f = itLengths.next();
f.setValue(f.getValue() + deltaLength);
}
}else{
for (int i = index; i < lengths.size(); i++){
FloatWrapper f = lengths.get(i);
f.setValue(f.getValue() + deltaLength);
}
}
}
} else{
totalLength = 0;
lengths.clear();
}

pvb = mesh.getBuffer(VertexBuffer.Type.TexCoord);
FloatBuffer texCoord = (FloatBuffer) pvb.getData();
texCoord.limit(texCoord.limit() - 4);
if (texCoord.capacity() / 4 >= MINIMUM_TEXCOORD_BUFFER_SIZE && texCoord.limit() <= texCoord.capacity() / 4){ // need to create new buffer
FloatBuffer newBuffer = BufferUtils.createFloatBuffer(texCoord.capacity() / 2); // reasonable assumption: 2 times the previous size
newBuffer.limit(texCoord.limit()); // two new vertices, hence 4 new texture coordinates
newBuffer.put(0.0f);
newBuffer.put(0.0f);
newBuffer.put(0.0f);
newBuffer.put(1.0f);

if (totalLength == 0){
for (FloatWrapper l : lengths){
newBuffer.put(0.0f);
newBuffer.put(0.0f);
newBuffer.put(0.0f);
newBuffer.put(1.0f);
}
} else{
for (FloatWrapper l : lengths){
float length = l.getValue() / totalLength;
newBuffer.put(length);
newBuffer.put(0.0f);
newBuffer.put(length);
newBuffer.put(1.0f);
}
}

newBuffer.flip();
mesh.setBuffer(Type.TexCoord, 2, newBuffer); // TODO: is this necessary???
}else{
if (points.size() != 0){
texCoord.rewind();
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(1.0f);

if (totalLength == 0){
for (FloatWrapper l : lengths){
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(1.0f);
}
}else{
for (FloatWrapper l : lengths){
float length = l.getValue() / totalLength;
texCoord.put(length);
texCoord.put(0.0f);
texCoord.put(length);
texCoord.put(1.0f);
}
}
texCoord.flip();
}
pvb.updateData(texCoord); // TODO: is this necessary???
//mesh.setBuffer(Type.TexCoord, 2, texCoord); // TODO: shouldn't it work without this???
//mesh.updateCounts(); // TODO: is this necessary???
}
mesh.updateCounts(); // TODO: is this necessary???
behaviour.removedPoint(this, index, oldPoint);
return oldPoint;
}

/**
* Recomputes the texture coordinates by using the line segments length.
* Call this after modifying the points directly through getPoint() or getPoints()
*/
public void recomputeTextureCoordinates(){
totalLength = 0;
lengths.clear();
if (points.size() > 1){
Vector3f distance = new Vector3f();
Iterator<Vector3f> itPoint = points.iterator();
Vector3f point = itPoint.next();
while (itPoint.hasNext()){
Vector3f nextPoint = itPoint.next();
totalLength += nextPoint.subtract(point, distance).length();
lengths.add(new FloatWrapper(totalLength));
point = nextPoint;
}
}

VertexBuffer pvb = mesh.getBuffer(Type.TexCoord);
FloatBuffer texCoord = (FloatBuffer) pvb.getData();
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(1.0f);
if (totalLength == 0){
for (FloatWrapper l : lengths){
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(1.0f);
}
}else{
for (FloatWrapper l : lengths){
float length = l.getValue() / totalLength;
texCoord.put(length);
texCoord.put(0.0f);
texCoord.put(length);
texCoord.put(1.0f);
}
}

texCoord.flip();
mesh.setBuffer(Type.TexCoord, 2, texCoord);
}

/**
* Adds a point to the line.
* @param point Point you want to add.
* @param width Width of the point.
*/
public void addPoint(Vector3f point, float width){
if (point == null)
throw new IllegalArgumentException("LineNodes.addPoint: point may not be null");
halfWidths.add(width/2);
points.add(point);

// positions
VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position);
FloatBuffer positions = (FloatBuffer) pvb.getData();
if (positions.limit() == positions.capacity()){ // need to create new buffer
FloatBuffer newBuffer = BufferUtils.createFloatBuffer(positions.capacity() * 2); // reasonable assumption: 2 times the previous size
newBuffer.limit(positions.limit() + 6); // two new vertices added, hence six new coordinates
mesh.setBuffer(Type.Position, 3, newBuffer); // TODO: is this necessary???
}else{
positions.limit(positions.limit() + 6); // two new vertices added, hence six new coordinates
pvb.updateData(positions); // TODO: is this necessary???
//mesh.setBuffer(Type.Position, 3, positions); // TODO: shouldn't it work without this???
mesh.updateCounts(); // TODO: is this necessary???
}

// indexes
pvb = mesh.getBuffer(VertexBuffer.Type.Index);
IntBuffer indexes = (IntBuffer) pvb.getData();
int indexLimit = indexes.limit();
if (indexLimit == indexes.capacity()){ // need to create new buffer
IntBuffer newBuffer = BufferUtils.createIntBuffer(indexes.capacity() * 2); // reasonable assumption: 2 times the previous size
newBuffer.limit(indexLimit + 2); // two new vertices added, hence two new indexes
newBuffer.put(indexes); // copy old buffer into new buffer
newBuffer.put(indexLimit); // add first new index
newBuffer.put(indexLimit + 1); // add second new index
newBuffer.flip();
mesh.setBuffer(Type.Index, 3, newBuffer); // TODO: is this necessary
}else{
indexes.limit(indexLimit + 2); // two new vertices added
indexes.rewind();
indexes.put(indexLimit, indexLimit); // add first new index
indexes.put(indexLimit + 1, indexLimit + 1); // add second new index
indexes.rewind(); // is the position set when using absolute puts??? doing this for safety
pvb.updateData(indexes); // TODO: is this necessary
//mesh.setBuffer(Type.Index, 3, indexes); // TODO: shouldn't it work without this???
mesh.updateCounts(); // TODO: is this necessary
}

// texcoord
if (points.size() > 1){
totalLength += (point.subtract(points.get(points.size()-2)).length());
lengths.add(new FloatWrapper(totalLength));
}

pvb = mesh.getBuffer(VertexBuffer.Type.TexCoord);
FloatBuffer texCoord = (FloatBuffer) pvb.getData();
int texCoordLimit = texCoord.limit();
if (texCoordLimit == texCoord.capacity()){ // need to create new buffer
FloatBuffer newBuffer = BufferUtils.createFloatBuffer(texCoord.capacity() * 2); // reasonable assumption: 2 times the previous size
newBuffer.limit(texCoordLimit + 4); // two new vertices, hence 4 new texture coordinates
newBuffer.put(0.0f);
newBuffer.put(0.0f);
newBuffer.put(0.0f);
newBuffer.put(1.0f);

if (totalLength == 0){
for (FloatWrapper l : lengths){
newBuffer.put(0.0f);
newBuffer.put(0.0f);
newBuffer.put(0.0f);
newBuffer.put(1.0f);
}
}else{
for (FloatWrapper l : lengths){
float length = l.getValue() / totalLength;
newBuffer.put(length);
newBuffer.put(0.0f);
newBuffer.put(length);
newBuffer.put(1.0f);
}
}

newBuffer.flip();
mesh.setBuffer(Type.TexCoord, 2, newBuffer); // TODO: is this necessary ???
}else{
texCoord.limit(texCoordLimit + 4); // two new vertices, hence 4 new texture coordinates
texCoord.rewind();
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(1.0f);

if (totalLength == 0){
for (FloatWrapper l : lengths){
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(0.0f);
texCoord.put(1.0f);
}
}else{
for (FloatWrapper l : lengths){
float length = l.getValue() / totalLength;
texCoord.put(length);
texCoord.put(0.0f);
texCoord.put(length);
texCoord.put(1.0f);
}
}

texCoord.flip();
pvb.updateData(texCoord); // TODO: is this necessary ???
//mesh.setBuffer(Type.TexCoord, 2, texCoord); // TODO: shouldn't it work without this???
mesh.updateCounts(); // TODO: is this necessary ???
}
behaviour.addedPoint(this);
}

/**
* Returns the Length of this line.
* @return length of this line.
*/
public float getTotalLength(){
return this.totalLength;
}

/**
* LineBehaviours define the billboarding algorithm of the line.
*/
public interface LineBehaviour{
/**
* Update is called when the line is rendered. The billboarding algorithm should be implemented here.
* @param line Line which is billboarded.
* @param rm RenderManager
* @param vp ViewPort
*/
public void update(LineControl line, RenderManager rm, ViewPort vp);
/**
* Called whenever a point was added to the line. The newly added point has the biggest index in the line's points list.
* In this method precomputations are done to make update() faster.
* @param line Line which was altered
*/
public void addedPoint(LineControl line);
/**
* Called whenever a point was removed from the line.
* In this method precomputations are done to make update() faster.
* @param line Line which was altered.
* @param index Index at which the point WAS in the line's point list.
* @param oldPoint point which was removed from the line.
*/
public void removedPoint(LineControl line, int index, Vector3f oldPoint);
/**
* Called whenever set was called on the line.
* In this method precomputations are done to make update() faster.
*/
public void set();
}

/**
* Algo1CamDirBB implements a billboarding algorithm for lines.
* It billboards the line according to the camera's look direction. Otherwise it's the same as Algo1CamPosBB
*/
public static class Algo1CamDirBB implements LineBehaviour{

@Override
public void update(LineControl line, RenderManager rm, ViewPort vp){
Mesh mesh = ((Geometry)line.getSpatial()).getMesh();
VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position);
List<Vector3f> points = line.getPoints();
List<Float> halfWidths = line.getHalfWidths();

FloatBuffer positions = (FloatBuffer) pvb.getData();
positions.rewind();

Vector3f direction = rm.getCurrentCamera().getDirection();
line.getSpatial().getWorldRotation().inverse().multLocal(direction);

if (points.size() < 2)
return;

Iterator<Vector3f> itPoint = points.iterator();
Iterator<Float> itHalfWidth = halfWidths.iterator();

Vector3f axisBC = new Vector3f();
Vector3f point = itPoint.next();

float halfWidth = itHalfWidth.next();
Vector3f nextPoint = itPoint.next();
nextPoint.subtract(point, axisBC);
axisBC.normalizeLocal();

Vector3f axis = axisBC.cross(direction);
axis.normalizeLocal();

Vector3f vertex = point.add(axis.multLocal(halfWidth));
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axis, vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);

while (itPoint.hasNext()){
point = nextPoint;
nextPoint = itPoint.next();
halfWidth = itHalfWidth.next();
nextPoint.subtract(point, axis).normalizeLocal();
axisBC.addLocal(axis).crossLocal(direction).normalizeLocal();
point.add(axisBC.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axisBC, vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
axisBC.set(axis);

}

halfWidth = itHalfWidth.next();
axisBC.cross(direction, axis);

axis.normalizeLocal();
nextPoint.add(axis.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
nextPoint.subtract(axis, vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);


//mesh.updateBound(); // TODO: check if this is necessary
positions.flip();
pvb.updateData(positions);
line.getSpatial().updateModelBound();
}

@Override
public void addedPoint(LineControl line){

}

@Override
public void removedPoint(LineControl line, int index, Vector3f oldPoint){

}

@Override
public void set() {

}
}

/**
* Algo1CamPosBB implements a billboarding algorithm for lines.
* It billboards the line according to the cameras position relative to the points. Otherwise it's the same as Algo1CamDirBB
*/
public static class Algo1CamPosBB implements LineBehaviour{
Vector3f camPos;

public Algo1CamPosBB(){
this.camPos = new Vector3f();
}

@Override
public void update(LineControl line, RenderManager rm, ViewPort vp){
Mesh mesh = ((Geometry)line.getSpatial()).getMesh();
VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position);
List<Vector3f> points = line.getPoints();
List<Float> halfWidths = line.getHalfWidths();

FloatBuffer positions = (FloatBuffer) pvb.getData();
positions.rewind();

if (points.size() < 2)
return;

camPos.set(rm.getCurrentCamera().getLocation());
camPos.subtractLocal(line.getSpatial().getWorldTranslation());

Iterator<Vector3f> itPoint = points.iterator();
Iterator<Float> itHalfWidth = halfWidths.iterator();

Vector3f axisBC = new Vector3f();
Vector3f point = itPoint.next();
Vector3f direction = point.subtract(camPos);

float halfWidth = itHalfWidth.next();
Vector3f nextPoint = itPoint.next();
nextPoint.subtract(point, axisBC);
axisBC.normalizeLocal();

Vector3f axis = axisBC.cross(direction);
axis.normalizeLocal();

Vector3f vertex = point.add(axis.multLocal(halfWidth));
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axis, vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);

while (itPoint.hasNext()){
point = nextPoint;
point.subtract(camPos, direction);
nextPoint = itPoint.next();
halfWidth = itHalfWidth.next();
nextPoint.subtract(point, axis).normalizeLocal();
axisBC.addLocal(axis).crossLocal(direction).normalizeLocal();
point.add(axisBC.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axisBC, vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
axisBC.set(axis);
}

nextPoint.subtract(camPos, direction);

halfWidth = itHalfWidth.next();
axisBC.cross(direction, axis);

axis.normalizeLocal();
nextPoint.add(axis.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
nextPoint.subtract(axis, vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);

//mesh.updateBound(); // TODO: check if this is necessary
positions.flip();
pvb.updateData(positions);
line.getSpatial().updateModelBound();
}

@Override
public void addedPoint(LineControl line){

}

@Override
public void removedPoint(LineControl line, int index, Vector3f oldPoint){

}

@Override
public void set() {

}
}

/**
* Algo2CamDirBB implements a billboarding algorithm for lines.
* It billboards the line according to the camera's look direction.
*/
public static class Algo2CamDirBB implements LineBehaviour{

@Override
public void update(LineControl line, RenderManager rm, ViewPort vp) {
Mesh mesh = ((Geometry)line.getSpatial()).getMesh();
VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position);
List<Vector3f> points = line.getPoints();
List<Float> halfWidths = line.getHalfWidths();

if (points.size() < 2)
return;

FloatBuffer positions = (FloatBuffer) pvb.getData();
positions.rewind();

Iterator<Vector3f> itPoints = points.iterator();
Iterator<Float> itHalfWidths = halfWidths.iterator();

Vector3f axisBC = new Vector3f();
Vector3f point = itPoints.next();
Vector3f nextPoint = itPoints.next();

Vector3f direction = rm.getCurrentCamera().getDirection();
line.getSpatial().getWorldRotation().inverse().multLocal(direction);

float halfWidth = itHalfWidths.next();
nextPoint.subtract(point, axisBC);

Vector3f axis = axisBC.cross(direction);
axis.normalizeLocal();

Vector3f vertex = point.add(axis.mult(halfWidth));
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axis.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);

Vector3f axis2 = new Vector3f();
while (itPoints.hasNext()){
point = nextPoint;
nextPoint = itPoints.next();
halfWidth = itHalfWidths.next();

nextPoint.subtract(point, axisBC);

axisBC.cross(direction, axis2);
axis2.normalizeLocal();
axis.addLocal(axis2);

point.add(axis.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axis.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
axis.set(axis2);
}

halfWidth = itHalfWidths.next();

axis.normalizeLocal();
nextPoint.add(axis.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
nextPoint.subtract(axis.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);

//mesh.updateBound(); // TODO: check if this is necessary
positions.flip();
pvb.updateData(positions);

line.getSpatial().updateModelBound();
}

@Override
public void addedPoint(LineControl line) {

}

@Override
public void removedPoint(LineControl line, int index, Vector3f oldPoint) {

}

@Override
public void set() {

}
}

/**
* Algo2CamDirBB implements a billboarding algorithm for lines.
* It billboards the line according to the camera's look direction.
* It normalizes the axis which is used to set the vertices at that point.
*/
public static class Algo2CamDirBBNormalized implements LineBehaviour{

@Override
public void update(LineControl line, RenderManager rm, ViewPort vp) {
Mesh mesh = ((Geometry)line.getSpatial()).getMesh();
VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position);
List<Vector3f> points = line.getPoints();
List<Float> halfWidths = line.getHalfWidths();

if (points.size() < 2)
return;

FloatBuffer positions = (FloatBuffer) pvb.getData();
positions.rewind();

Iterator<Vector3f> itPoints = points.iterator();
Iterator<Float> itHalfWidths = halfWidths.iterator();

Vector3f axisBC = new Vector3f();
Vector3f point = itPoints.next();
Vector3f nextPoint = itPoints.next();

Vector3f direction = rm.getCurrentCamera().getDirection();
line.getSpatial().getWorldRotation().inverse().multLocal(direction);

float halfWidth = itHalfWidths.next();
nextPoint.subtract(point, axisBC);

Vector3f axis = axisBC.cross(direction);
axis.normalizeLocal();

Vector3f vertex = point.add(axis.mult(halfWidth));
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axis.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);

Vector3f axis2 = new Vector3f();
while (itPoints.hasNext()){
point = nextPoint;
nextPoint = itPoints.next();
halfWidth = itHalfWidths.next();

nextPoint.subtract(point, axisBC);

axisBC.cross(direction, axis2);
axis.addLocal(axis2);
axis.normalizeLocal();

point.add(axis.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axis.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
axis.set(axis2);
}

halfWidth = itHalfWidths.next();

axis.normalizeLocal();
nextPoint.add(axis.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
nextPoint.subtract(axis.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);

//mesh.updateBound(); // TODO: check if this is necessary
positions.flip();
pvb.updateData(positions);

line.getSpatial().updateModelBound();
}

@Override
public void addedPoint(LineControl line) {

}

@Override
public void removedPoint(LineControl line, int index, Vector3f oldPoint) {

}

@Override
public void set() {

}
}

/**
* Algo2CamPosBBNormalized implements a billboarding algorithm for lines.
* It billboards the line according to the camera's position relative to the points.
* It normalizes the axis which is used to set the vertices at that point.
* Probably the closest to Unity3d's LineRenderer implementation
*/
public static class Algo2CamPosBBNormalized implements LineBehaviour{

Vector3f camPos;

public Algo2CamPosBBNormalized(){
camPos = new Vector3f();
}

@Override
public void update(LineControl line, RenderManager rm, ViewPort vp) {
Mesh mesh = ((Geometry)line.getSpatial()).getMesh();
VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position);
List<Vector3f> points = line.getPoints();
List<Float> halfWidths = line.getHalfWidths();

if (points.size() < 2)
return;

camPos.set(rm.getCurrentCamera().getLocation());
camPos.subtractLocal(line.getSpatial().getWorldTranslation());

FloatBuffer positions = (FloatBuffer) pvb.getData();
positions.rewind();

Iterator<Vector3f> itPoints = points.iterator();
Iterator<Float> itHalfWidths = halfWidths.iterator();

Vector3f axisBC = new Vector3f();
Vector3f point = itPoints.next();
Vector3f nextPoint = itPoints.next();

Vector3f direction = point.subtract(camPos);

float halfWidth = itHalfWidths.next();
nextPoint.subtract(point, axisBC);

Vector3f axis = axisBC.cross(direction);
axis.normalizeLocal();

Vector3f vertex = point.add(axis.mult(halfWidth));
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axis.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);

Vector3f axis2 = new Vector3f();
while (itPoints.hasNext()){
point = nextPoint;
nextPoint = itPoints.next();

point.subtract(camPos, direction);
direction.normalizeLocal(); // TODO: check if this is necessary

halfWidth = itHalfWidths.next();

nextPoint.subtract(point, axisBC);

axisBC.cross(direction, axis2);
axis.addLocal(axis2);
axis.normalizeLocal();

point.add(axis.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axis.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
axis.set(axis2);
}

halfWidth = itHalfWidths.next();

axis.normalizeLocal();
nextPoint.add(axis.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
nextPoint.subtract(axis.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);

//mesh.updateBound(); // TODO: check if this is necessary
positions.flip();
pvb.updateData(positions);

line.getSpatial().updateModelBound();
}

@Override
public void addedPoint(LineControl line) {

}

@Override
public void removedPoint(LineControl line, int index, Vector3f oldPoint) {

}

@Override
public void set() {

}
}
}


/*
unoptimized version, loop could run in parallel
Vector3f axisBC = Vector3f.ZERO;
Vector3f point = points.get(0);

float halfWidth = halfWidths.get(0);
points.get(1).subtract(point, axisBC);

Vector3f axis = axisBC.cross(direction);
axis.normalizeLocal();

Vector3f vertex = point.add(axis.mult(halfWidth));
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axis.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);

int lastPoint = this.points.size() - 1;
Vector3f axis2 = Vector3f.ZERO;
for (int i = 1; i < lastPoint; i++){
halfWidth = halfWidths.get(i);
Vector3f axe1 = points.get(i).subtract(points.get(i-1)).normalize();
Vector3f axe2 = points.get(i+1).subtract(points.get(i)).normalize();
Vector3f normal = axe1.add(axe2).cross(direction);
normal.normalizeLocal();
points.get(i).add(normal.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
points.get(i).subtract(normal.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
}

point = points.get(lastPoint);
halfWidth = halfWidths.get(lastPoint);

point.subtract(points.get(lastPoint-1), axisBC);

axis = axisBC.cross(direction);

axis.normalizeLocal();
point.add(axis.mult(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
point.subtract(axis.multLocal(halfWidth), vertex);
positions.put(vertex.x).put(vertex.y).put(vertex.z);
*/

/*
unoptimized version of alternative algorithm, loop could run in parallel
Vector3f axisAB = Vector3f.ZERO;
Vector3f axisBC = Vector3f.ZERO;
for (int i = 0; i < this.points.size(); i++)
{
float halfWidth = halfWidths.get(i);

if (i > 0)
points.get(i).subtract(points.get(i-1), axisAB);
else
axisAB = Vector3f.ZERO;

if (i + 1 < (int) points.size())
points.get(i+1).subtract(points.get(i), axisBC);
else
axisBC = Vector3f.ZERO;

Vector3f axis1 = axisAB.cross(direction); // consider normalizing axis1
Vector3f axis2 = axisBC.cross(direction); // consider normalizing axis2
axis1.addLocal(axis2).normalize(); // consider not normalizing axis1
//Vector3f axis = axis1.add(axis2);
//axis.normalize();

//Vector3f vertex = points.get(i).add(axis.mult(halfWidth));
Vector3f vertex = points.get(i).add(axis1.mult(halfWidth));
positions.put(vertex.x).put(vertex.y).put(vertex.z);
//vertex = points.get(i).subtract(axis.mult(halfWidth));
vertex = points.get(i).subtract(axis1.mult(halfWidth));
positions.put(vertex.x).put(vertex.y).put(vertex.z);
//vertices[i*2] = (points.get(i).add(axis.mult(halfWidth)));
//vertices[i*2 + 1] = (points.get(i).subtract(axis.mult(halfWidth)));
}
*/[/java]

[java]package cvlad.plugins;

import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
* Attach this to a node. A trail will start following it.
* The line which you need to inject should be attached to a Geometry node, which should be attached to the root.
* Moving that Geometry node around has rather weird results, since the points for the trail are computed in world space.
* @author cvlad
*/
public class TrailControl extends AbstractControl {

float lifeSpan = 5;
LinkedList<Vector3f> bin = new LinkedList<Vector3f>();
LinkedList<Double> birthTime = new LinkedList<Double>();
LineControl line;
Vector3f lastSpawnPos;
Vector3f difference = new Vector3f();
float segmentLength = 0.1f;
float segmentLengthSqr = segmentLength*segmentLength;
float startWidth = 1;
float endWidth = 1;
double localTime = 0;

/**
* Constructor
* @param line Inject a line which is attached to a Geometry node which is attached to the root node.
*/
public TrailControl(LineControl line){
this.line = line;
}

@Override
public void setSpatial(Spatial spatial){
super.setSpatial(spatial);
lastSpawnPos = spatial.getWorldTranslation().clone();
}

@Override
protected void controlUpdate(float f) {
if (this.line == null){
LineControl l = this.spatial.<LineControl>getControl(LineControl.class);
if (l == null)
return;
this.line = l;
}
localTime += f;
Vector3f currentPos = spatial.getWorldTranslation();
lastSpawnPos.subtract(currentPos, difference);
boolean changed = false;
float differenceSqr = difference.lengthSquared();
if (differenceSqr != 0){
if (line.getNumPoints() == 0 || differenceSqr > segmentLengthSqr){
Vector3f newPoint = null;
if (bin.isEmpty())
newPoint = currentPos.clone();
else{
newPoint = bin.remove();
newPoint.set(currentPos);
}
lastSpawnPos.set(newPoint);
line.addPoint(newPoint, startWidth);
birthTime.add(localTime + lifeSpan);
}else{
line.setPoint(line.getNumPoints() - 1, currentPos);
}
changed = true;
}
Iterator<Double> itBirthTime = birthTime.iterator();

while (itBirthTime.hasNext()){
if (localTime >= itBirthTime.next() + lifeSpan){
itBirthTime.remove();
Vector3f removed = line.removePoint(0);
if (bin.size() < line.getNumPoints()){
bin.add(removed);
}
changed = true;
}else
break;
}

if (changed && startWidth != endWidth){
List<Float> widths = line.getHalfWidths();
int widthsSize = widths.size();
widths.clear();
if (widthsSize != 0){
if (widthsSize == 1)
widths.add(startWidth);
else{
int widthSizeMinusOne = widthsSize - 1;
for (int i = 0; i < widthsSize; i++){
widths.add(startWidth*(widthSizeMinusOne - i)/widthSizeMinusOne + endWidth*i/widthSizeMinusOne);
}
}
}
}
}

@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
//throw new UnsupportedOperationException("Not supported yet.");
}

@Override
public Control cloneForSpatial(Spatial sptl) {
TrailControl clone = new TrailControl(sptl.<LineControl>getControl(LineControl.class));
clone.setLifeSpan(this.lifeSpan);
clone.setSegmentLength(this.segmentLength);
sptl.addControl(clone);
return clone;
}

/**
* Sets how fast shall the trail disappear
* @param lifeSpan Lifespan of the points which make up the trail
*/
public void setLifeSpan(float lifeSpan){
this.lifeSpan = lifeSpan;
}

/**
* Gets how fast shall the trail disappear
* @return Lifespan of the points which make up the trail
*/
public float getLifeSpan(){
return this.lifeSpan;
}

/**
* Sets after how much distance shall a new point be generated. The last point which was added
* to the trail will be moved around until a new point is generated. So lowering this value generally
* gives better results.
* @param segmentLength
*/
public void setSegmentLength(float segmentLength){
this.segmentLength = segmentLength;
this.segmentLengthSqr = segmentLength*segmentLength;
}

/**
* Gets after how much distance shall a new point be generated.
* @return after how much distance shall a new point be generated.
*/
public float getSegmentLength(){
return this.segmentLength;
}

/**
* Gets after how much distance shall a new point be generated. Distance is squared.
* @return after how much distance shall a new point be generated. Distance is squared.
*/
public float getSegmentLengthSqr(){
return this.segmentLengthSqr;
}

/**
* Sets the width at the starting point of the trail. So the oldest point in this trail has this width.
* @param startWidth
*/
public void setStartWidth(float startWidth){
this.startWidth = startWidth / 2;
}

public float getStartWidth(){
return this.startWidth * 2;
}

/**
* Sets the width at the ending point of the trail. So the newest point in this trail has this width.
* You know, the point closest to the spatial onto which you attached this control.
* @param endWidth Width at the ending point of the trail.
*/
public void setEndWidth(float endWidth){
this.endWidth = endWidth / 2;
}

/**
* Gets the width at the ending point of the trail. So the newest point in this trail has this width.
* You know, the point closest to the spatial onto which you attached this control.
* @return Width at the ending point of the trail.
*/
public float getEndWidth(){
return this.endWidth * 2;
}
}[/java]
18 Likes

There has been a bug in the TrailControl code I posted. I fixed the code in the first post, it should work now. Sorry about that.



Here is some example code:



[java]package mygame;



import com.jme3.app.SimpleApplication;

import com.jme3.input.KeyInput;

import com.jme3.input.MouseInput;

import com.jme3.input.controls.AnalogListener;

import com.jme3.input.controls.KeyTrigger;

import com.jme3.input.controls.MouseButtonTrigger;

import com.jme3.material.Material;

import com.jme3.material.RenderState.FaceCullMode;

import com.jme3.math.ColorRGBA;

import com.jme3.math.Vector3f;

import com.jme3.renderer.RenderManager;

import com.jme3.scene.Geometry;

import com.jme3.scene.Node;

import com.jme3.scene.shape.Box;



public class Main extends SimpleApplication {



Geometry geom;



public static void main(String[] args) {

Main app = new Main();

app.start();

}



@Override

public void simpleInitApp() {

Box b = new Box(Vector3f.ZERO, 1, 1, 1);

geom = new Geometry("Box", b);



Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");

mat.setColor("Color", ColorRGBA.Blue);

geom.setMaterial(mat);



Geometry trailGeometry = new Geometry();

LineControl line = new LineControl(new LineControl.Algo1CamDirBB(), true);

trailGeometry.addControl(line);

TrailControl trailControl = new TrailControl(line);

geom.addControl(trailControl);



//rootNode.attachChild(trail); // either attach the trail geometry node to the root…



trailGeometry.setIgnoreTransform(true); // or set ignore transform to true. this should be most useful when attaching nodes in the editor

Node test = new Node();

test.attachChild(trailGeometry);

test.setLocalTranslation(new Vector3f(0,2,0)); // without ignore transform this would offset the trail

rootNode.attachChild(test);



mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off);

trailGeometry.setMaterial(mat);



rootNode.attachChild(geom);

initKeys();

}



@Override

public void simpleUpdate(float tpf) {



}



@Override

public void simpleRender(RenderManager rm) {}



private void initKeys() {

inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_J));

inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_K));

inputManager.addMapping("Rotate", new KeyTrigger(KeyInput.KEY_SPACE),

new MouseButtonTrigger(MouseInput.BUTTON_LEFT));



inputManager.addListener(analogListener, new String[]{"Left", "Right", "Rotate"});



}





private AnalogListener analogListener = new AnalogListener() {

public void onAnalog(String name, float value, float tpf) {

if (name.equals("Rotate")) {

geom.rotate(0, valuespeed, 0);

}

if (name.equals("Right")) {

Vector3f v = geom.getLocalTranslation();

geom.setLocalTranslation(v.x + value
speed5, v.y, v.z);

}

if (name.equals("Left")) {

Vector3f v = geom.getLocalTranslation();

geom.setLocalTranslation(v.x - value
speed*5, v.y, v.z);

}

}

};

}

[/java]

1 Like

Wow cool, that is a really nice effect. Thanks for contributing it!

It’s even got documentation :slight_smile:

This is a nice contribution, thanks a lot! I will try it out when I get some time over :slight_smile:

Really nice. Trails is something I might use.



Thanks.

Nice.

Oh brilliant, I was thinking of making something like that!

Thanks a lot mate!



If you want to make this contribution more visible to prospective users and on par with the jME3 core, please consider making a plugin.

Actually this thing could be candidate to core.

I didn’t look the code in detail but the trailing feature could definitely be a core feature.

@normen, @Sploreg, @Momoko_Fan, @pspeed, what do you think?

Hi @cvlad! I’ve been playing around with this a little and I do like it! I have a little problem with it when using textures though and maybe you can provide me with som answers.



How is the texture coordinates generated? Is the start and end of the mesh supposed to always have the same texture coordinate? The behavior I’m looking for is that a texture is stretched across the whole mesh with no wrapping, is this possible right now? Is there any way for me to know where on the line mesh I am in a shader?



http://i.imgur.com/F3Fb0.png



By stretching an image as the one above using alpha blending one could make a nice trail which will fade out.

@kwando

There should be no wrapping, unless I messed up again (which I actually did when removing points, I fixed that now too, code above is updated). I gave it a shot with your texture, seems to work here:



[java]package mygame;



import com.jme3.app.SimpleApplication;

import com.jme3.input.KeyInput;

import com.jme3.input.MouseInput;

import com.jme3.input.controls.AnalogListener;

import com.jme3.input.controls.KeyTrigger;

import com.jme3.input.controls.MouseButtonTrigger;

import com.jme3.material.Material;

import com.jme3.material.RenderState.FaceCullMode;

import com.jme3.math.ColorRGBA;

import com.jme3.math.Vector3f;

import com.jme3.renderer.RenderManager;

import com.jme3.scene.Geometry;

import com.jme3.scene.Node;

import com.jme3.scene.shape.Box;



public class Main extends SimpleApplication {



Node geom;



public static void main(String[] args) {

Main app = new Main();

app.start();

}



@Override

public void simpleInitApp() {

Box b = new Box(Vector3f.ZERO, 1, 1, 1);

geom = new Node(); //new Geometry(“Box”, b);



Material mat = new Material(assetManager, “Common/MatDefs/Misc/Unshaded.j3md”);

mat.setColor(“Color”, ColorRGBA.Blue);

//geom.setMaterial(mat);

mat.setTexture(“ColorMap”,assetManager.loadTexture(“Textures/F3Fb0.png”));



Geometry trailGeometry = new Geometry();

LineControl line = new LineControl(new LineControl.Algo1CamDirBB(), true);

trailGeometry.addControl(line);

TrailControl trailControl = new TrailControl(line);

geom.addControl(trailControl);



//rootNode.attachChild(trail); // either attach the trail geometry node to the root…



trailGeometry.setIgnoreTransform(true); // or set ignore transform to true. this should be most useful when attaching nodes in the editor

Node test = new Node();

test.attachChild(trailGeometry);

test.setLocalTranslation(new Vector3f(0,2,0)); // without ignore transform this would offset the trail

rootNode.attachChild(test);



mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off);

trailGeometry.setMaterial(mat);



rootNode.attachChild(geom);

initKeys();

}



@Override

public void simpleUpdate(float tpf) {



}



@Override

public void simpleRender(RenderManager rm) {}



private void initKeys() {

inputManager.addMapping(“Left”, new KeyTrigger(KeyInput.KEY_J));

inputManager.addMapping(“Right”, new KeyTrigger(KeyInput.KEY_K));

inputManager.addMapping(“Rotate”, new KeyTrigger(KeyInput.KEY_SPACE),

new MouseButtonTrigger(MouseInput.BUTTON_LEFT));



inputManager.addListener(analogListener, new String[]{“Left”, “Right”, “Rotate”});



}





private AnalogListener analogListener = new AnalogListener() {

public void onAnalog(String name, float value, float tpf) {

if (name.equals(“Rotate”)) {

geom.rotate(0, valuespeed, 0);

}

if (name.equals(“Right”)) {

Vector3f v = geom.getLocalTranslation();

geom.setLocalTranslation(v.x + value
speed5, v.y, v.z);

}

if (name.equals(“Left”)) {

Vector3f v = geom.getLocalTranslation();

geom.setLocalTranslation(v.x - value
speed*5, v.y, v.z);

}

}

};

}[/java]



Screenshot:

http://i.imgur.com/E6pB5.png





And here’s a small attempt to explain the idea behind the texture coordinates’ computation:

http://i.imgur.com/jvMr7.png

As you can see texture coordinates range from zero to one, so yeah, there should be no wrapping.



However I added a getter for totalLength, so by pushing that into a shader and multiplying totalLength with the texcoord.x in that shader you can find out how long in your world units (meters?) your line is at that vertex/pixel if you need that for some reason.





@erlend_sh I will try and do that, unless it’s made part of the core anyway. I didn’t know if it was ok for me to spam the contribution center with small contributions like this, since I have a few other things planned too. Or should I make one big plugin with all my stuff in it?

2 Likes

It works if I use thinner lines, so it was probably something fishy with my code… :stuck_out_tongue:

Thanks for the great explanation, now I know how it is supposed to work :slight_smile:

This is a fine contribution, thanks!

I will try and do that, unless it’s made part of the core anyway. I didn’t know if it was ok for me to spam the contribution center with small contributions like this, since I have a few other things planned too. Or should I make one big plugin with all my stuff in it?
Even if it has a chance at core it's best to start it off as a plugin anyhow because we need to get to know the code properly first.

As for the "other things planned", well, start by making this plugin. Then you either extend that plugin with more functionality if it's directly related, or you just contribute multiple smaller plugins if they don't have functionality in common.

@nehon I think it is a feature lots of people would use, I can think of many uses for it. So yea I think it could be a core candidate. It’s not that much code so it won’t take long to review it and see how it works and what needs to change. I guess it boils down to: do we fully support it in core, or partially support it as a plugin. Maybe being that we are trying to get a stable (non-beta) 3.0 release out, it can be a plugin for now, then get pulled into core for 3.1.

I was looking for these kind of effects!!! I just tested the sampler code a few minutes ago! very nice!!!

Wow, two things: your voice sounds exactly like @androlo 's! And the other is: gratz for the job man, looks really, really cool, I can also think of some uses of this! ^^

How does it compare vs. the TrailMesh from jME2?

@Momoko_Fan said:
How does it compare vs. the TrailMesh from jME2?


Looking at the code over here: http://code.google.com/p/jmonkeyengine/source/browse/trunk/src/com/jmex/effects/TrailMesh.java?r=4089
Performance of my implementation is probably worse, because of the way I compute texture coordinates.
And because he only allocates memory for the buffers once, though amortized it shouldn't really be a problem with the way I did it.
Well, he uses get(int) on LinkedList though.

As for flexibility, I implemented trails on top of lines, so it should be more flexible.
His trail implementation has FacingMode, UpdateMode and per point adjustable width, which my trail implementation does not have yet, but this could be easily added, since lines already have width per points and swappable billboarding behaviors.

Both our implementations don't cache the axis from one point to another when updating, for which I wrote the methods in the behavior interface, but did not yet change the behaviors' implementations.

Now the question at hand is what to do about the texture coordinates. I opted for leaving the "correct" way in there, but using the dependency injection/strategy pattern as I did with the billboarding behavior should be applicable, too, so if you want to plug in a cheaper way of computing them, that could be possible.

Overall I don't know how much ms these implementations take compared to each other, I can't imagine there being a lot difference, which is why I posted the source as it is.

@cvlad I’m trying to get this working following the mouse pointer while left mouse button is held down. Not sure what I’m missing but any chance you can help me out? Other than the input change and cam move everything is the same from your most recent example. It worked fine before trying to change the input.

[java]
package lineTracer;

import com.jme3.app.SimpleApplication;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;

public class LineTracerTestMain extends SimpleApplication {

Node geom;
public static final Quaternion YAW180   = new Quaternion().fromAngleAxis(FastMath.PI  ,   new Vector3f(0,1,0));

public static void main(String[] args) {
    LineTracerTestMain app = new LineTracerTestMain();
    app.start();
}

@Override
public void simpleInitApp() {
    Box b = new Box(Vector3f.ZERO, 1, 1, 1);
    geom = new Node(); //new Geometry("Box", b);

    Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat.setColor("Color", ColorRGBA.Red);
    //geom.setMaterial(mat);
    mat.setTexture("ColorMap",assetManager.loadTexture("Textures/lineTracer2.png"));

    Geometry trailGeometry = new Geometry();
    LineControl line = new LineControl(new LineControl.Algo1CamDirBB(), true);
    trailGeometry.addControl(line);
    TrailControl trailControl = new TrailControl(line);
    geom.addControl(trailControl);

    //rootNode.attachChild(trailGeometry);  // either attach the trail geometry node to the root…

    trailGeometry.setIgnoreTransform(true); // or set ignore transform to true. this should be most useful when attaching nodes in the editor
    Node test = new Node();
    test.attachChild(trailGeometry);
    test.setLocalTranslation(new Vector3f(0,2,0));  // without ignore transform this would offset the trail
    rootNode.attachChild(test);

    mat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off);
    trailGeometry.setMaterial(mat);

    rootNode.attachChild(geom);
    initKeys();
    flyCam.setEnabled(false);
    cam.setLocation(new Vector3f(0,0,30));
    cam.setRotation(YAW180);
}

@Override
public void simpleUpdate(float tpf) {

}

@Override
public void simpleRender(RenderManager rm) {}

private void initKeys() {
    inputManager.addMapping("drag", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
    inputManager.addListener(analogListener, new String[]{"drag"});
}


private AnalogListener analogListener = new AnalogListener() {
    public void onAnalog(String name, float value, float tpf) {
        if (name.equals("drag")) {
            Vector2f mouseCoords = inputManager.getCursorPosition();
            Vector3f location = cam.getWorldCoordinates(mouseCoords, 25).clone();
            geom.setLocalTranslation(location);
        }
    }
};

}
[/java]

Scratch the above. I figured out that I was just setting it to a point too close and found a better way of grabbing a point to move it too. Now my problem is I can’t get rid of the black background around the line and I’m not seeing where its coming from. I’m sure its some sort of rendering setting but I can’t find it. I want the same effect as how the last example looks on the black scene but on any background.