Files
my_site/vue/public/race/track.js
yoyuzh d669738967 init
2026-02-27 14:29:05 +08:00

426 lines
15 KiB
JavaScript

'use strict';
function trackPreUpdate()
{
// calcuate track x offsets and projections (iterate in reverse)
const cameraTrackInfo = new TrackSegmentInfo(cameraOffset);
const cameraTrackSegment = cameraTrackInfo.segmentIndex;
const cameraTrackSegmentPercent = cameraTrackInfo.percent;
const turnScale = 2;
for(let x=0, v=0, i=0; i<drawDistance; ++i)
{
const j = cameraTrackSegment+i;
if (!track[j])
continue;
// create track world position
const s = i < 1 ? 1-cameraTrackSegmentPercent : 1;
track[j].pos = track[j].offset.copy();
track[j].pos.x = x += v += turnScale*s*track[j].pos.x;
track[j].pos.z -= cameraOffset;
}
}
function drawTrack()
{
glEnableFog = 0; // track looks better without fog
drawRoad(1); // first draw just flat ground with z write
glSetDepthTest(0,0); // disable z testing
drawRoad(); // draw ground and road
// set evertyhing back to normal
glEnableFog = 1;
glSetDepthTest();
}
function drawRoad(zwrite)
{
// draw the road segments
const drawLineDistance = 500;
const cameraTrackInfo = new TrackSegmentInfo(cameraOffset);
const cameraTrackSegment = cameraTrackInfo.segmentIndex;
for(let i = drawDistance, segment1, segment2; i--; )
{
const segmentIndex = cameraTrackSegment+i;
segment1 = track[segmentIndex];
if (!segment1 || !segment2)
{
segment2 = segment1;
continue;
}
if (i % (lerp(i/drawDistance,1,8)|0)) // fade in road resolution
continue;
const p1 = segment1.pos;
const p2 = segment2.pos;
const normals = [segment1.normal, segment1.normal, segment2.normal, segment2.normal];
function pushRoadVerts(width, color, offset=0, width2=width, offset2=offset, oy=0)
{
const point1a = vec3(p1.x+width+offset, p1.y+oy, p1.z);
const point1b = vec3(p1.x-width+offset, p1.y+oy, p1.z);
const point2a = vec3(p2.x+width2+offset2, p2.y+oy, p2.z);
const point2b = vec3(p2.x-width2+offset2, p2.y+oy, p2.z);
const poly = [point1a, point1b, point2a, point2b];
color.a && glPushVertsCapped(poly, normals, color);
}
{
// ground
const color = segment1.colorGround;
const width = 1e5; // fill the width of the screen
pushRoadVerts(width, color);
}
if (!zwrite)
{
const roadHeight = 10;
// road
const color = segment1.colorRoad;
const width = segment1.width;
const width2 = segment2.width;
pushRoadVerts(width, color, undefined, width2,undefined,roadHeight);
if (i < drawLineDistance)
{
// lines on road
const w = segment1.width;
const lineBias = .2
const laneCount = 2*w/laneWidth - lineBias;
for(let j=1; j<laneCount; ++j)
{
const color = segment1.colorLine;
const lineWidth = 30;
const offset = j*laneWidth-segment1.width;
const offset2 = j*laneWidth-segment2.width;
pushRoadVerts(lineWidth, color, offset, undefined, offset2,roadHeight);
}
}
}
segment2 = segment1;
}
glRender();
}
function drawTrackScenery()
{
// this is last pass from back to front so do do not write to depth
glSetDepthTest(1, 0);
glEnableLighting = 0;
const cameraTrackInfo = new TrackSegmentInfo(cameraOffset);
const cameraTrackSegment = cameraTrackInfo.segmentIndex;
for(let i=drawDistance; i--; )
{
const segmentIndex = cameraTrackSegment+i;
const trackSegment = track[segmentIndex];
if (!trackSegment)
continue;
// draw objets for this segment
random.setSeed(trackSeed+segmentIndex);
for(const trackObject of trackSegment.trackObjects)
trackObject.draw();
// random scenery
const levelInfo = getLevelInfo(trackSegment.level);
const levelFloat = trackSegment.offset.z/checkpointDistance;
const levelInfoNext = getLevelInfo(levelFloat+1);
const levelLerpPercent = percent(levelFloat%1, 1-levelLerpRange, 1);
const w = trackSegment.width;
if (enhancedMode && trackSegment.level == 3)
{
// snow
const x = random.floatSign(1e4);
const h = 1e4;
const y = h-(random.float(h)+time*2e3)%h;
pushSprite(vec3(x + 1e3*trackSegment.getWind(),y).addSelf(trackSegment.pos), vec3(50), WHITE, spriteList.dot.spriteTile);
}
if (!trackSegment.sideStreet) // no sprites on side streets
for(let k=3;k--;)
{
const spriteSide = (segmentIndex+k)%2 ? 1 : -1;
if (spriteSide == levelInfo.waterSide)
{
// water
const sprite = spriteList.water;
const s = sprite.size*sprite.getRandomSpriteScale();
const o2 = w+random.float(12e3,8e4);
const o = spriteSide * o2;
// get taller in distance to cover horizon
const h = .4;
const wave = time-segmentIndex/70;
const p = vec3(o+2e3*Math.sin(wave),0).addSelf(trackSegment.pos);
const waveWind = 9*Math.cos(wave); // fake wind to make wave seam more alive
pushTrackObject(p, vec3(spriteSide*s,s*h,s), WHITE, sprite, waveWind);
}
else
{
// lerp in next level scenery at end
const sceneryLevelInfo = random.bool(levelLerpPercent) ? levelInfoNext : levelInfo;
// scenery on far side like grass and flowers
const sceneryList = sceneryLevelInfo.scenery;
const sceneryListBias = sceneryLevelInfo.sceneryListBias;
if (sceneryLevelInfo.scenery)
{
const sprite = random.fromList(sceneryList,sceneryListBias);
const s = sprite.size*sprite.getRandomSpriteScale();
// push farther away if big collision
const xm = w+sprite.size+6*sprite.collideScale*s;
const o = spriteSide * random.float(xm,3e4);
const p = vec3(o,0).addSelf(trackSegment.pos);
const wind = trackSegment.getWind();
const color = sprite.getRandomSpriteColor();
const scale = vec3(sprite.canMirror && random.bool() ? -s : s,s,s);
pushTrackObject(p, scale, color, sprite, wind);
}
}
}
}
glRender();
if (!js13kBuild) // final thing rendered, so no need to reset
{
glSetDepthTest();
glEnableLighting = 1;
}
}
function pushTrackObject(pos, scale, color, sprite, trackWind)
{
if (optimizedCulling)
{
const cullScale = 200;
if (cullScale*scale.y < pos.z)
return; // cull out small sprites
if (abs(pos.x)-abs(scale.x) > pos.z*4+2e3)
return; // out of view
if (pos.z < 0)
return; // behind camera
}
const shadowScale = sprite.shadowScale;
const wind = sprite.windScale * trackWind;
const yShadowOffset = freeCamMode ? cameraPos.y/20 : 10; // fix shadows in free cam mode
const spriteYOffset = scale.y*(1+sprite.spriteYOffset) + (freeCamMode?cameraPos.y/20:0);
pos.y += yShadowOffset;
if (shadowScale)
pushShadow(pos, scale.y*shadowScale, scale.y*shadowScale/6);
// draw on top of shadow
pos.y += spriteYOffset - yShadowOffset;
pushSprite(pos, scale, color, sprite.spriteTile, wind);
}
///////////////////////////////////////////////////////////////////////////////
/*function draw3DTrackScenery()
{
const cameraTrackSegment = cameraTrackInfo.segmentIndex;
// 3d scenery
for(let i=drawDistance, segment1, segment2; i--; )
{
segment2 = segment1;
const segmentIndex = cameraTrackSegment+i;
segment1 = track[segmentIndex];
if (!segment1 || !segment2)
continue;
if (segmentIndex%7)
continue
const d = segment1.pos.subtract(segment2.pos);
const heading = PI-Math.atan2(d.x, d.z);
// random scenery
random.setSeed(trackSeed+segmentIndex);
const w = segment1.width;
const o =(segmentIndex%2?1:-1)*(random.float(5e4,1e5))
const r = vec3(0,-heading,0);
const p = vec3(-o,0).addSelf(segment1.pos);
const s = vec3(random.float(500,1e3),random.float(1e3,4e3),random.float(500,1e3));
//const s = vec3(500,random.float(2e3,2e4),500);
const m4 = buildMatrix(p,r,s);
const c = hsl(0,0,random.float(.2,1));
cubeMesh.render(m4, c);
}
}
*/
///////////////////////////////////////////////////////////////////////////////
// an instance of a sprite
class TrackObject
{
constructor(trackSegment, sprite, offset, color=WHITE, sizeScale=1)
{
this.trackSegment = trackSegment;
this.sprite = sprite;
this.offset = offset;
this.color = color;
const scale = sprite.size * sizeScale;
this.scale = vec3(scale);
const trackWidth = trackSegment.width;
const trackside = offset.x < trackWidth*2 && offset.x > -trackWidth*2;
if (trackside && sprite.trackFace)
this.scale.x *= sign(offset.x);
else if (sprite.canMirror && random.bool())
this.scale.x *= -1;
this.collideSize = sprite.collideScale*abs(scale);
}
draw()
{
const trackSegment = this.trackSegment;
const pos = trackSegment.pos.add(this.offset);
const wind = trackSegment.getWind();
pushTrackObject(pos, this.scale, this.color, this.sprite, wind);
}
}
class TrackSegment
{
constructor(segmentIndex,offset,width)
{
if (segmentIndex >= levelGoal*checkpointTrackSegments)
width = 0; // no track after end
this.offset = offset;
this.width = width;
this.pitch = 0;
this.normal = vec3();
this.trackObjects = [];
const levelFloat = segmentIndex/checkpointTrackSegments;
const level = this.level = testLevelInfo ? testLevelInfo.level : levelFloat|0;
const levelInfo = getLevelInfo(level);
const levelInfoNext = getLevelInfo(levelFloat+1);
const levelLerpPercent = percent(levelFloat%1, 1-levelLerpRange, 1);
const checkpointLine = segmentIndex > 25 && segmentIndex < 30
|| segmentIndex%checkpointTrackSegments > checkpointTrackSegments-10;
const recordPoint = bestDistance/trackSegmentLength;
const recordPointLine = segmentIndex>>3 == recordPoint>>3;
this.sideStreet = levelInfo.sideStreets && ((segmentIndex%checkpointTrackSegments)%495<36);
{
// setup colors
const groundColor = levelInfo.groundColor.lerp(levelInfoNext.groundColor,levelLerpPercent);
const lineColor = levelInfo.lineColor.lerp(levelInfoNext.lineColor,levelLerpPercent);
const roadColor = levelInfo.roadColor.lerp(levelInfoNext.roadColor,levelLerpPercent);
const largeSegmentIndex = segmentIndex/9|0;
const stripe = largeSegmentIndex% 2 ? .1: 0;
this.colorGround = groundColor.brighten(Math.cos(segmentIndex*2/PI)/20);
this.colorRoad = roadColor.brighten(stripe&&.05);
if (recordPointLine)
this.colorRoad = hsl(0,.8,.5);
else if (checkpointLine)
this.colorRoad = WHITE; // starting line
this.colorLine = lineColor;
if (stripe)
this.colorLine.a = 0;
if (this.sideStreet)
this.colorLine = this.colorGround = this.colorRoad;
}
// spawn track objects
if (debug && testGameSprite)
{
// test sprite
this.addSprite(testGameSprite,random.floatSign(width/2,1e4));
}
else if (debug && testTrackBillboards)
{
// test billboard
const billboardSprite = random.fromList(spriteList.billboards);
this.addSprite(billboardSprite,random.floatSign(width/2,1e4));
}
else if (segmentIndex == levelGoal*checkpointTrackSegments)
{
// goal!
this.addSprite(spriteList.sign_goal);
}
else if (segmentIndex%checkpointTrackSegments == 0)
{
// checkpoint
if (segmentIndex < levelGoal*checkpointTrackSegments)
{
this.addSprite(spriteList.sign_checkpoint1,-width+500);
this.addSprite(spriteList.sign_checkpoint2, width-500);
}
}
if (segmentIndex == 30)
{
// starting area
this.addSprite(spriteList.sign_start);
// left
const ol = -(width+100);
this.addSprite(spriteList.sign_opGames,ol,1450);
this.addSprite(spriteList.sign_zzfx,ol,850);
this.addSprite(spriteList.sign_avalanche,ol);
// right
const or = width+100;
this.addSprite(spriteList.sign_frankForce,or,1500);
this.addSprite(spriteList.sign_github,or,350);
this.addSprite(spriteList.sign_js13k,or);
if (js13kBuild)
random.seed = 1055752394; // hack, reset seed for js13k
}
}
getWind()
{
const offset = this.offset;
const noiseScale = .001;
return Math.sin(time+(offset.x+offset.z)*noiseScale)/2;
}
addSprite(sprite,x=0,y=0,extraScale=1)
{
// add a sprite to the track as a new track object
const offset = vec3(x,y);
const sizeScale = extraScale*sprite.getRandomSpriteScale();
const color = sprite.getRandomSpriteColor();
const trackObject = new TrackObject(this, sprite, offset, color, sizeScale);
this.trackObjects.push(trackObject);
}
}
// get lerped info about a track segment
class TrackSegmentInfo
{
constructor(z)
{
const segment = this.segmentIndex = z/trackSegmentLength|0;
const percent = this.percent = z/trackSegmentLength%1;
if (track[segment] && track[segment+1])
{
if (track[segment].pos && track[segment+1].pos)
this.pos = track[segment].pos.lerp(track[segment+1].pos, percent);
else
this.pos = vec3(0,0,z);
this.pitch = lerp(percent, track[segment].pitch, track[segment+1].pitch);
this.offset = track[segment].offset.lerp(track[segment+1].offset, percent);
this.width = lerp(percent, track[segment].width,track[segment+1].width);
}
else
this.offset = this.pos = vec3(this.pitch = this.width = 0,0,z);
}
}