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 :
- cameraDepth was somewhat incorrect and thus produced a slightly skewed display
- SDL2 polygon routine can only really deal with integers which, again, produces a slightly incorrect display
- 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);
}
}