polyline fixed width

Started by Harald, March 24, 2021, 11:50:41 AM

Previous topic - Next topic

Harald

TL,DR: i am currently programming a trackline to a moving object. right now I am using a polyline with a fixed width. If I zoom in or out on that fixed length the object obviously gets smaller and bigger accordingly, but the polyline keeps exactly the same width. Is there a way to fix the width rendering of a polyline to an object / a plane?

I am currently working on a gps system for agriculture and try to visualize the process with jPCT. The code is getting executed on an android device. The visualization covers a arrow shaped object that is moving accordingly to gps positions that get translated into a 3D World position. The position gets transformed by things that should not be taken into consideration in this problem. In the End an Object3D is moving through the rendered world.

To keep track of the progress (the are that the object already covered) I tried several things:
1. I created a polyline with a width that is about hte same of the object width. When moving the polyline will update its array with new points. So far it looked pretty good but if replace the camera (eg. zoom in or out) the polyline width does not change but the object appears smaller  / bigger so that the polyline / object ratio is not the same anymore.
2. To avoid this problem i started to create two polylines on each end of the object and creted object3Ds with two triangles in runtime. this got the job done for the correct ratio but is killing the fps. I also read on the wiki that creating obejcts in runtime should be strictly avoided so I think this will not suit for a solution as well.

So in the end I want to ask if there is a way to get the width of a polyline fixed to the render distance to that polyline or something simular?

Also if my explanations are not understandable just ask.

EgonOlsen

Polyline has a setWidth() method. Can't you just change the width according to the distance? Or maybe I don't fully understand the problem...

Harald

Accuracy is key here. My Option 1 of just scaling the width so that is fits the object was made by trial and error.
So far I cant see a relation between the object width and the polyline width. But to show a precise visualization of the process everything need to fit perfectly

To say it in different words. Both the object and the polyline do not change in size while zooming in or out. But by zooming the rendered object on screen get smaller / bigger while the polyline width stays the same size on the screen. Is there a relation / scaling factor I am not seeing here?

But you are right. I could scale the polyline at the same time I am zooming in or out. 2 Problems do come with it:

1. like I said I dont know the relation
2. Is way more important. I have created my own Object called EndlessPolyline, because as far as I understood it, on the creation of the Polyline I have to give a fixed size. If I exceed the size of the array that holds the points of the polyline I get an overflow error. So what I did is only holding the last 10 points in one Polyline and create a new one the points were filled. Hard to explain.
What I want to say is, that I have only a pointer to the last 10 points of the endless polyline. So If i try to rescale i would only rescale the last few points i captured and all the others would stay the same.

I might be missing some functionality about your polyline class or how to use it.

to add the code that i used to create an endlessPolyline (this is in Kotlin and not java but can be converted. But reading this should not be to hard)

import com.threed.jpct.*

class EndlessTrackLine(private val world: World? = null, private val color: RGBColor = RGBColor(100,100,100,255), private val width: Float = 1f){

    val line = mutableListOf<SimpleVector>() //this is holding all points of the line
    private var polyline: Polyline? = null //this is holding the last ARRAY_SIZE points of the line (to render)
   
    private var currentInterval = 0

    private var cutPoints = 0

    private fun createTrackLine(toFixTo: Object3D? = null){
        polyline = Polyline(Array(ARRAY_SIZE +1) {SimpleVector()}, color)
        polyline!!.width = width
        polyline!!.isPointMode = false
        world!!.addPolyline(polyline)
        if (toFixTo != null){
            polyline!!.parent = toFixTo
        }
    }

    fun trackMovement(toFollow: Object3D, childObject: Object3D? = null, offset: Float = 0f){
        if (polyline == null){
            createTrackLine(toFollow)
        }
        //this makes sure thing will not get overloaded with points
        //todo: make this corresponding to the tractor speed and not the time / fps passed
        if (currentInterval == TRACK_INTERVAL){
            //a mutable list gets filled with current points and will store ALL OF THEM
            if (childObject != null){
                //this m
                val xCorrection = offset * toFollow.rotationMatrix.zAxis.z
                val zCorrection = offset * toFollow.rotationMatrix.zAxis.x
                val trackLineCurrentPositionLeft = SimpleVector( childObject.transformedCenter.x + xCorrection, LINE_OFFSET,  childObject.transformedCenter.z - zCorrection)
                line.add(trackLineCurrentPositionLeft)
            }else{
                line.add(SimpleVector(toFollow.transformedCenter.x, LINE_OFFSET, toFollow.transformedCenter.z))
            }

            //this sets the cut points further up if the end of the array is reached
            //this also recreates the trackline so that the old one stays in the world and a new one gets created
            if (line.size - cutPoints == ARRAY_SIZE){
                cutPoints += ARRAY_SIZE
                createTrackLine()
            }
            //a temporary array gets filled by the list so that it can be put inside the polyline
            val temporaryArray = if (line.size < ARRAY_SIZE){
                line.toTypedArray()
            }else {
                line.drop(cutPoints -2).toTypedArray()
            }

            //the endless line will update a polyline until the (not scalable array) is filled
            //if the polyline is filled it will refresh the polyline with a new one and start over

            //this updates the polyline with the temporary array
            polyline!!.update(temporaryArray)

            currentInterval = 0
        }else{
            currentInterval++
        }
    }
    companion object{
        const val LINE_OFFSET = -0.3f
        const val TRACK_INTERVAL = 10
        const val ARRAY_SIZE = 10
    }

}


EgonOlsen

Disclaimer: In the following, I assume that by "zoom in/out" you mean to move the camera away or towards the object. Not changing the camera's FOV.

The scaling is linear in perspective projection. I.e. if you take the distance from the camera to the object you can calculate the width of the polyline by dividing some magic constant (you have to figure out a value that fits your needs) by that distance and you should get a matching width. As for your solution with the different patches joined to create one single line: You could scale each patch according to the distance of that patch to the camera and use the approach mentioned above to calculate a width.  This will never be 100% accurate, because each patch can only have one width (because a line isn't a proper 3D object) but it might be good enough.

Harald

what you assumed was exactly what i meant.

But with the information i just got and some further tests (zooming pretty far out and looking from different directions) It semms like the polyline is making some abstract behavior. In the end a strict polyline (at least the way it works right now) will work as a solution to my problem because the user would get visual misinformation to a certain degree and will assume the whole system does not work.

Do you have any other way in mind that will help to track a specific area? My option 2 worked for the precision but killed the framerate with it. Is there a better way to create objects on the fly? can you create a basic object and transform, stretch and copy it to minimize the work that comes with it?

Is there maybe an option that I did not see before? Otherwise I probably have to switch the engine which would kill at least 3 month of work.

Also thank you for your quick replies

EgonOlsen

QuoteIt semms like the polyline is making some abstract behavior.

I'm not sure what you mean by that!?

Also, can you provide a screen shot of your application. It might help to know how it actually looks like to find a better solution.

Harald

here are 5 pictures describing my problem (i added them to two because only two attachements are allowed):

In the fist pictures you can see the behavior of the polyline. Widthout manually fixing the width you will get a different relative width compared to objects. When looking at thr round edges you can see some fragments and depending on the camera angle the width seem to change.

These errors can be fixed by the Option seen in picture 2 (dont mind the small colored lines on the left and right to it) the ratio fits perfectly. But the framerate significantly drops the more "filled area" you get into the picture.

I hope this made things more clear.

PS: sorry for the resolution I had to crop a lot to get it to 64kb

EgonOlsen

These polyline artifacts are indeed strange, but I don't think that you can do anything about them unless they are caused by different width settings in your code. In general, drawing lines using GL on Android isn't very reliable in my experience. I had devices that ignored the width altogether and just rendered single pixel lines no matter what.

How is your "additional object"-approach implemented? By adding a bunch of objects to form a path? In that case, you might want to use one (or a few) large object(s) instead and change the vertices on the fly. That will still impact performance, because the mesh will be constantly changing, but it might be fast enough. I did something similar as a proof of concept a while back for desktop jPCT, but it should work on Android as well, with some minor changes. The idea was to create an object with several, let's say 1000, polygons that are all out of sight at the start.

Then, as the object causing the trail moves, you move polygon by polygon back into view to form the trail. Once you hit the maximum number of polygons in the trail-object, you start moving polygons from the end of the trail to the start. Here's an example with a trail consisting of 1000 polygons: https://www.youtube.com/watch?v=E4I16yxCQDA

Here's the code for this demo:


import java.awt.Color;

import com.threed.jpct.*;
import com.threed.jpct.util.Light;

public class TrailTest {
private static int TRAIL_POLYGONS = 1000;

private World world;
private FrameBuffer buffer;
private Object3D sphere;

private Object3D trail;

public static void main(String[] args) throws Exception {
new TrailTest().loop();
}

public TrailTest() throws Exception {
world = new World();
world.setAmbientLight(100, 100, 100);

sphere = Primitives.getSphere(20, 1f);
sphere.build();
sphere.compile();
sphere.translate(-100, 0, 50);
world.addObject(sphere);

Camera cam = world.getCamera();
cam.setPosition(0, -50, 0);
cam.lookAt(sphere.getTransformedCenter());

Light light = new Light(world);
light.setPosition(new SimpleVector(-100, 0, -150));
light.setAttenuation(-1);

trail = createTrailObject(sphere);
world.addObject(trail);
}

/**
* This looks a bit hacky...it creates an object with a defined number of
* unique polygons that are all out of sight. The vertex controller will
* then take polygons from this soup and arrange them to form the trail.
*
* @param emitter
* @return
*/
private Object3D createTrailObject(Object3D emitter) {
Logger.log("Creating trail object...");
Object3D trail = new Object3D(TRAIL_POLYGONS);
trail.disableVertexSharing();
for (int i = 0; i < TRAIL_POLYGONS / 2; i++) {
trail.addTriangle(new SimpleVector(-1100000 + i, -1200000 + i, -1300000 + i), new SimpleVector(-1400000 + i, -1500000 + i, -1600000 + i), new SimpleVector(
-1700000 + i, -1800000 + i, -1900000 + i));
trail.addTriangle(new SimpleVector(2000000 + i, 2100000 + i, 2200000 + i), new SimpleVector(2300000 + i, 2400000 + i, 2500000 + i), new SimpleVector(2600000 + i,
2700000 + i, 2800000 + i));
}

trail.calcNormals();
trail.getMesh().setVertexController(new TrailBlazer(emitter), false);
trail.forceGeometryIndices(false);
trail.compile(true);
trail.build();

trail.setAdditionalColor(Color.YELLOW);
trail.setCulling(Object3D.CULLING_DISABLED);
trail.setTransparency(0);

return trail;
}

private void loop() throws Exception {
buffer = new FrameBuffer(640, 480, FrameBuffer.SAMPLINGMODE_NORMAL);
buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE);
buffer.enableRenderer(IRenderer.RENDERER_OPENGL);

float cnt = 0;
float y = 0;
float yAdd = -0.1f;

while (!org.lwjgl.opengl.Display.isCloseRequested()) {
SimpleVector lastTrans = sphere.getTranslation();
sphere.translate((float) Math.sin(cnt), yAdd, (float) Math.cos(cnt));
SimpleVector newTrans = sphere.getTranslation();
lastTrans.sub(newTrans);
sphere.getRotationMatrix().setTo(lastTrans.getRotationMatrix());

trail.getMesh().applyVertexController();
trail.touch();

cnt += 0.05f;

if (y < -30) {
yAdd = 0.1f;
} else if (y > 30) {
yAdd = -0.1f;
}
y += yAdd;

buffer.clear(java.awt.Color.BLACK);
world.renderScene(buffer);
world.draw(buffer);
buffer.update();
buffer.displayGLOnly();
Thread.sleep(10);
}
System.exit(0);
}

private static class TrailBlazer extends GenericVertexController {
private int maxPos = TRAIL_POLYGONS / 2;
private int pos = 0;
private Object3D emitter = null;
private SimpleVector p0 = new SimpleVector();
private SimpleVector p1 = new SimpleVector();
private SimpleVector p2 = new SimpleVector();
private SimpleVector p3 = new SimpleVector();
private SimpleVector center = new SimpleVector();
private SimpleVector lastP2 = null;
private SimpleVector lastP3 = null;
private SimpleVector z = new SimpleVector();
private SimpleVector x = new SimpleVector();
private SimpleVector tmp = new SimpleVector();

private static final long serialVersionUID = 1L;

public TrailBlazer(Object3D emitter) {
this.emitter = emitter;
}

public void apply() {
center = emitter.getTransformedCenter(center);
SimpleVector[] dest = this.getDestinationMesh();

z = emitter.getRotationMatrix().getZAxis(z);
x = emitter.getRotationMatrix().getXAxis(z);

if (lastP2 == null) {
tmp.set(center);
tmp.sub(x);
lastP2 = new SimpleVector(tmp);
tmp.add(x);
tmp.add(x);
lastP3 = new SimpleVector(tmp);
}

p0.set(lastP2);
p1.set(lastP3);

int cPos = pos * 6;

tmp.set(center);
tmp.sub(z);
tmp.sub(x);
p2.set(tmp);
tmp.add(x);
tmp.add(x);
p3.set(tmp);

dest[cPos++].set(p0);
dest[cPos++].set(p1);
dest[cPos++].set(p3);

dest[cPos++].set(p2);
dest[cPos++].set(p0);
dest[cPos].set(p3);

pos++;
if (pos >= maxPos) {
pos = 0;
}
lastP2.set(p2);
lastP3.set(p3);
}
}
}


Harald

This looks pretty promising. I will get into this as soon asI have some moe time (probably around easter) I am working on a full time job while programming this at the moment. This is also the reason for the slow response time.

I try to find a way to get a (nearly) unlimited area covered by adding some additional tricks

My code creates polygons on the fly and adds them to the world in runtime. Like I said before I know that this is a bad implementation and was just for testing purposes.
It looks like this: (its kotlin as well)

fun createTrackLines(world: World){
        trackLineLeft = EndlessTrackLine(world, RGBColor(0,255,0,100), 5f)
        trackLineRight = EndlessTrackLine(world, RGBColor(0,255,255,100), 5f)
    }

    fun trackMovementWithLines(movingObject: Object3D){
        if (equipment != null){
            val width = equipment.part1Width * MyRenderer.UNIT_TO_MM_RATIO
            val offset = width / 2
            trackLineLeft.trackMovement(movingObject, mainObject, -offset)
            trackLineRight.trackMovement(movingObject,mainObject, offset)
        }
    }

    //this drops the framerate
    fun createTrackObject(world: World){
        val trackObject = Object3D(2)
        if (equipment != null){
            //lines
            val left = trackLineLeft.line
            val right = trackLineRight.line
            if (left.size > 2 && right.size > 2){
                //vectors
                val backLeft = left[left.size -2]
                val backRight = right[right.size -2]
                val frontLeft = left[left.size -1]
                val frontRight = right[right.size -1]
                //triangles
                trackObject.addTriangle(backLeft, backRight, frontRight)
                trackObject.addTriangle(frontRight, frontLeft, backLeft)
                trackObject.setTexture("colSecondary")
                //trackObject.transparency = 10 //todo: add this once everything is tested

                trackObject.build()
                world.addObject(trackObject)
            }

        }
    }


the createTrackObject function is executed every 10 frames.

Harald

I just figured out the right way to do it (and the framerate is mostly stable with just a few drops to 45 frames per second on the android device)
EgonOlsens code helped me a lot (a huge thanks). To help people who will stumble across this I want to share the code I used to get an Endless Version of the code (and not continuing like EgonsOlsens version).
Beware when using this: With everything that looks like it is endless in programming there is a point where things break. So far I do not have a solution for this.

This code is taking two scalable lists of vectors (called leftLine and rightLine) and create the data to follow by these two lists. The rest is kind of copy pasted or slightly changed.

import com.threed.jpct.*

class EndlessTrackArea(private val leftLine: EndlessTrackLine, private val rightLine: EndlessTrackLine) {
    private var trackObject = Object3D(TRACK_POLYGONS)
    private var trackCreator = TrackCreator(leftLine, rightLine)
    private var world: World? = null
    private var color = RGBColor(0,255,255,100)

    fun moveTrackArea(){
        if (trackCreator.pos >= trackCreator.destinationMesh.size -1){
            trackCreator = TrackCreator(leftLine, rightLine)
            setTrackArea()
        }
        trackObject.mesh.applyVertexController()
        trackObject.touch()
    }
    fun setAreaVariables(world: World, color: RGBColor = RGBColor(0,255,255,100)){
        this.world = world
        this.color = color
        setTrackArea()
    }

    private fun setTrackArea(){
        Logger.log("Creating Track Object")
        val trackObject = Object3D(TRACK_POLYGONS)

        for (i in 0 until TRACK_POLYGONS / 2){
            trackObject.addTriangle(
                SimpleVector(
                    (-1100000 + i).toDouble(), (-1200000 + i).toDouble(),
                    (-1300000 + i).toDouble()
                ), SimpleVector(
                    (-1400000 + i).toDouble(), (-1500000 + i).toDouble(),
                    (-1600000 + i).toDouble()
                ), SimpleVector(
                    (-1700000 + i).toDouble(), (-1800000 + i).toDouble(), (-1900000 + i).toDouble()
                )
            )
            trackObject.addTriangle(
                SimpleVector(
                    (2000000 + i).toDouble(),
                    (2100000 + i).toDouble(), (2200000 + i).toDouble()
                ), SimpleVector(
                    (2300000 + i).toDouble(), (2400000 + i).toDouble(),
                    (2500000 + i).toDouble()
                ), SimpleVector(
                    (2600000 + i).toDouble(),
                    (2700000 + i).toDouble(), (2800000 + i).toDouble()
                )
            )
        }
        trackObject.calcNormals()
        trackObject.mesh.setVertexController(trackCreator, false)
        trackObject.forceGeometryIndices(false)
        trackObject.compile(true)
        trackObject.build()
        trackObject.additionalColor = color
        trackObject.culling = Object3D.CULLING_DISABLED
        trackObject.transparency = 10
        world!!.addObject(trackObject)

        this.trackObject = trackObject
    }
    companion object{
        const val TRACK_POLYGONS = 100
    }
    private class TrackCreator(private val leftLine: EndlessTrackLine,private val rightLine: EndlessTrackLine): GenericVertexController(){

       var pos = 0

        override fun apply() {
            //this is some crazy testing
            //lines
            val left = leftLine.line
            val right = rightLine.line
            if (left.size > 2 && right.size > 2){
                //points of the lines
                val backLeft = left[left.size -2]
                val backRight = right[right.size -2]
                val frontLeft = left[left.size -1]
                val frontRight = right[right.size -1]

                if (pos + 4 >= destinationMesh.size -1){
                    pos = 0
                }
                destinationMesh[pos++].set(backLeft) //0
                destinationMesh[pos++].set(backRight) //1
                destinationMesh[pos++].set(frontRight) //3
                destinationMesh[pos++].set(frontLeft) //2
                destinationMesh[pos++].set(backLeft) //0
                destinationMesh[pos++].set(frontRight) //3

            }
        }

        companion object {
            private const val serialVersionUID = 1L
        }
    }
}