3

I've converted CodeinComplete's Pseudo 3D road routine to use SDL2 - unfortunately for some reason the curves are incredibly jarring (both entering and leaving it).

I've also converted it to GLBasic, and get the same effect there too (although much less pronounced). I cant think of a reason for it at the moment, as it is more or less directly converted, and the X values are accumulated over the display list.

If anyone can work out why it's happening, I'll be very happy!

Video of the SDL2 problem is here : https://www.youtube.com/watch?v=ij6POR9MkfA Video of the GLBasic problem is here : https://www.youtube.com/watch?v=uzF9Nsl7Ayg

Update : There are three causes unfortunately :

  1. cameraDepth was somewhat incorrect and thus produced a slightly skewed display
  2. SDL2 polygon routine can only really deal with integers which, again, produces a slightly incorrect display
  3. The main problem is that the road is split into 9 sections which seems to affect the calculations as the projection is based on the centre of a segment

The GLBasic code is :

TYPE __camera
    x
    y
    z
ENDTYPE

TYPE __pScreen
    scale
    x
    y
    w
ENDTYPE

TYPE __world
    x
    y
    z
ENDTYPE

TYPE __segmentPart
    world AS __world
    camera AS __camera
    screen AS __pScreen
ENDTYPE

TYPE __tile
    colour%
    p1 AS __segmentPart
    p2 AS __segmentPart
ENDTYPE

TYPE __segment
    index%
    curve

    segment[] AS __tile

    colour%
ENDTYPE


TYPE Utility
    PI = 3.14

    FUNCTION accelerate:v,accel,dt
        RETURN v + (accel * dt)
    ENDFUNCTION

    FUNCTION limit:value, minV,maxV
        RETURN MAX(minV,MIN(value,maxV))
    ENDFUNCTION

    FUNCTION easeIn:a,b,percent
        RETURN a + (b-a)*POW(percent,2)
    ENDFUNCTION

    FUNCTION easeOut:a,b,percent
        RETURN a + (b-a)*(1.0-POW(1-percent,2))
    ENDFUNCTION

    FUNCTION easeInOut:a,b,percent
    LOCAL cosV,v

        cosV=(percent*self.PI)*(180.0/self.PI)
        v=a + (b-a)*((-COS(cosV)/2.0) + 0.5)
        RETURN v
    ENDFUNCTION

    FUNCTION percentageRemaining:n,total
        RETURN FMOD(n,total)/total
    ENDFUNCTION

    FUNCTION interpolate:a,b,percent
        RETURN a+(b-a)*percent
    ENDFUNCTION

    FUNCTION project%:p AS __segmentPart,cameraX, cameraY, cameraZ, cameraDepth, swidth, sheight, roadWidth%, tileWidth%, loop%
        p.camera.x=p.world.x - cameraX
        p.camera.y=p.world.y - cameraY
        p.camera.z=p.world.z - cameraZ

        p.screen.scale=cameraDepth/p.camera.z

        p.screen.x=((swidth/2.0)+(p.screen.scale*p.camera.x*swidth/2.0))
        p.screen.y=((sheight/2.0)-(p.screen.scale*p.camera.y*sheight/2.0))
        p.screen.w=(p.screen.scale*roadWidth%*swidth/2.0)
    ENDFUNCTION

ENDTYPE

TYPE Road
    numTiles%
    tileWidth

    width%;height%
    stp
    roadWidth%
    segmentLength%
    rumbleLength%
    lanes%  =   3
    fieldOfView
    cameraHeight
    drawDistance%
    maxSpeed%
    trackLength%
    cameraDepth
    fps
    PI

    segments[] AS __segment
    utils AS Utility

    FUNCTION Initialise%:
        DIM self.segments[0]

        GETSCREENSIZE self.width%,self.height%

        self.PI=3.14
        self.fps = 60;
        self.stp = 1.0 / self.fps;
        self.roadWidth = 1800;                    // actually half the roads width, easier math if the road spans from -roadWidth to +roadWidth
        self.segmentLength = 10;                 // length of a single segment
        self.rumbleLength = 3;
        self.lanes = 3;                          // number of lanes
        self.fieldOfView = 100;                   // angle (degrees) for field of view
        self.cameraHeight = 1000;
        self.drawDistance = 300;
        self.maxSpeed = INTEGER(self.segmentLength / self.stp);
        self.trackLength = 0;
        self.cameraDepth = 1.0 / TAN(self.fieldOfView / 2.0)*self.PI / 180.0;
        self.numTiles = 9;
        self.tileWidth%=self.roadWidth% / self.numTiles%
    ENDFUNCTION

    FUNCTION findSegment AS __segment:z
    LOCAL index%

        index%=INTEGER(z/FMOD(self.segmentLength%,BOUNDS(self.segments[],0)))
        RETURN self.segments[index%]
    ENDFUNCTION

    FUNCTION drawSegment%:position
    LOCAL segment AS __segment
    LOCAL baseSegment AS __segment
    LOCAL basePercentage
    LOCAL x,dx,playerX=0,maxy
    LOCAL segmentIndex%

        baseSegment=self.findSegment(position)
        basePercentage=self.utils.percentageRemaining(position,self.segmentLength)
        x=0.0
        dx=-(baseSegment.curve*basePercentage)
        maxy=self.height%

        FOR n%=0 TO self.drawDistance-1
            segmentIndex%=MOD(baseSegment.index%+n%,BOUNDS(self.segments[],0))
            PRINT "X:"+FORMAT$(4,4,x)+"/"+FORMAT$(4,4,dx),0,0
            PRINT "BP:"+FORMAT$(4,4,basePercentage),0,16
        //  DEBUG "X : "+FORMAT$(4,4,x)+"\n"
        //  DEBUG "DX : "+FORMAT$(4,4,dx)+"\n"
            FOR loop%=0 TO 0
                self.utils.project(self.segments[segmentIndex%].segment[loop%].p1,(playerX*self.tileWidth%)-x,self.cameraHeight,position,self.cameraDepth,self.width,self.height,self.roadWidth,self.tileWidth,loop%)
                self.utils.project(self.segments[segmentIndex%].segment[loop%].p2,(playerX*self.tileWidth%)-x-dx,self.cameraHeight,position,self.cameraDepth,self.width,self.height,self.roadWidth,self.tileWidth,loop%)
            NEXT

            INC x,dx
            INC dx,self.segments[segmentIndex%].curve

            IF self.segments[segmentIndex%].segment[0].p1.camera.z<=self.cameraDepth% OR _
                self.segments[segmentIndex%].segment[0].p2.screen.y>=self.segments[segmentIndex%].segment[0].p1.screen.y OR _
                self.segments[segmentIndex%].segment[0].p2.screen.y>=maxy
                CONTINUE
            ENDIF

            FOR loop%=0 TO 0
                self.renderSegment(self.segments[segmentIndex%].segment[loop%].p1.screen.x - self.segments[segmentIndex%].segment[loop%].p1.screen.w,   _
                                    self.segments[segmentIndex%].segment[loop%].p1.screen.y,    _
                                    self.segments[segmentIndex%].segment[loop%].p1.screen.x + self.segments[segmentIndex%].segment[loop%].p1.screen.w,  _
                                    self.segments[segmentIndex%].segment[loop%].p1.screen.y,    _
                                    self.segments[segmentIndex%].segment[loop%].p2.screen.x + self.segments[segmentIndex%].segment[loop%].p2.screen.w,  _
                                    self.segments[segmentIndex%].segment[loop%].p2.screen.y,    _
                                    self.segments[segmentIndex%].segment[loop%].p2.screen.x - self.segments[segmentIndex%].segment[loop%].p2.screen.w,  _
                                    self.segments[segmentIndex%].segment[loop%].p2.screen.y, _
                                    self.segments[segmentIndex%].segment[loop%].colour%)
            NEXT
        NEXT
    ENDFUNCTION

    FUNCTION renderSegment%:x1,y1,x2,y2,x3,y3,x4,y4,colour%
        STARTPOLY -1
            POLYVECTOR  x1,y1,x2,y2,colour%
            POLYVECTOR  x2,y2,x3,y3,colour%
            POLYVECTOR  x3,y3,x4,y4,colour%
            POLYVECTOR  x4,y4,x1,y1,colour%
        ENDPOLY
    ENDFUNCTION

    FUNCTION lastSlope:segments[] AS __segment
    LOCAL size%

        size=BOUNDS(segments[],0)
        IF size%=0
            RETURN 0.0
        ELSE
            RETURN segments[size%-1].segment[0].p2.world.y
        ENDIF
    ENDFUNCTION

    FUNCTION addSegment%:segments[] AS __segment,curve,y
    LOCAL size%
    LOCAL segment AS __segment
    LOCAL halfTile%
    LOCAL slope

        size%=BOUNDS(self.segments[],0)

        segment.index%=size%
        segment.curve=curve

        halfTile%=9/2

        DIM segment.segment[1]
        FOR n%=0 TO BOUNDS(segment.segment[],0)-1
            segment.segment[n].p1.world.x = 0.0 // (0.0-halfTile%+n)*self.tileWidth
            segment.segment[n].p1.world.y = self.lastSlope(segments[])
            segment.segment[n].p1.world.z = size*self.segmentLength

            segment.segment[n].p2.world.x = 0.0 // (0.0 - halfTile + n)*self.tileWidth
            segment.segment[n].p2.world.y = slope
            segment.segment[n].p2.world.z = (size% + 1)*self.segmentLength

            segment.segment[n].colour=IIF(bAND(n%,1), RGB(255,0,0),RGB(0,255,0))
        NEXT

        DIMPUSH self.segments[],segment
    ENDFUNCTION

    FUNCTION addRoad%:segments[] AS __segment,enter, hold, leave, curve, y
    LOCAL startY,endY,total
    LOCAL n%

        startY=self.lastSlope(segments[])
        endY=startY+(INTEGER(y)*self.segmentLength%)
        total=enter+hold+leave

//      DEBUG "Start Y : "+startY+" endY : "+endY+"\n"
//      KEYWAIT
        FOR n%=0 TO enter-1
            self.addSegment(segments[],self.utils.easeIn(0,curve,n%/enter),self.utils.easeInOut(startY,endY,n%/total))
        NEXT
//      DEBUG "Holding ---\n"
        FOR n%=0 TO hold-1
            self.addSegment(segments[],curve,self.utils.easeInOut(startY,endY,(enter+n%)/total))
        NEXT
//      DEBUG "---\n"

        FOR n%=0 TO leave-1
            self.addSegment(segments[],self.utils.easeInOut(curve,0,n%/leave),self.utils.easeInOut(startY,endY,(enter+hold+n)/total))
        NEXT
    ENDFUNCTION

    FUNCTION addStraight%:segments[] AS __segment,num=25
        self.addRoad(segments[],num,num,num,0,0)
    ENDFUNCTION

    FUNCTION addHill%:segments[] AS __segment,num,height
        self.addRoad(segments[],num,num,num,0,height)
    ENDFUNCTION

    FUNCTION addCurve%:segments[] AS __segment,num, curve,height
        self.addRoad(segments[],num,num,num,curve,height)
    ENDFUNCTION

    FUNCTION addLowRollingHills%:segments[] AS __segment,num,height
        self.addRoad(segments[],num, num, num, 0, height/2)
        self.addRoad(segments[],num, num, num, 0, -height)
        self.addRoad(segments[],num, num, num, 0, height)
        self.addRoad(segments[],num, num, num, 0, 0)
        self.addRoad(segments[],num, num, num, 0, height/2)
        self.addRoad(segments[],num, num, num, 0, 0)
    ENDFUNCTION

    FUNCTION addSCurves%:segments[] AS __segment
//      self.addRoad(ROAD_LENGTH_MEDIUM, ROAD_LENGTH_MEDIUM, ROAD_LENGTH_MEDIUM, -ROAD_CURVE_EASY, ROAD_HILL_NONE)
//      self.addRoad(ROAD_LENGTH_MEDIUM, ROAD_LENGTH_MEDIUM, ROAD_LENGTH_MEDIUM, ROAD_CURVE_MEDIUM, ROAD_HILL_MEDIUM)
//      self.addRoad(ROAD_LENGTH_MEDIUM, ROAD_LENGTH_MEDIUM, ROAD_LENGTH_MEDIUM, ROAD_CURVE_EASY, -ROAD_HILL_LOW)
//      self.addRoad(ROAD_LENGTH_MEDIUM, ROAD_LENGTH_MEDIUM, ROAD_LENGTH_MEDIUM, -ROAD_CURVE_EASY, ROAD_HILL_MEDIUM)
//      self.addRoad(ROAD_LENGTH_MEDIUM, ROAD_LENGTH_MEDIUM, ROAD_LENGTH_MEDIUM, -ROAD_CURVE_MEDIUM, -ROAD_HILL_MEDIUM)
    ENDFUNCTION

    FUNCTION addBumps%:
//      self.addRoad(10, 10, 10, 0,  5);
//      self.addRoad(10, 10, 10, 0, -2);
//      self.addRoad(10, 10, 10, 0, -5);
//      self.addRoad(10, 10, 10, 0,  8);
//      self.addRoad(10, 10, 10, 0,  5);
//      self.addRoad(10, 10, 10, 0, -7);
//      self.addRoad(10, 10, 10, 0,  5);
//      self.addRoad(10, 10, 10, 0, -2);
    ENDFUNCTION

    FUNCTION addDownhillToEnd%:num=200
  //    self.addRoad(num, num, num, -ROAD_CURVE_EASY, -lastY()/self.segmentLength);
    ENDFUNCTION

    FUNCTION createTestRoad%:
    LOCAL segment AS __segment

        DIM self.segments[0]

        self.addCurve(self.segments[],12,200,150)
    ENDFUNCTION

ENDTYPE

The C SDL2 code (relevent parts that is) :

(DGInt is a float and DGNat is an integer)

void Utils::project(struct __segmentPart *p, DGInt cameraX, DGInt cameraY, DGInt cameraZ, DGInt cameraDepth, DGNat width, DGNat height, DGNat roadWidth,DGNat tileWidth,DGNat tileIndex)
{
    p->camera.x = p->world.x - cameraX;
    p->camera.y = p->world.y - cameraY;
    p->camera.z = p->world.z - cameraZ;
    p->screen.scale = cameraDepth / p->camera.z;
    p->screen.x = (width / 2.0) + ((p->screen.scale*p->camera.x*width) / 2.0);
    p->screen.y = (height / 2.0) - ((p->screen.scale*p->camera.y*height) / 2.0);
    p->screen.w = (p->screen.scale*tileWidth*width / 2.0);
}

struct __segment *Road::findSegment(DGInt z)
{
    return (struct __segment *) &segments(FLOOR(z / segmentLength));
}

void Road::render(DGInt position)
{
    struct __segment *baseSegment;
    struct __segment *segment;
    DGInt maxy = height;
    DGNat n;
    DGInt basePercentage;
    DGInt x = 0.0;
    DGInt dx;
    DGInt playerX = 0;

    baseSegment = findSegment(position);
    basePercentage = utils.percentRemaining(position, segmentLength);
    dx = - (baseSegment->curve*basePercentage);

    ALPHAMODE(-1);
    PRINT("Base Per :", 0, 0);
    PRINT(FORMAT_Str(4, 4, basePercentage), 0, 32);
    for (n = 0; n < drawDistance; n++)
    {
        //DGNat sIndex = (baseSegment->index + n) % BOUNDS(segments, 0);

        segment = (struct __segment *) &segments((baseSegment->index + n) % BOUNDS(segments, 0));
        for (DGNat loop = 0; loop < 9; loop++)
        {
            utils.project(&segment->segment[loop].p1, (playerX * roadWidth) - x , cameraHeight, position, cameraDepth, width, height, roadWidth,tileWidth,loop);
            utils.project(&segment->segment[loop].p2, (playerX * roadWidth) - x - dx , cameraHeight, position, cameraDepth, width, height, roadWidth,tileWidth,loop);
        }

        x += dx;
        dx += segment->curve;

        if ((segment->segment[0].p1.camera.z <= cameraDepth) || // behind us
            (segment->segment[0].p2.screen.y>= segment->segment[0].p1.screen.y) ||
            (segment->segment[0].p2.screen.y >= maxy))          // clip by (already rendered) segment
            continue;

        //Render.polygon(ctx, x1 - w1, y1, x1 + w1, y1, x2 + w2, y2, x2 - w2, y2, color.road);
        renderSegment(segment);
        //w1 = segment->p1.screen.x + segment->p1.screen.w;

        maxy=segment->segment[0].p1.screen.y;
    }
}

void Road::renderSegment(struct __segment *segment)
{
    Sint16 x[4], y[4];

    DRAWRECT(0.0, segment->segment[0].p2.screen.y, segment->segment[0].p2.screen.x, segment->segment[0].p1.screen.y - segment->segment[0].p2.screen.y, segment->border[0]);
    for (DGNat loop = 0; loop < 9; loop++)
    {
        x[0] = (Sint16) (segment->segment[loop].p1.screen.x - segment->segment[loop].p1.screen.w);  y[0] = (Sint16)(segment->segment[loop].p1.screen.y);
        x[1] = (Sint16)(segment->segment[loop].p1.screen.x + segment->segment[loop].p1.screen.w);   y[1] = (Sint16)(segment->segment[loop].p1.screen.y);
        x[2] = (Sint16)(segment->segment[loop].p2.screen.x + segment->segment[loop].p2.screen.w);   y[2] = (Sint16)(segment->segment[loop].p2.screen.y);
        x[3] = (Sint16)(segment->segment[loop].p2.screen.x - segment->segment[loop].p2.screen.w);   y[3] = (Sint16)(segment->segment[loop].p2.screen.y);

        filledPolygonRGBA(GETRENDERER(), (const Sint16 *) &x, (const Sint16 *) &y, 4, RGBR(segment->segment[loop].colour),RGBG(segment->segment[loop].colour),RGBB(segment->segment[loop].colour),255);
        //(SDL_Renderer * renderer, const Sint16 * vx,
        //  const Sint16 * vy, int n, Uint8 r, Uint8 g, Uint8 b, Uint8 a);


        //DRAWLINE(x[0], y[0], x[1], y[1], segment->segment[loop].colour);
        //DRAWLINE(x[1], y[1], x[2], y[2], segment->segment[loop].colour);
        //DRAWLINE(x[2], y[2], x[3], y[3], segment->segment[loop].colour);
        //DRAWLINE(x[3], y[3], x[0], y[0], segment->segment[loop].colour);
    }
}
MrTAToad
  • 51
  • 4
  • 2
    To get answered faster, you should post image that shows the fault. – Katu Sep 26 '15 at 16:42
  • 1
    A picture in itself would be pointless. A video however, would be more useful, This one is from the GLBasic version. I hope to have the SDL2 version uploaded later. https://www.youtube.com/watch?v=uzF9Nsl7Ayg – MrTAToad Sep 26 '15 at 17:21
  • 1
    Nice, now add that video link to your question, so users see it right away. – Katu Sep 26 '15 at 18:31

2 Answers2

1

The problem was caused by several things : The camera depth calculation was not correct (due to conversion to radians, which wasn't needed), causing a distorted (but looked okay) display, especially of curves. Finally, the actual step values were way too steep (as pixels were being used instead of points).

MrTAToad
  • 51
  • 4
0

What is happening is that as you traverse a skewed segment, the camera's center relative to the road changes. That is, by the time you get to the end of that segment, the road is no longer centered. When a new segment is entered the dx value does not line up with the old dx value and the jitter occurs.

So you basically LERP from the current dx to the dx in the next segment to smooth out the curves (as the % is a value from 0 to 1):

dx = -percentage_of_segment_traversed * ddx

Jake mentions this in his tutorial here: http://codeincomplete.com/posts/2012/6/24/javascript_racer_v2_curves/ in the "rendering curves" section:

Finally, in order to avoid jarring transitions when crossing segment boundaries we must ensure dx is initialized with an interpolated value for the current base segments curve.

And the relevant piece of sample code in his Javascript Racer:

var baseSegment = findSegment(position);
var basePercent = Util.percentRemaining(position, segmentLength);
var dx = - (baseSegment.curve * basePercent);
Felsir
  • 4,067
  • 2
  • 16
  • 32
  • All that is in the code... The only thing I can think is happening is either the wrong base segment is being used or the wrong percentage is being calculated. – MrTAToad Sep 27 '15 at 18:26
  • Sorry, I missed that. Because the effect in the video is exactly what this code intends to fix. Maybe do a debug of that percentage? – Felsir Sep 27 '15 at 18:29
  • The percentage does return a value between 0.0 and 1.0 – MrTAToad Sep 27 '15 at 19:44
  • And the segments? Is the findsegment returning the segment you expect? It may return the segment before the last one you draw? (Reduce the draw distance to, let's say 3 so you can more clearly what segments are used?). – Felsir Sep 27 '15 at 19:50
  • It is something I need to check - at the moment, I do know cameraDepth is incorrect... – MrTAToad Sep 27 '15 at 21:36
  • At the moment, the cameraDepth value seems to have been the cause of the problem. I wont know until I can view it properly, but it looks like that was skewing the display. – MrTAToad Sep 28 '15 at 08:34
  • As I have added in the initial post, the main cause is the the fact that the item is split into 9 sections, which seems to affect the projection calculations. I dont know how to get it "fixed" – MrTAToad Sep 28 '15 at 19:34
  • Ah, so you have 9 sections "side-by-side" that you calculate per piece? I've split my road in several sections as well: https://www.youtube.com/watch?v=O0OyHSuDoJ4 – Felsir Sep 28 '15 at 19:36
  • Thats right - I had hoped that the projection calculations would still be the same. Did you need to change the projection calculations ? – MrTAToad Sep 28 '15 at 19:39
  • What I did was this: You calculate 1 projection for each section. you have two scale values (near and far). I used these values to calculate the 'sections'. Note how Jake draws the rumble strips and the lane dividers. I even used the same method to 'split' the road. – Felsir Sep 28 '15 at 19:42
  • So its just a matter of splitting a complete segment into parts. Wouldn't that cause problems with curves ? – MrTAToad Sep 28 '15 at 19:59
  • Nope, no problem in curves. The lines in the javascript demo work in that exact way. – Felsir Sep 28 '15 at 20:03
  • True - I'll give that a go – MrTAToad Sep 29 '15 at 08:35
  • It is a lot better, although there is still a noticable jitter on curves. I suspect it is now due to SDL2 possibly using integers for line drawing. – MrTAToad Sep 30 '15 at 21:46
  • I have tried it with GLBasic too, and that jitters as well. I've no idea what causes it now... – MrTAToad Oct 02 '15 at 13:33
  • If you plot dx, is that a jitterless curve? That one should go pretty smooth. – Felsir Oct 02 '15 at 20:53
  • Solved it now. It turns out (aside from the camera problem), that the curves were far, far too sharp and thus not a smooth curve – MrTAToad Oct 04 '15 at 20:39
  • Glad you solved it! – Felsir Oct 05 '15 at 05:23
  • Me too! The only slight annoyance is that SDL2_gfx is extremely slow on my machine, so no filled tiles there! – MrTAToad Oct 06 '15 at 08:06