Archives
Trending
Support
Login
clear text
XML
Django
JavaScript
MATLAB
C
C++
C#
Python
SQL
Shell
Bash
Markdown
YAML
JSON
HTML
CSS
PHP
Java
Ruby
Go
Rust
Swift
Kotlin
Arduino
TypeScript
Perl
Autohotkey
Lua
SQF
R
Scala
Haskell
Groovy
Dart
Clojure
VB.NET
Objective-C
PowerShell
Bash
CoffeeScript
Verilog
-- This Source Code Form is subject to the terms of the bCDDL, v. 1.1. -- If a copy of the bCDDL was not distributed with this -- file, You can obtain one at http://beamng.com/bCDDL-1.1.txt local M = {} local E = setmetatable({}, {__newindex = function(t, key, val) log('E', 'ai.lua', 'Tried to insert new elements into token empty table') end}) M.mode = 'disabled' -- this is the main mode M.manualTargetName = nil M.debugMode = 'off' M.speedMode = nil M.routeSpeed = nil M.extAggression = 0.3 M.cutOffDrivability = 0 M.driveInLaneFlag = 'off' M.extAvoidCars = 'auto' -- [[ STORE FREQUENTLY USED FUNCTIONS IN UPVALUES ]] -- local buffer = require("string.buffer") local max = math.max local min = math.min local sin = math.sin local asin = math.asin local pi = math.pi local abs = math.abs local sqrt = math.sqrt local floor = math.floor local tableInsert = table.insert local tableRemove = table.remove local tableConcat = table.concat local strFormat = string.format -- [[ HEAVY DEBUG MODE ]] -- local visDebug = {trajecRec = {last = 0}, routeRec = {last = 0}, labelRenderDistance = 10, debugSpots = {}, candidatePaths = nil} --local newPositionsDebug = {} -- for debug purposes -- [[ Simulation time step]] -- local dt -- [[ ENVIRONMENT VARIABLES ]] -- local g = obj:getGravity() -- standard gravity is negative local gravityDir = vec3(0, 0, sign2(g)) g = max(1e-30, abs(g)) -- prevent divivion by 0 gravity -- [[ PERFORMANCE RELATED ]] -- local aggression = 1 -- [[ AI DATA: POSITION, CONTROL, STATE ]] -- local ego = { pos = obj:getFrontPosition(), dirVec = obj:getDirectionVector(), prevDirVec = obj:getDirectionVector(), upVec = obj:getDirectionVectorUp(), rightVec = vec3(), width = nil, length = nil, wheelBase = nil, currentSegment = {}, vel = vec3(obj:getSmoothRefVelocityXYZ()), speed = vec3(obj:getSmoothRefVelocityXYZ()):length(), staticFrictionCoef = 1, ghostR = nil, ghostL = nil, } ego.race = { dT_min = 0.1, dT_max = 0.3, Td = 0.1, Tp = 0.01, d = 4, --distance thredshold, unless my distance from vehicle ahead is greater than this, I'll want to go faster than him kp = 0.6 } local targetSpeedDifSmoother = newTemporalSmoothingNonLinear(1e300, 4, 0) local targetSpeedSmoother = newTemporalSmoothingNonLinear(10, 10, ego.speed) local egoDeviationSmoother = newTemporalSmoothing(1) local smoothTcs = newTemporalSmoothingNonLinear(0.1, 0.9) local throttleSmoother = newTemporalSmoothing(1e30, 0.2) local internalState = { changePlanTimer = 0, egoCannotMoveTime = 0, egoForceGoFrontTime = 0, road = 'onroad', crash = {time = 0, manoeuvre = 0, dir = nil, pos = nil}, chaseData = {playerState = nil, playerStoppedTimer = 0, playerRoad = nil, driveAhead = false, targetSpeed = nil} } local twt = { state = 0, dirState = {0, 0}, -- direction, steer posTable = {vec3(), vec3(), vec3(), vec3()}, dirTable = {vec3(), vec3(), vec3(), vec3()}, minRay = "", rayMins = {math.huge, math.huge, math.huge, math.huge, math.huge, math.huge, math.huge, math.huge}, --clockwise beginning from corner front left minRayCoefs = {0, 0, 0, 0, 0, 0, 0, 0}, -- clockwise as above blueNoiseCoefs = {0, 0, 0, 0, 0, 0, 0, 0}, biasedCoefs = {0, 0, 0, 0, 0, 0, 0, 0}, blueNoiseRR = 0, sampleCounter = 0, -- for starting scan, to give some time for precision targetSpeed = 0, OBBinRange = {}, RRT = {1,1,1,2,1,2,2,2,2,2,2,3,2,3,3,3,3,3,3,4,3,4,4,4,4,4,4,1,4,1,1,1}, speedSmoother = newTemporalSmoothingNonLinear(1000, 0.25), steerSmoother = newTemporalSmoothingNonLinear(2) } twt.reset = function() twt.state = 0 twt.dirState[1], twt.dirState[2] = 0, 0 twt.minRay = nil for i = 1, #twt.rayMins do twt.rayMins[i] = math.huge end for i = 1, #twt.blueNoiseCoefs do twt.blueNoiseCoefs[i] = 0 end twt.sampleCounter = 0 twt.targetSpeed = 0 twt.speedSmoother:set(0) twt.steerSmoother:set(0) end local forces = {} local velocities = {} local lastCommand = {steering = 0, throttle = 0, brake = 0, parkingbrake = 0} local opt = {driveInLaneFlag = false, racing = false, trajMethod = nil, aggressionMode = nil, avoidCars = 'on'} -- [[ TRAFFIC ]] -- local trafficStates = {} local trafficPathState = {} local traffic = { trafficTable = {}, trafficFilterQue = {array = {}, dict = {}}, trafficFilterFromQue = {}, frameQue = 0, rateQue = 0, -- 1 means adding a que vehicle every second filtered = true, -- TrafficFilter on or off (true/false) intersection = true, -- check for intersection between ego and other vehicle vdraw = false, -- draw debuf spheres to show traffic vehicles in trafficTable Rfl = 60, -- larger radius for traffic filter queue Rfl_fun = function(v) return clamp((v+10)*3, 60, 200) end, -- Rfs = 60, -- smaller radius for traffic table Rfs_fun = function(v) return clamp(v*3, 60, 100) end, distAhead = 40 -- minimum Ahead distance for searching traffic vehicle } -- [[ OPPONENT DATA ]] -- local player -- [[ SETTINGS, PARAMETERS, AUXILIARY DATA ]] -- local mapData -- map data including node connections (edges and edge data), node positions and node radii local signalsData -- traffic intersection and signals data local currentRoute local MIN_PLAN_COUNT = 3 local targetWPName local edgeDict local wpList local manualPath local validateInput = nop local speedProfile local loopPath, noOfLaps local parameters local restoreGearboxMode = false local targetObjectSelectionMode local scriptData local scriptai = nil local recover = {recoverOnCrash = false, recoverTimer = 0, _recoverOnCrash = false} ------------------------------ -- For Debugging (useful function to print a numbered list of upvalues of a specific function) local function inspect_upvalues(func) local i = 1 while true do local name, value = debug.getupvalue(func, i) if not name then break end print(string.format("Upvalue %d: %s = %s", i, name, tostring(value))) i = i + 1 end end local dataLogger = {logData = nop} local function logDataTocsv() if not dataLogger.csvFile then print('Started Logging Data') dataLogger.csvFile = require('csvlib').newCSV("time", "posX", "posY", "posZ", "speed", "targetSpeed", "ax", "ay", "az", "vx", "vy", "vz", "throttle", "brake", "steering", "staticFrictionCoef") dataLogger.time = 0 else dataLogger.time = dataLogger.time + dt end dataLogger.csvFile:add( dataLogger.time, ego.pos.x, ego.pos.y, ego.pos.z, ego.speed, ((currentRoute or E).plan or E).targetSpeed, -sensors.gy, sensors.gx, sensors.gz, ego.vel:dot(ego.dirVec), -(ego.vel:dot(ego.rightVec)), ego.vel:dot(ego.upVec), lastCommand.throttle * 100, lastCommand.brake * 100, lastCommand.steering * 100, ego.staticFrictionCoef) end local function writeCsvFile(name) if dataLogger.csvFile then print('Writing Data to CSV.') dataLogger.csvFile:write(name) dataLogger.csvFile = nil dataLogger.time = nil print('Done') else print('No data to write') end end local function startStopDataLog(name) if dataLogger.logData == nop then print('Initialized Data Log') dataLogger.logData = logDataTocsv dataLogger.name = name else print('Stopped Logging Data') dataLogger.logData = nop writeCsvFile(dataLogger.name or name) dataLogger.name = nil end end local function getObjectBoundingBox(id) local x = obj:getObjectDirectionVector(id) local z = obj:getObjectDirectionVectorUp(id) local y = z:cross(x) y:setScaled(obj:getObjectInitialWidth(id) * 0.5 / max(y:length(), 1e-30)) x:setScaled(obj:getObjectInitialLength(id) * 0.5) z:setScaled(obj:getObjectInitialHeight(id) * 0.5) return obj:getObjectCenterPosition(id), x, y, z end local function drawOBB(c, x, y, z, col) -- c: center point -- x: front half extent vec -- y: left half extent vec -- z: up half extent vec local debugDrawer = obj.debugDrawProxy col = col or color(255, 0, 0, 255) local p1 = c - x + y - z -- RLD local p2 = c - x - y - z -- RRD local p3 = c - x - y + z -- RRU local p4 = c - x + y + z -- RLU local p5 = c + x + y - z -- FLD local p6 = c + x - y - z -- FRD local p7 = c + x - y + z -- FRU local p8 = c + x + y + z -- FLU -- rear face debugDrawer:drawCylinder(p1, p2, 0.02, col) debugDrawer:drawCylinder(p2, p3, 0.02, col) debugDrawer:drawCylinder(p3, p4, 0.02, col) debugDrawer:drawCylinder(p4, p1, 0.02, col) -- front face debugDrawer:drawCylinder(p5, p6, 0.02, col) debugDrawer:drawCylinder(p6, p7, 0.02, col) debugDrawer:drawCylinder(p7, p8, 0.02, col) debugDrawer:drawCylinder(p8, p5, 0.02, col) -- left face debugDrawer:drawCylinder(p1, p5, 0.02, col) debugDrawer:drawCylinder(p4, p8, 0.02, col) -- right face debugDrawer:drawCylinder(p2, p6, 0.02, col) debugDrawer:drawCylinder(p3, p7, 0.02, col) end local function drawVehicleBoundingBox(vid, col) local c, vx, vy, vz = getObjectBoundingBox(vid) drawOBB(c, vx, vy, vz, col) end local function aistatus(status, category) guihooks.trigger("AIStatusChange", {status=status, category=category}) end local function getState() return M end local function stateChanged() if playerInfo.anyPlayerSeated then guihooks.trigger("AIStateChange", getState()) end end local function setSpeed(speed) if type(speed) ~= 'number' then M.routeSpeed = nil else M.routeSpeed = speed end end local function setSpeedMode(speedMode) if speedMode == 'set' or speedMode == 'limit' or speedMode == 'legal' or speedMode == 'off' then M.speedMode = speedMode else M.speedMode = nil end end local function resetSpeedModeAndValue() M.speedMode = nil -- maybe this should be 'off' M.routeSpeed = nil end local function setAggressionInternal(v) aggression = v or M.extAggression end local function setAggressionExternal(v) M.extAggression = v or M.extAggression setAggressionInternal() stateChanged() end local function setAggressionMode(aggrmode) if aggrmode == 'rubberBand' then opt.aggressionMode = aggrmode else opt.aggressionMode = nil setAggressionInternal() end end local function resetAggression() setAggressionInternal() end local function resetInternalStates() trafficStates.block = {timer = 0, coef = 0, timerLimit = 6, block = false} trafficStates.side = {timer = 0, cTimer = 0, side = 1, displacement = 0, timerLimit = 6} trafficStates.action = {hornTimer = -1, hornTimerLimit = 1, forcedStop = false} trafficStates.intersection = {timer = 0, turn = 0, block = false} internalState.chaseData.playerState = nil internalState.chaseData.playerStoppedTimer = 0 internalState.chaseData.playerRoad = nil internalState.chaseData.driveAhead = false internalState.chaseData.targetSpeed = nil internalState.crash.time = 0 internalState.crash.manoeuvre = 0 internalState.crash.dir = nil internalState.crash.pos = nil if electrics.values.horn == 1 then electrics.horn(false) end if electrics.values.signal_left_input or electrics.values.signal_right_input then electrics.set_warn_signal(false) end trafficPathState = {} end resetInternalStates() local function resetParameters() -- parameters are used for finer AI control parameters = { turnForceCoef = 2, -- coefficient for curve spring forces awarenessForceCoef = 0.25, -- coefficient for vehicle awareness displacement edgeDist = 0, -- minimum distance from the edge of the road trafficWaitTime = 2, -- traffic delay after stopping at intersection delayFactorWaitTime = {0.55 + (0.95 - 0.55) * math.random() , 0.05 + (0.45 - 0.05) * math.random()}, enableElectrics = true, -- use electrics such as hazard lights (especially for traffic) driveStyle = 'default', staticFrictionCoefMult = 0.95, lookAheadKv = 0.6, applyWidthMarginOffset = true, planErrorSmoothing = true, springForceIntegratorDispLim = 0.1, -- node displacement force magnitude limit understeerThrottleControl = 'on', -- anything other than an 'off' value will keep this active oversteerThrottleControl = 'on', -- anything other than an 'off' value will keep this active throttleTcs = 'on', -- anything other than an 'off' value will keep this active abBrakeControl = 'on', underSteerBrakeControl = 'on', throttleKp = 0.5, targetSpeedSmootherRate = 10, } end resetParameters() local function setParameters(data) tableMerge(parameters, data) end local function dumpParameters() dump(parameters) dump('aggression = ', aggression) end M.dumpParameters = dumpParameters local function setTargetObjectID(id) M.targetObjectID = M.targetObjectID ~= objectId and id or -1 if M.targetObjectID ~= -1 then targetObjectSelectionMode = 'manual' end end local function calculateWheelBase() local avgWheelNodePos, numOfWheels = vec3(), 0 for _, wheel in pairs(wheels.wheels) do -- obj:getNodePosition is the pos vector of query node (wheel.node1) relative to ref node in world coordinates avgWheelNodePos:setAdd(obj:getNodePosition(wheel.node1)) numOfWheels = numOfWheels + 1 end if numOfWheels == 0 then return 0 end avgWheelNodePos:setScaled(1 / numOfWheels) local dirVec = obj:getDirectionVector() local avgFrontWheelPos, frontWheelCount = vec3(), 0 local avgBackWheelPos, backWheelCount = vec3(), 0 for _, wheel in pairs(wheels.wheels) do local wheelPos = obj:getNodePosition(wheel.node1) if wheelPos:dot(dirVec) > avgWheelNodePos:dot(dirVec) then avgFrontWheelPos:setAdd(wheelPos) frontWheelCount = frontWheelCount + 1 else avgBackWheelPos:setAdd(wheelPos) backWheelCount = backWheelCount + 1 end end avgFrontWheelPos:setScaled(1 / frontWheelCount) avgBackWheelPos:setScaled(1 / backWheelCount) return avgFrontWheelPos:distance(avgBackWheelPos) end local function updatePlayerData() mapmgr.getObjects() if not next(mapmgr.objects) then return end -- prevents changes if table is empty if mapmgr.objects[M.targetObjectID] and targetObjectSelectionMode == 'manual' then player = mapmgr.objects[M.targetObjectID] elseif tableSize(mapmgr.objects) == 2 then if player ~= nil then player = mapmgr.objects[player.id] else for k, v in pairs(mapmgr.objects) do if k ~= objectId then M.targetObjectID = k player = v break end end targetObjectSelectionMode = 'auto' end else if player ~= nil and player.active == true then player = mapmgr.objects[player.id] else for k, v in pairs(mapmgr.objects) do if k ~= objectId and v.active == true then M.targetObjectID = k player = v break end end targetObjectSelectionMode = 'targetActive' end end end local function driveCar(steering, throttle, brake, parkingbrake) input.event("steering", steering, "FILTER_AI", nil, nil, nil, "ai") input.event("throttle", throttle, "FILTER_AI", nil, nil, nil, "ai") input.event("brake", brake, "FILTER_AI", nil, nil, nil, "ai") input.event("parkingbrake", parkingbrake, "FILTER_AI", nil, nil, nil, "ai") lastCommand.steering = steering lastCommand.throttle = throttle lastCommand.brake = brake lastCommand.parkingbrake = parkingbrake end local function populateOBBinRange(range) range = range * range local i = 0 for id in pairs(mapmgr.getObjects()) do if id ~= objectId then local pos = obj:getObjectCenterPosition(id) if pos:squaredDistance(ego.pos) < range then twt.OBBinRange[i+1] = pos -- Get bounding box direction vectors twt.OBBinRange[i+2] = obj:getObjectDirectionVector(id) -- x twt.OBBinRange[i+4] = obj:getObjectDirectionVectorUp(id) -- z twt.OBBinRange[i+3] = twt.OBBinRange[i+3] or vec3() twt.OBBinRange[i+3]:setCross(twt.OBBinRange[i+4], twt.OBBinRange[i+2]); twt.OBBinRange[i+3]:normalize() -- y (left) -- Scale bounding box direction vectors to vehicle diamensions twt.OBBinRange[i+3]:setScaled(obj:getObjectInitialWidth(id) * 0.5) twt.OBBinRange[i+2]:setScaled(obj:getObjectInitialLength(id) * 0.5) twt.OBBinRange[i+4]:setScaled(obj:getObjectInitialHeight(id) * 0.5) i = i + 4 end end end twt.OBBinRange.n = i end local function castRay(rpos, rdir, rayDist) for i = 1, twt.OBBinRange.n, 4 do local minHit, maxHit = intersectsRay_OBB(rpos, rdir, twt.OBBinRange[i], twt.OBBinRange[i+1], twt.OBBinRange[i+2], twt.OBBinRange[i+3]) if maxHit > 0 then rayDist = min(rayDist, max(minHit, 0)) end end return obj:castRayStatic(rpos, rdir, rayDist) end local function driveToTarget(targetPos, throttle, brake, targetSpeed) if not targetPos then return end local plan = currentRoute and currentRoute.plan targetSpeed = targetSpeed or plan and plan.targetSpeed if not targetSpeed then return end local targetVec = targetPos - ego.pos; targetVec:normalize() local dirAngle = asin(ego.rightVec:dot(targetVec)) -- oversteer local throttleOverCoef = 1 if ego.speed > 1 then local rightVel = ego.rightVec:dot(ego.vel) if rightVel * ego.rightVec:dot(targetPos - ego.pos) > 0 then local rotVel = min(1, (ego.prevDirVec:projectToOriginPlane(ego.upVec):normalized()):distance(ego.dirVec) * dt * 10000) throttleOverCoef = max(0, 1 - abs(rightVel * ego.speed * 0.05) * min(1, dirAngle * dirAngle * ego.speed * 6) * rotVel) end end local dirVel = ego.vel:dot(ego.dirVec) local absegoSpeed = abs(dirVel) local throttleUnderCoef = 1 local brakeUnderCoef = 1 if plan and plan[3] and dirVel > 3 then local p1, p2 = plan[1].pos, plan[2].pos local p2p1DirVec = p2 - p1; p2p1DirVec:normalize() local tp2 = (plan.targetSeg or 0) > 1 and targetPos or plan[3].pos local targetSide = (tp2 - p2):dot(p2p1DirVec:cross(ego.upVec)) local outDeviation = egoDeviationSmoother:value() - plan.egoDeviation * sign(targetSide) outDeviation = sign(outDeviation) * min(1, abs(outDeviation)) egoDeviationSmoother:set(outDeviation) egoDeviationSmoother:getUncapped(0, dt) if outDeviation > 0 and absegoSpeed > 3 then local steerCoef = outDeviation * absegoSpeed * absegoSpeed * min(1, dirAngle * dirAngle * 4) local understeerCoef = max(0, steerCoef) * min(1, abs(ego.vel:dot(p2p1DirVec) * 3)) local noUndersteerCoef = max(0, 1 - understeerCoef) throttleUnderCoef = noUndersteerCoef brakeUnderCoef = min(brakeUnderCoef, max(0, 1 - understeerCoef * understeerCoef)) end else egoDeviationSmoother:set(0) end -- wheel speed local throttleTcsCoef = 1 local brakeABSCoef = 1 if absegoSpeed > 0.05 then if sensors.gz <= 0.1 then local totalSlip = 0 local propSlip = 0 local totalDownForce = 0 local lwheels = wheels.wheels for i = 0, tableSizeC(lwheels) - 1 do local wd = lwheels[i] if not wd.isBroken then local lastSlip = wd.lastSlip local downForce = wd.downForceRaw totalSlip = totalSlip + lastSlip * downForce totalDownForce = totalDownForce + downForce if wd.isPropulsed then propSlip = max(propSlip, lastSlip) end end end absegoSpeed = max(absegoSpeed, 3) totalSlip = totalSlip / (totalDownForce + 1e-25) -- abs brakeABSCoef = min(1, 1.5 * square(square(square(max(0, absegoSpeed - totalSlip) / absegoSpeed)))) -- tcs propSlip = propSlip * (parameters.driveStyle == 'offRoad' and 0.8 or 1) local tcsCoef = max(0, absegoSpeed - propSlip * propSlip) / absegoSpeed throttleTcsCoef = min(tcsCoef, smoothTcs:get(tcsCoef, dt)) else brakeABSCoef = 0 throttleTcsCoef = 0 end end local brakeCoef = 1 if parameters.underSteerBrakeControl ~= 'off' then brakeCoef = min(brakeCoef, brakeUnderCoef) end if parameters.abBrakeControl ~= 'off' then brakeCoef = min(brakeCoef, brakeABSCoef) end local throttleCoef = 1 if parameters.oversteerThrottleControl ~= 'off' then throttleCoef = min(throttleCoef, throttleOverCoef) end if parameters.understeerThrottleControl ~= 'off' then throttleCoef = min(throttleCoef, throttleUnderCoef) end if parameters.throttleTcs ~= 'off' then throttleCoef = min(throttleCoef, throttleTcsCoef) end local dirTarget = ego.dirVec:dot(targetVec) local dirTargetAxis = ego.rightVec:dot(targetVec) if internalState.crash.manoeuvre == 1 and dirTarget < ego.dirVec:dot(internalState.crash.dir) then driveCar(-sign(dirAngle), brake * brakeCoef, throttle * throttleCoef, 0) return else internalState.crash.manoeuvre = 0 end if parameters.driveStyle == 'offRoad' then brakeCoef = 1 throttleCoef = sqrt(throttleCoef) end internalState.egoForceGoFrontTime = max(0, internalState.egoForceGoFrontTime - dt) if twt.state == 1 and internalState.egoCannotMoveTime > 1 and internalState.egoForceGoFrontTime == 0 then twt.state = 0 internalState.egoCannotMoveTime = 0 internalState.egoForceGoFrontTime = 2 end if internalState.egoForceGoFrontTime > 0 and dirTarget < 0 then dirTarget = -dirTarget dirAngle = -dirAngle end if currentRoute and (dirTarget < 0 or (twt.state == 1 and dirTarget < 0.866)) then -- TODO: Improve entry condition when twt.state == 1 local helperVec = 0.35 * ego.upVec -- auxiliary vector twt.posTable[2]:setAdd2(ego.pos, helperVec) helperVec:setScaled2(ego.dirVec, -0.05 * ego.length) twt.posTable[2]:setAdd(helperVec) helperVec:setScaled2(ego.rightVec, 0.5 * (0.7 * ego.width)) -- lateral translation twt.posTable[2]:setAdd(helperVec) -- front right corner pos helperVec:setScaled(-2) twt.posTable[1]:setAdd2(twt.posTable[2], helperVec) -- front left corner pos helperVec:setScaled2(ego.dirVec, -0.9 * ego.length) -- longitudinal translation twt.posTable[3]:setAdd2(twt.posTable[2], helperVec) -- back right corner pos twt.posTable[4]:setAdd2(twt.posTable[1], helperVec) -- back left corner pos twt.dirTable[1]:setScaled2(ego.rightVec, -1) twt.dirTable[2]:set(ego.dirVec) twt.dirTable[3]:set(ego.rightVec) twt.dirTable[4]:setScaled2(ego.dirVec, -1) local sizeRatio = ego.length/ego.width local blueNoiseRange = 6 + 2 * sizeRatio local rayDist = 4 * ego.wheelBase -- TODO: Optimize rayDist. Higher is better for more open spaces but w performance hit populateOBBinRange(rayDist) local tmpVec = vec3() local RRPicks = max(1, min(floor(150 * dt + 0.5), 8)) -- x * dt where x/fps is the numer of samples for k = 1, RRPicks do --the ray cast loop: each iteration scans one area and casts on the minimum local i = 0 local blueIndex = twt.blueNoiseRR * blueNoiseRange if blueIndex < 3 then i = floor(1 + blueIndex) elseif blueIndex < 6 then i = floor(1 + blueIndex) + 1 elseif blueIndex < 6 + sizeRatio then i = 4 else i = 8 end local j = i * 4 tmpVec:setLerp(twt.posTable[twt.RRT[j-3]], twt.posTable[twt.RRT[j-2]], twt.biasedCoefs[i]) helperVec:setLerp(twt.dirTable[twt.RRT[j-1]], twt.dirTable[twt.RRT[j]], twt.biasedCoefs[i]) -- LERP corner direction helperVec:normalize() local rayLen = castRay(tmpVec, helperVec, min(twt.rayMins[i], rayDist)) if twt.rayMins[i] > rayLen or rayLen == rayDist then twt.minRayCoefs[i] = twt.biasedCoefs[i] twt.rayMins[i] = rayLen else tmpVec:setLerp(twt.posTable[twt.RRT[j-3]], twt.posTable[twt.RRT[j-2]], twt.minRayCoefs[i]) helperVec:setLerp(twt.dirTable[twt.RRT[j-1]], twt.dirTable[twt.RRT[j]], twt.minRayCoefs[i]) -- LERP corner direction helperVec:normalize() twt.rayMins[i] = castRay(tmpVec, helperVec, min(max(2 * twt.rayMins[i], 0.02), rayDist)) end twt.blueNoiseRR = getBlueNoise1d(twt.blueNoiseRR) twt.blueNoiseCoefs[i] = getBlueNoise1d(twt.blueNoiseCoefs[i]) local criticality = 2 * twt.rayMins[i] / (twt.rayMins[i] + rayDist) + 0.25 twt.biasedCoefs[i] = biasGainFun(twt.blueNoiseCoefs[i], twt.minRayCoefs[i], criticality) end local minRayDist = min(twt.rayMins[1], twt.rayMins[2], twt.rayMins[3], twt.rayMins[4], twt.rayMins[5], twt.rayMins[6], twt.rayMins[7], twt.rayMins[8]) twt.sampleCounter = twt.sampleCounter + 1 if twt.state == 0 then if twt.sampleCounter * dt > max(0.5, min(50 * dt, 2.5)) and ego.speed <= 0.5 then -- TODO: Optimize sample count condition twt.state = 1 else local speed = max(0, ego.speed - 0.25) local throttle = max(0, -sign(speed * dirVel)) * 0.3 local brake = max(0, sign(speed * dirVel)) * 0.3 driveCar(0, throttle, brake, 0) end end if twt.state == 1 then -- start driving if twt.targetSpeed < 0.66 or twt.minRay == "S" then -- TODO: Some branches may be consolidated if minRayDist == twt.rayMins[7] then twt.dirState[1] = 1 twt.dirState[2] = sign2(dirTargetAxis) twt.minRay = "BL" elseif minRayDist == twt.rayMins[5] then twt.dirState[1] = 1 twt.dirState[2] = sign2(dirTargetAxis) twt.minRay = "BR" elseif minRayDist == twt.rayMins[1] then twt.dirState[1] = -1 twt.dirState[2] = -sign2(dirTargetAxis) twt.minRay = "FL" if twt.dirState[2] == 1 and twt.rayMins[8] < 0.4 and twt.targetSpeed < 0.6 then twt.dirState[2] = 0 end elseif minRayDist == twt.rayMins[3] then twt.dirState[1] = -1 twt.dirState[2] = -sign2(dirTargetAxis) if twt.dirState[2] == -1 and twt.rayMins[4] < 0.4 and twt.targetSpeed < 0.6 then twt.dirState[2] = 0 end twt.minRay = "FR" elseif minRayDist == twt.rayMins[6] then twt.dirState[1] = 1 twt.dirState[2] = sign2(dirTargetAxis) twt.minRay = "B" elseif minRayDist == min(twt.rayMins[4], twt.rayMins[8]) then local sideRuler = 0 local minSide = minRayDist == twt.rayMins[8] and "L" or "R" if minSide == "L" then sideRuler = ego.length * (1 - twt.minRayCoefs[8]) else sideRuler = ego.length * twt.minRayCoefs[4] end twt.dirState[2] = 0 if sideRuler < twt.rayMins[6] and dirTarget < 0 then --TODO : test this twt.dirState[1] = -1 elseif (ego.length - sideRuler) < twt.rayMins[2] then twt.dirState[1] = 1 if minSide == "L" then twt.dirState[2] = 1 else twt.dirState[2] = -1 end end twt.minRay = "S" else twt.dirState[1] = -1 twt.dirState[2] = -sign2(dirTargetAxis) twt.minRay = "F" end end local threshold = min(ego.speed - 0.1, 0.66) local dirCoef, minDist = 1, nil --TODO: what is car is trapped/sandwiched? if twt.dirState[1] == -1 then -- reverse dirCoef = min(1.2 - dirTarget, 1) -- dirTarget -> targetSpeed modulation if twt.dirState[2] == -1 then -- steer left minDist = min(twt.rayMins[4], twt.rayMins[6], twt.rayMins[7]) elseif twt.dirState[2] == 1 then -- steer right minDist = min(twt.rayMins[5], twt.rayMins[6], twt.rayMins[8]) else -- no steering minDist = twt.rayMins[6] if twt.minRay ~= "S" then threshold = twt.rayMins[2] -- possibly max(threshold, twt.rayMins[2]) end end else -- forward if twt.dirState[2] == -1 then minDist = min(twt.rayMins[1], twt.rayMins[2], twt.rayMins[8]) elseif twt.dirState[2] == 1 then minDist = min(twt.rayMins[2], twt.rayMins[3], twt.rayMins[4]) else minDist = twt.rayMins[2] end end local targetSpeed = sqrt(2 * g * min(aggression, ego.staticFrictionCoef) * max(0, minDist - threshold) * dirCoef) twt.targetSpeed = max(min(twt.speedSmoother:get(targetSpeed, dt), min(6, aggression * 6)), 0.3) local speedDif = twt.targetSpeed - twt.dirState[1] * sign2(dirVel) * ego.speed local steering = twt.steerSmoother:get(twt.dirState[2], dt) local pbrake = 0 -- * clamp(sign2(0.83 + ego.upVec:dot(gravityDir)), 0, 1) -- >= 10 deg local throttle, brake = 0, 0 if twt.dirState[1] == -1 then throttle = clamp(-speedDif * 0.4, 0, 0.4) brake = clamp(speedDif * 0.2, 0, 1) elseif twt.dirState[1] == 1 then throttle = clamp(speedDif * 0.2, 0, 1) brake = clamp(-speedDif * 0.4, 0, 0.4) end driveCar(steering, throttle, brake, pbrake) end else twt.reset() local pbrake if ego.vel:dot(ego.dirVec) < 0 and ego.speed > 0.1 then -- TODO: handbrake when stoped or moving backward -- apply brake (in arcade mode throttle input when moving backwards activates the brakes) and or parkingbrake -- when vehicle is moving backwards but the target speed is effectively zero if ego.speed < 0.15 and targetSpeed <= 1e-5 then pbrake = 1 else pbrake = 0 end throttle = 0.5 * throttleCoef brake = 0 else if (ego.speed > 4 and ego.speed < 30 and abs(dirAngle) > 0.97 and brake == 0) or (ego.speed < 0.15 and targetSpeed <= 1e-5) then -- The first condition was supposed to help the vehicles turn in tight corners by applying handbrake. This is very old logic. It might -- no longer be needed. Revise. -- The second condition is supposed to apply handbrake when the vehicle is at a stop but it looks to be unstable -- TODO: handbrake when turning pbrake = 1 else pbrake = 0 end throttle = throttle * throttleCoef brake = brake * brakeCoef end local aggSq = square(aggression + max(0, -(ego.dirVec:dot(gravityDir)))) local rate = max(throttleSmoother[throttleSmoother:value() < throttle], 10 * aggSq * aggSq) throttle = throttleSmoother:getWithRateUncapped(throttle, dt, rate) driveCar(dirAngle, throttle, brake, pbrake) end end local function posOnPlan(pos, plan, dist) if not plan then return end dist = dist or 4 dist = dist * dist local bestSeg, bestXnorm for i = 1, #plan-2 do local p0, p1 = plan[i].pos, plan[i+1].pos local xnorm1 = pos:xnormOnLine(p0, p1) if xnorm1 > 0 then local p2 = plan[i+2].pos local xnorm2 = pos:xnormOnLine(p1, p2) if xnorm1 < 1 then -- contained in segment i if xnorm2 > 0 then -- also partly contained in segment i+1 local sqDistFromP1 = pos:squaredDistance(p1) if sqDistFromP1 <= dist then bestSeg = i bestXnorm = 1 break -- break inside conditional end else local sqDistFromLine = pos:squaredDistance(p0 + (p1 - p0) * xnorm1) if sqDistFromLine <= dist then bestSeg = i bestXnorm = xnorm1 end break -- break should be outside above conditional end elseif xnorm2 < 0 then local sqDistFromP1 = pos:squaredDistance(p1) if sqDistFromP1 <= dist then bestSeg = i bestXnorm = 1 end break -- break outside conditional end else break end end return bestSeg, bestXnorm end local function egoPosOnPlan(plan) local planCount = plan.planCount local egoSeg = 1 local egoXnormOnSeg = 0 for i = 1, planCount-1 do local p0Pos, p1Pos = plan[i].pos, plan[i+1].pos local xnorm = ego.pos:xnormOnLine(p0Pos, p1Pos) if xnorm < 1 then if i < planCount - 2 then local nextXnorm = ego.pos:xnormOnLine(p1Pos, plan[i+2].pos) if nextXnorm >= 0 then local p1Radius = plan[i+1].radiusOrig if ego.pos:squaredDistance(linePointFromXnorm(p1Pos, plan[i+2].pos, nextXnorm)) < square(ego.width + lerp(p1Radius, plan[i+2].radiusOrig, min(1, nextXnorm))) then egoXnormOnSeg = nextXnorm egoSeg = i + 1 break end end end egoXnormOnSeg = xnorm egoSeg = i break end end local disp = 0 if egoSeg > 1 then local sumLen = 0 disp = egoSeg - 1 for i = 1, disp do sumLen = sumLen + plan[i].length end for i = 1, plan.planCount do plan[i] = plan[i+disp] if disp > 0 and i + disp > plan.planCount then velocities[i] = vec3(0,0,0) else velocities[i] = velocities[i+disp] end end plan.planCount = plan.planCount - disp plan.planLen = max(0, plan.planLen - sumLen) plan.stopSeg = max(1, plan.stopSeg - disp) end plan.egoXnormOnSeg = egoXnormOnSeg plan.egoSeg = egoSeg - disp end -- returns the node index local function getLastNodeWithinDistance(plan, dist) dist = dist - plan[1].length * (1 - plan.egoXnormOnSeg) if dist < 0 then return 1 end local planSeg = plan.planCount for i = 2, plan.planCount-1 do dist = dist - plan[i].length if dist < 0 then planSeg = i break end end return planSeg end local function calculateTarget(plan) egoPosOnPlan(plan) local targetLength = max(ego.speed * parameters.lookAheadKv, 4.5) if plan.planCount >= 3 then local xnorm = clamp(plan.egoXnormOnSeg, 0, 1) targetLength = max(targetLength, plan[1].length * (1 - xnorm), plan[2].length * xnorm) end local remainder = targetLength local targetPos = vec3(plan[plan.planCount].pos) local targetSeg = max(1, plan.planCount-1) local prevPos = linePointFromXnorm(plan[1].pos, plan[2].pos, plan.egoXnormOnSeg) -- ego.pos local segVec, segLen = vec3(), nil for i = 2, plan.planCount do local pos = plan[i].pos segVec:setSub2(pos, prevPos) segLen = segLen or segVec:length() if remainder <= segLen then targetSeg = i - 1 targetPos:setScaled2(segVec, remainder / (segLen + 1e-30)); targetPos:setAdd(prevPos) -- smooth target local xnorm = clamp(targetPos:xnormOnLine(prevPos, pos), 0, 1) local lp_n1n2 = linePointFromXnorm(prevPos, pos, xnorm * 0.5 + 0.25) if xnorm <= 0.5 then if i >= 3 then targetPos = linePointFromXnorm(linePointFromXnorm(plan[i-2].pos, prevPos, xnorm * 0.5 + 0.75), lp_n1n2, xnorm + 0.5) end else if i <= plan.planCount - 2 then targetPos = linePointFromXnorm(lp_n1n2, linePointFromXnorm(pos, plan[i+1].pos, xnorm * 0.5 - 0.25), xnorm - 0.5) end end break end prevPos = pos remainder = remainder - segLen segLen = plan[i].length end plan.targetPos = targetPos plan.targetSeg = targetSeg end local function targetsCompatible(baseRoute, newRoute) local baseTvec = baseRoute.plan.targetPos - ego.pos local newTvec = newRoute.plan.targetPos - ego.pos if ego.speed < 2 then return true end if newTvec:dot(ego.dirVec) * baseTvec:dot(ego.dirVec) <= 0 then return false end local baseTargetRight = baseTvec:cross(ego.upVec); baseTargetRight:normalize() return abs(newTvec:normalized():dot(baseTargetRight)) * ego.speed < 2 end local function getMinPlanLen(limLow, speed, accelg) -- given current speed, distance required to come to a stop if I can decelerate at 0.2g limLow = limLow or 150 speed = speed or ego.speed accelg = max(0.2, accelg or 0.2) return min(550, max(limLow, 0.5 * speed * speed / (accelg * g))) end local function pickAiWp(wp1, wp2, dirVec) dirVec = dirVec or ego.dirVec local vec1 = mapData.positions[wp1] - ego.pos local vec2 = mapData.positions[wp2] - ego.pos local dot1 = vec1:dot(dirVec) local dot2 = vec2:dot(dirVec) if (dot1 * dot2) <= 0 then if dot1 < 0 then return wp2, wp1 end else if vec2:squaredLength() < vec1:squaredLength() then return wp2, wp1 end end return wp1, wp2 end local function pathExtend(path, newPath) if newPath == nil then return end local pathCount = #path if path[pathCount] ~= newPath[1] then return end pathCount = pathCount - 1 for i = 2, #newPath do path[pathCount+i] = newPath[i] end end -- http://cnx.org/contents/--TzKjCB@8/Projectile-motion-on-an-inclin local function projectileSqSpeedToRangeRatio(pos1, pos2, pos3) local sinTheta = (pos2.z - pos1.z) / pos1:distance(pos2) local sinAlpha = (pos3.z - pos2.z) / pos2:distance(pos3) local cosAlphaSquared = max(1 - sinAlpha * sinAlpha, 0) local cosTheta = sqrt(max(1 - sinTheta * sinTheta, 0)) -- in the interval theta = {-pi/2, pi/2} cosTheta is always positive return 0.5 * g * cosAlphaSquared / max(cosTheta * (sinTheta*sqrt(cosAlphaSquared) - cosTheta*sinAlpha), 0) end local function getPathLen(path, startIdx, stopIdx) if not path then return end startIdx = startIdx or 1 stopIdx = stopIdx or #path local positions = mapData.positions local pathLen = 0 for i = startIdx+1, stopIdx do pathLen = pathLen + positions[path[i-1]]:distance(positions[path[i]]) end return pathLen end local function getPathBBox(path, startIdx, stopIdx) if not path then return end startIdx = startIdx or 1 stopIdx = stopIdx or #path local positions = mapData.positions local p = positions[path[startIdx]] local v = positions[path[stopIdx]] + p; v:setScaled(0.5) local r = getPathLen(path, startIdx, stopIdx) * 0.5 return {v.x - r, v.y - r, v.x + r, v.y + r}, v, r end local function waypointInPath(path, waypoint, startIdx, stopIdx) if not path or not waypoint then return end startIdx = startIdx or 1 stopIdx = stopIdx or #path for i = startIdx, stopIdx do if path[i] == waypoint then return i end end end local function getPlanLen(plan, from, to) from = max(1, from or 1) to = min(plan.planCount-1, to or math.huge) local planLen = 0 for i = from, to do planLen = planLen + plan[i].length end return planLen end local function abortUpcommingLaneChange() if not currentRoute then return end if currentRoute.laneChanges[1] then currentRoute.laneChanges[1].side = 0 end local lastPlanPidx = currentRoute.plan[currentRoute.plan.planCount].pathidx for _, lc in ipairs(currentRoute.laneChanges) do if lc.pathIdx <= lastPlanPidx then lc.side = 0 end end end local function updatePlanLen(plan, j, k) --profilerPushEvent("ai_update_planLen") -- bulk recalculation of plan edge lengths and length of entire plan -- j: index of earliest node position that has changed -- k: index of latest node position that has changed j = max((j or 1) - 1, 1) k = min(k or plan.planCount, plan.planCount-1) local planLen = plan.planLen for i = j, k do local edgeLen = plan[i+1].pos:distance(plan[i].pos) planLen = planLen - plan[i].length + edgeLen plan[i].length = edgeLen end plan.planLen = planLen --profilerPopEvent("ai_update_planLen") end -- expand lane lateral limits of this plan node to the lane range limits local function openLaneToLaneRange(planNode) local roadHalfWidth = planNode.halfWidth planNode.laneLimLeft = linearScale(planNode.rangeLeft, 0, 1, -roadHalfWidth, roadHalfWidth) planNode.laneLimRight = linearScale(planNode.rangeRight, 0, 1, -roadHalfWidth, roadHalfWidth) planNode.lanesOpen = true end -- expand lane lateral limits to the lane range for all nodes up to and including idx local function openPlanLanesToLaneRange(plan, idx) plan = plan or (currentRoute and currentRoute.plan) if not plan then return end for i = 1, min(plan.planCount, idx) do openLaneToLaneRange(plan[i]) end end local function laneChange(plan, dist, signedDisp) if not plan and currentRoute then plan = currentRoute.plan end if not plan then return end -- Apply node displacement local invDist = 1 / (dist + 1e-30) local curDist, normalDispVec = 0, vec3() for i = 2, plan.planCount do openLaneToLaneRange(plan[i]) curDist = curDist + plan[i-1].length plan[i].lateralXnorm = clamp(plan[i].lateralXnorm + signedDisp * min(curDist * invDist, 1), plan[i].laneLimLeft, plan[i].laneLimRight) normalDispVec:setScaled2(plan[i].normal, plan[i].lateralXnorm) plan[i].pos:setAdd2(plan[i].posOrig, normalDispVec) -- Recalculate vec and dirVec plan[i].vec:setSub2(plan[i-1].pos, plan[i].pos); plan[i].vec.z = 0 plan[i].dirVec:set(plan[i].vec) plan[i].dirVec:normalize() end updatePlanLen(plan) --[[ For debugging table.clear(newPositionsDebug) for i = 1, #newPositions do newPositionsDebug[i] = vec3(newPositions[i]) end --]] end local function setStopPoint(plan, dist, args) if not plan and currentRoute then plan = currentRoute.plan end if not plan then return end if not dist then -- reset stop segment plan.stopSeg = math.huge return end if (args or E).avoidJunction and currentRoute.path then -- prevents stopping directly in junctions -- this code is temporary, and inefficient... local seg while true do seg = getLastNodeWithinDistance(plan, dist) local nid = currentRoute.path[plan[seg].pathidx] if seg < plan.planCount - 1 and tableSize(mapData.graph[nid]) > 2 and plan[seg].pos:squaredDistance(mapData.positions[nid]) <= square(mapData.radius[nid]) then dist = dist + 20 else break end end plan.stopSeg = seg else plan.stopSeg = getLastNodeWithinDistance(plan, dist) end end local function numOfLanesFromRadius(rad1, rad2) return max(1, math.floor(min(rad1, rad2 or math.huge) * 2 / 3.61 + 0.5)) -- math.floor(min(rad1, rad2) / 2.7) + 1 end local laneStringBuffer = buffer.new() local function flipLanes(lanes) -- ex. '--+++' becomes '---++' for i = #lanes, 1, -1 do laneStringBuffer:put(lanes:byte(i) == 43 and '-' or lanes:byte(i) == 45 and '+' or '0') end return laneStringBuffer:get() end -- returns the number of lanes in a given direction local function numOfLanesInDirection(lanes, dir) -- lanes: a lane string -- dir: '+' or '-' dir = dir == '+' and 43 or dir == '-' and 45 local lanesN = 0 for i = 1, #lanes, 1 do if lanes:byte(i) == dir then lanesN = lanesN + 1 end end return lanesN end -- Returns the lane configuration of an edge as traversed in the fromNode -> toNode direction -- if an edge does not have lane data they are deduced from the node radii local function getEdgeLaneConfig(fromNode, toNode) local lanes local edge = mapData.graph[fromNode][toNode] if edge.lanes then lanes = edge.lanes else -- make up some lane data in case they don't exist if edge.oneWay then local numOfLanes = numOfLanesFromRadius(mapData.radius[fromNode], mapData.radius[toNode]) lanes = string.rep("+", numOfLanes) else local numOfLanes = max(1, math.floor(numOfLanesFromRadius(mapData.radius[fromNode], mapData.radius[toNode]) * 0.5)) if mapmgr.rules.rightHandDrive then lanes = string.rep("+", numOfLanes)..string.rep("-", numOfLanes) else lanes = string.rep("-", numOfLanes)..string.rep("+", numOfLanes) end end end return edge.inNode == fromNode and lanes or flipLanes(lanes) -- flip lanes string based on inNode data end -- Calculate the edge incident on wp2 which is most similar to the edge wp1->wp2 local function roadNaturalContinuation(wp1, wp2) local inLaneConfig = getEdgeLaneConfig(wp1, wp2) local inRadiuswp2, inRadiuswp1 = mapData:getEdgeRadii(wp2, wp1) local inEdgeDir = vec3(); inEdgeDir:setSub2(mapData:getEdgePositions(wp2, wp1)); inEdgeDir:normalize() local laneFlow = mapData.graph[wp1][wp2].drivability * 4 * min(inRadiuswp2, inRadiuswp1) / #inLaneConfig local inLaneCount = numOfLanesInDirection(inLaneConfig, '+') local inFwdFlow = inLaneCount * laneFlow local inLaneCountOpposite = (#inLaneConfig - inLaneCount) local inBackFlow = inLaneCountOpposite * laneFlow local outEdgeDir, maxOutflow, minNode = vec3(), 0, nil for k, v in pairs(mapData.graph[wp2]) do if k ~= wp1 then local outLaneConfig = getEdgeLaneConfig(wp2, k) local numOfOutLanes = numOfLanesInDirection(outLaneConfig, '+') outEdgeDir:setSub2(mapData:getEdgePositions(k, wp2)); outEdgeDir:normalize() local dirCoef = 0.5 * max(0, 1 + outEdgeDir:dot(inEdgeDir)) local outLaneCountOpposite = (#outLaneConfig - numOfOutLanes) local outRadiuswp2, outRadiusk = mapData:getEdgeRadii(wp2, k) if numOfOutLanes == inLaneCount and outLaneCountOpposite == inLaneCountOpposite then laneFlow = mapData.graph[wp2][k].drivability * 4 * 0.5 * (inRadiuswp2 + outRadiuswp2) / #outLaneConfig else laneFlow = mapData.graph[wp2][k].drivability * 4 * min(outRadiuswp2, outRadiusk)/ #outLaneConfig end local outFwdFlow = min(inFwdFlow, numOfOutLanes * laneFlow) local outBackFlow = min(inBackFlow, outLaneCountOpposite * laneFlow) local outflow = outFwdFlow * (1 + outBackFlow) * dirCoef if outflow > maxOutflow then maxOutflow = outflow minNode = k end end end return minNode end -- returns the lane indices of the left most and right most lanes in the direction of travel local function laneRangeIdx(laneConfig) local numOfLanes = #laneConfig local leftIdx, rightIdx = 1, numOfLanes for i = 1, numOfLanes do if laneConfig:byte(i) == 43 then -- "+" leftIdx = i break end end for i = numOfLanes, 1, -1 do if laneConfig:byte(i) == 43 then -- "+" rightIdx = i break end end return leftIdx, rightIdx, numOfLanes end -- returns the lane lateral xnorm limits (boundaries in [0, 1] range 0 being the left boundary) -- of the left most and right most lanes in the direction of travel and the number of lanes in that range. -- This range might include lanes in the opposite direction if they are interleaved with lanes in the direction of travel -- ex. '--++-++' the left most lane is lane 3, the right most lane is lane 7. The range is from lane 3 to lane 7 inclusive. local function laneRange(laneConfig) local leftLim, rightLim, numOfLanes = laneRangeIdx(laneConfig) return (leftLim - 1) / numOfLanes, rightLim / numOfLanes, rightLim - leftLim + 1 end -- Splits the lane string into three parts 1:leftIdx-1, leftIdx:rightIdx, rightIdx+1:numOfLanes and returns the three strings -- leftIdx is the indice of the left most lane in the direction of travel -- rightIdx is the indice of the right most lane in the direction of travel -- therefore leftIdx:rightIdx is the lane string from the left most to the right most lane in the direction of travel local function splitLaneRange(laneConfig) local leftIdx, rightIdx, numOfLanes = laneRangeIdx(laneConfig) --leftIdx = leftIdx --rightIdx = rightIdx return laneConfig:sub(1, leftIdx-1), laneConfig:sub(leftIdx, rightIdx), laneConfig:sub(rightIdx+1, numOfLanes) end -- Lateral xnorm limits ([0, 1]) of the left most and right most lanes from "lane" in the direction of "lane" local function getLaneRangeFromLane(laneConfig, lane) -- laneConfig: a string representing lane configuration ex. "-a-a+a+a" -- lane: the lane number (1 to n) counting from left to right in the direction of travel local numOfLanes = #laneConfig local leftIdx, rightIdx = lane, lane -- search left for i = lane-1, 1, -1 do if laneConfig:byte(i) == 43 then -- "+" leftIdx = i else break end end -- search right for i = lane+1, numOfLanes do if laneConfig:byte(i) == 43 then -- "+" rightIdx = i else break end end return (leftIdx-1) / numOfLanes, rightIdx / numOfLanes, leftIdx, rightIdx end -- Calculates the composite lane configuration coming into newNode -- and adjusted lateral position, lane limits and lane range of the last plan node. local function processNodeIncommingLanes(newNode, planNode) local leftRange, centerRange, rightRange = splitLaneRange(newNode.inEdgeLanes) local latXnorm = planNode.lateralXnorm local laneLimLeft = planNode.laneLimLeft local laneLimRight = planNode.laneLimRight local halfWidth = planNode.halfWidth local numOfOutLanes = numOfLanesInDirection(newNode.outEdgeLanes, "+") local numOfInLanes = numOfLanesInDirection(newNode.inEdgeLanes, "+") local defaultLaneWidth = min(2 * halfWidth / (#newNode.inEdgeLanes), 3.45) local inEdgeDrivabilityInv = 1 / ((newNode.inEdgedrivability or 1) + 1e-30) if not planNode.rangeLeft then planNode.rangeLeft, planNode.rangeRight = laneRange(newNode.inEdgeLanes) end local rangeLeft = linearScale(planNode.rangeLeft or 0, 0, 1, -halfWidth, halfWidth) local rangeRight = linearScale(planNode.rangeRight or 1, 0, 1, -halfWidth, halfWidth) for nodeId, edgeData in pairs(mapData.graph[newNode.name]) do if nodeId ~= newNode.nextNodeInPath and nodeId ~= newNode.prevNodeInPath then -- and not mapmgr.signalsData.nodes[newNode.name] local lanes = getEdgeLaneConfig(nodeId, newNode.name) local thisEdgeInLanes = numOfLanesInDirection(lanes, '+') if thisEdgeInLanes > 0 then -- TODO: Should also possibly include traffic light data local nodePos = mapData.positions[nodeId] local dirRatio = max(0, (mapData.positions[newNode.nextNodeInPath or newNode.name] - newNode.posOrig):normalized():dot((newNode.posOrig - nodePos):normalized())) local drivabilityRatio = min(1, edgeData.drivability * inEdgeDrivabilityInv) thisEdgeInLanes = math.floor(thisEdgeInLanes * dirRatio * drivabilityRatio + 0.5) if thisEdgeInLanes > 0 then if newNode.inEdgeNormal:dot(nodePos) < newNode.inEdgeNormal:dot(newNode.posOrig) then -- coming into newNode from the left numOfInLanes = numOfInLanes + thisEdgeInLanes centerRange = string.rep("+", thisEdgeInLanes)..centerRange local radius = thisEdgeInLanes * defaultLaneWidth * 0.5 latXnorm = latXnorm + radius laneLimLeft = laneLimLeft + radius laneLimRight = laneLimRight + radius halfWidth = halfWidth + radius rangeLeft = rangeLeft - radius rangeRight = rangeRight + radius -- TODO: is this correct? elseif numOfInLanes < numOfOutLanes then local capInLanes = min(numOfInLanes + thisEdgeInLanes, numOfOutLanes) thisEdgeInLanes = capInLanes - numOfInLanes numOfInLanes = capInLanes local radius = thisEdgeInLanes * defaultLaneWidth * 0.5 centerRange = centerRange..string.rep("+", thisEdgeInLanes) latXnorm = latXnorm - radius laneLimLeft = laneLimLeft - radius laneLimRight = laneLimRight - radius halfWidth = halfWidth + radius rangeLeft = rangeLeft - radius rangeRight = rangeRight + radius end end end end end return leftRange..centerRange..rightRange, latXnorm, laneLimLeft, laneLimRight, rangeLeft, rangeRight, halfWidth end -- Calculate the most appropriate lane of laneConfig local function getBestLane(laneConfig, nodeLatPos, laneLeftLimLatPos, laneRightLimLatPos, plan, newNode) -- nodeLatPos, laneLeftLimLatPos, laneRightLimLatPos are in the [0, 1] interval local laneWidth = laneRightLimLatPos - laneLeftLimLatPos local numOfLanes = max(1, #laneConfig) local bestError = math.huge local bestLane, newLaneLimLeft, newLaneLimRight local planPos, dirVec if plan then planPos = plan[plan.planCount].pos dirVec = plan[plan.planCount].pos - plan[max(1, plan.planCount-1)].pos; dirVec:normalize() else planPos = vec3() dirVec = planPos newNode = {} newNode.posOrig = planPos newNode.radiusOrig = 0 newNode.halfWidth = 0 newNode.normal = planPos newNode.outEdgeLanes = laneConfig end if #newNode.outEdgeLanes == #laneConfig then -- TODO: this might not be correct dirVec:set(0, 0, 0) end -- Get driving side parameter of the map for more exterior lane selection local isRightHandDrive = mapmgr.rules.rightHandDrive local newNodeWidthVec = newNode.halfWidth * newNode.normal for i = 1, numOfLanes do -- traverse lanes in laneConfig local thisLaneLimLeft, thisLaneLimRight = (i - 1) / numOfLanes, i / numOfLanes local thisLaneLatPos = (thisLaneLimRight + thisLaneLimLeft) * 0.5 local laneColinearity = ((thisLaneLatPos * 2 - 1) * push3(newNodeWidthVec) + newNode.posOrig - planPos):normalized():dot(dirVec) local colinearityError = 1 - max(0, laneColinearity) local distError = abs(thisLaneLatPos - nodeLatPos) local overlapError = laneWidth - max(0, min(thisLaneLimRight, laneRightLimLatPos) - max(thisLaneLimLeft, laneLeftLimLatPos)) local directionError = laneConfig:byte(i) == 43 and 0 or 1 -- lane bias, as driving-side aware, for right-hand drive: prefer leftmost lanes, for left-hand drive: prefer rightmost lanes local laneBias = isRightHandDrive and i - 1 or numOfLanes - i local totalError = distError + overlapError * 10 + directionError * 100 + laneBias * 0.1 + colinearityError * 3 if totalError < bestError then bestError = totalError bestLane = i newLaneLimLeft = thisLaneLimLeft newLaneLimRight = thisLaneLimRight end end return bestLane, newLaneLimLeft, newLaneLimRight -- lateral positions are in the [0, 1] interval end local function getPathNodePosition(route, i) local path = route.path local wp1 = path[i-1] or route.plan[1].wp local wp2 = path[i] local wp3 = path[i+1] --dump('---- > in', i, path[i]) if not wp1 and not wp3 then --dump('!!!!!!!!!!!!!!', path, objectId) return mapData.positions[wp2]:copy() elseif not wp1 then local wp2Pos = mapData:getEdgePositions(wp2, wp3) return wp2Pos:copy() elseif not wp3 then local _, wp2Pos = mapData:getEdgePositions(wp1, wp2) return wp2Pos:copy() else local e1P1, e1P2 = mapData:getEdgePositions(wp1, wp2) local e2P1, e2P2 = mapData:getEdgePositions(wp2, wp3) --dump(wp1, wp2, wp3, e1P2:squaredDistance(e2P1)) if e1P2:squaredDistance(e2P1) < 0.005 then --dump('b0') return (e1P2 + e2P1) * 0.5 else local e1Xnorm, e2Xnorm = closestLinePoints(e1P1, e1P2, e2P1, e2P2) local e2Xnorm2 = closestLinePoints(e2P1, e2P2, e1P1, e1P2) local _, e1R2 = mapData:getEdgeRadii(wp1, wp2) local e2R1 = mapData:getEdgeRadii(wp2, wp3) if e1Xnorm == 0 and e2Xnorm2 == 0 then -- segments are parallel --dump('b1') return (e1P2 + e2P1) * 0.5 elseif e1Xnorm >= 0 and (e1Xnorm - 1) * e1P2:distance(e1P1) <= e1R2 and e2Xnorm * e2P1:distance(e2P2) >= -e2R1 and e2Xnorm <= 1 then -- intersection is within the (radius augmented at P2) e1 and e2 segments --dump('b2', 'e1Xnorm = ', e1Xnorm, 'e2Xnorm = ', e2Xnorm) local p1 = linePointFromXnorm(e1P1, e1P2, e1Xnorm) local p2 = linePointFromXnorm(e2P1, e2P2, e2Xnorm) p1:setAdd(p2); p1:setScaled(0.5) return p1 elseif e2Xnorm < 1 and e1Xnorm >= 0 and (e1Xnorm - 1) * e1P2:distance(e1P1) <= e1R2 then -- intersection is within (radius augmented at P2) e1 segment and not furnter than the end of e2 (enures that it is not over segment 2 end, if it is too short) --dump('b3', 'e1Xnorm = ', e1Xnorm, 'e2Xnorm = ', e2Xnorm) local segLen = e1P2:distance(e1P1) return linePointFromXnorm(e1P1, e1P2, max(e1Xnorm, 1 - e1R2/segLen)) elseif e1Xnorm > 0 and e2Xnorm * e2P1:distance(e2P2) >= -e2R1 and e2Xnorm <= 1 then -- intersection is within (radius augmented at P2) e2 segment and infront of e1P1 --dump('b4', 'e1Xnorm = ', e1Xnorm, 'e2Xnorm = ', e2Xnorm) local segLen = e2P1:distance(e2P2) return linePointFromXnorm(e2P1, e2P2, min(e2Xnorm, e2R1/segLen)) elseif e2Xnorm >= 1 or e1Xnorm < 0 then -- intersection is over segment 2 end or before segment 1 start (particular case) --dump('b5') return (e1P2 + e2P1) * 0.5 else -- intersection is between segments 1 and 2 but outside of radii extensions --dump('b6') local p1 = linePointFromXnorm(e1P1, e1P2, e1Xnorm) local p2 = linePointFromXnorm(e2P1, e2P2, e2Xnorm) p1:setAdd(p2); p1:setScaled(0.5) local avgPoint = (e1P2 + e2P1) * 0.5 local avgPointToP1Line = p1 - avgPoint return avgPoint + max(0, min(1, (e1R2 + e2R1) * 0.5 / avgPointToP1Line:length())) * avgPointToP1Line end end end end local function getPathNodeRadius(path, i) if not path[i-1] and not path[i+1] then return mapData.radius[path[i]] elseif not path[i-1] then local wp1Rad = mapData:getEdgeRadii(path[i], path[i+1]) return wp1Rad elseif not path[i+1] then local _, wp2Rad = mapData:getEdgeRadii(path[i-1], path[i]) return wp2Rad else local wp1Pos, wp2Pos = mapData:getEdgePositions(path[i-1], path[i]) local wp3Pos, wp4Pos = mapData:getEdgePositions(path[i], path[i+1]) local e1Xnorm, e2Xnorm = closestLineSegmentPoints(wp1Pos, wp2Pos, wp3Pos, wp4Pos) local wp1Rad, wp2Rad = mapData:getEdgeRadii(path[i-1], path[i]) local wp3Rad, wp4Rad = mapData:getEdgeRadii(path[i], path[i+1]) local r1 = wp1Rad + (wp2Rad - wp1Rad) * e1Xnorm local r2 = wp3Rad + (wp4Rad - wp3Rad) * e2Xnorm return (r1 + r2) * 0.5 end end local function buildNextRoute(route, currentplan) local plan, path = currentplan or route.plan, route.path local planCount = plan.planCount local nextPathIdx = (plan[planCount].pathidx or 0) + 1 if loopPath == true and noOfLaps and noOfLaps > 1 and not path[nextPathIdx] then -- in case the path loops local loopPathId local pathCount = #path local lastWayPoint = path[pathCount] for i = 1, pathCount do if lastWayPoint == path[i] then loopPathId = i break end end nextPathIdx = 1 + loopPathId -- nextPathIdx % #path noOfLaps = not plan.copy and noOfLaps - 1 or noOfLaps -- avoid decreasing noOfLaps for alternative plans end local newNodeName = path[nextPathIdx] if not newNodeName then return end local graph = mapData.graph if not graph[newNodeName] then return end -- gather information about new node local tmpPos = getPathNodePosition(route, nextPathIdx) local newNode = { name = newNodeName, posOrig = tmpPos, pos = tmpPos:copy(), radiusOrig = getPathNodeRadius(path, nextPathIdx), biNormal = -mapmgr.surfaceNormalBelow(mapData.positions[newNodeName], mapData.radius[newNodeName] * 0.5), prevNodeInPath = path[nextPathIdx-1], inEdgeDrivability = nil, inEdgeLanes = nil, -- lanes going into the node along the path inEdgeSpeedLimit = nil, nextNodeInPath = path[nextPathIdx+1], outEdgeLanes = nil, -- lanes coming out of the node along the path outEdgeSpeedLimit = nil, } if newNode.prevNodeInPath then newNode.prevNodePos = mapData.positions[newNode.prevNodeInPath] local link = graph[newNode.prevNodeInPath][newNode.name] if link then newNode.inEdgeLanes = getEdgeLaneConfig(newNode.prevNodeInPath, newNode.name) newNode.inEdgeSpeedLimit = link.speedLimit newNode.inEdgeDrivability = link.drivability end else -- if previous plan node is the ego.pos then newNode.prevNodePos = vec3(plan[1].posOrig) if planCount == 1 and plan[1].wp then -- check whether information is available --newNode.inEdgeLanes = plan[1].lanes -- TODO: use plan[1].wp to get lane configuration instead of saving the lane configuration in plan[1].lanes? newNode.prevNodeInPath = plan[1].wp newNode.inEdgeLanes = getEdgeLaneConfig(newNode.prevNodeInPath, newNode.name) newNode.inEdgeDrivability = graph[newNode.prevNodeInPath][newNode.name].drivability end end -- if there is no nextNodeInPath can we deduce it by elimination? -- we need to see if there is a unique node towards which we can lawfully drive if not newNode.nextNodeInPath then local nextPosibleNode = nil for k, v in pairs(graph[newNode.name]) do if k ~= newNode.prevNodeInPath then if numOfLanesInDirection(getEdgeLaneConfig(newNode.name, k), '+') > 0 then if nextPosibleNode then nextPosibleNode = nil break else nextPosibleNode = k end end end end newNode.nextNodeInPath = nextPosibleNode end if newNode.nextNodeInPath then local link = graph[newNode.name][newNode.nextNodeInPath] if link then newNode.outEdgeLanes = getEdgeLaneConfig(newNode.name, newNode.nextNodeInPath) newNode.outEdgeSpeedLimit = link.speedLimit end else if M.mode ~= 'traffic' then newNode.outEdgeLanes = newNode.inEdgeLanes newNode.outEdgeSpeedLimit = newNode.inEdgeSpeedLimit else return end end if newNode.outEdgeLanes and not newNode.inEdgeLanes then newNode.inEdgeLanes = newNode.outEdgeLanes end -- Adjust last plan node normal given information about node to be inserted: -- The normal of the node that is currently the last in the plan may need to be updated -- either because the path has been extended (where previously it was the last node in the path) -- or because the path has changed from this node forwards if planCount > 1 then if plan[planCount].wayPointAhead ~= path[nextPathIdx] then local norm1 = plan[planCount].biNormal:cross(plan[planCount].posOrig - plan[planCount-1].posOrig); norm1:normalize() local norm2 = plan[planCount].biNormal:cross(newNode.posOrig - plan[planCount].posOrig); norm2:normalize() plan[planCount].normal:setAdd2(norm1, norm2) local tmp = plan[planCount].normal:length() plan[planCount].normal:setScaled(1 / (tmp + 1e-30)) local chordLength = min(2 / tmp, (1 - norm1:dot(norm2) * 0.5) * tmp) -- 2 / tmp == cos(x/2) : x is angle between norm1, norm2 plan[planCount].halfWidth = plan[planCount].radiusOrig * chordLength end else plan[planCount].normal:setSub2(newNode.posOrig, plan[planCount].posOrig) plan[planCount].normal:setCross(plan[planCount].biNormal, plan[planCount].normal) plan[planCount].normal:normalize() end -- Calculate normal of node to be inserted into the plan -- This normal is calculated from the normals of the two path edges incident on it newNode.inEdgeNormal = vec3() if newNode.prevNodeInPath then -- TODO: why not check for newNode.prevNodePos? local wp1Pos, wp2Pos = mapData:getEdgePositions(newNode.prevNodeInPath, newNode.name) newNode.inEdgeNormal:setSub2(wp2Pos, wp1Pos) newNode.inEdgeNormal:setCross(newNode.biNormal, newNode.inEdgeNormal) newNode.inEdgeNormal:normalize() end newNode.outEdgeNormal = vec3() if newNode.nextNodeInPath then local wp1Pos, wp2Pos = mapData:getEdgePositions(newNode.name, newNode.nextNodeInPath) newNode.outEdgeNormal:setSub2(wp2Pos, wp1Pos) newNode.outEdgeNormal:setCross(newNode.biNormal, newNode.outEdgeNormal) newNode.outEdgeNormal:normalize() end newNode.normal = newNode.inEdgeNormal + newNode.outEdgeNormal local tmp = newNode.normal:length() newNode.normal:setScaled(1 / (tmp + 1e-30)) newNode.chordLength = min(2 / tmp, (1 - newNode.inEdgeNormal:dot(newNode.outEdgeNormal) * 0.5) * tmp) -- new node road width multiplier newNode.halfWidth = newNode.radiusOrig * newNode.chordLength local newNodeLaneLimLeft = 0 local newNodeLaneLimRight = 1 local newNodeRangeLeft = 0 local newNodeRangeRight = 1 local newNodeRangeLaneCount = 1 local rangeBestLane = 0 local bestLane local newNodeLaneConfig local newNodeRangeLeftIdx -- index of left most lane in range local newNodeRangeRightIdx -- index of right most lane in
Mark as private (unlisted)
for 30 minutes
for 6 hours
for 1 day
for 1 week
for 1 month
for 1 year
everlasting (like CCCP)