-- -- GroundResponse -- -- -- @author GIANTS Software -- @date 15/10/2016 -- -- Copyright (C) GIANTS Software GmbH, Confidential, All Rights Reserved. GroundResponse = {}; function GroundResponse.prerequisitesPresent(specializations) return true; end function GroundResponse:load(savegame) self.loadParticleSystemsForWheel = GroundResponse.loadParticleSystemsForWheel; self.updateWheelDirtPS = GroundResponse.updateWheelDirtPS; self.updateWheelSink = GroundResponse.updateWheelSink; self.getWheelCurrentSink = GroundResponse.getWheelCurrentSink; self.updateWheelTireFriction = Utils.overwrittenFunction(self.updateWheelTireFriction, GroundResponse.updateWheelTireFriction); self.applyRandomForceToWheels = GroundResponse.applyRandomForceToWheels; if g_currentMission.wheelDirt ~= nil and g_currentMission.wheelDirt.referenceShape ~= nil and g_currentMission.wheelDirt.referencePS["soilDry"] ~= nil and g_currentMission.wheelDirt.referencePS["soilWet"] ~= nil then if self.wheels ~= nil and table.getn(self.wheels) > 0 then local numWheels = table.getn(self.wheels); for iWheel=1,numWheels do local wheel = self.wheels[iWheel]; if wheel.hasParticles then local refNode = wheel.node; self:loadParticleSystemsForWheel(refNode, wheel) if wheel.additionalWheels ~= nil then for _,additionalWheel in pairs(wheel.additionalWheels) do self:loadParticleSystemsForWheel(refNode, additionalWheel); end end end end for iWheel=1,numWheels do local wheel1 = self.wheels[iWheel]; if wheel1.oppositeWheelIndex == nil then for jWheel=1,numWheels do if iWheel ~= jWheel then local wheel2 = self.wheels[jWheel]; if math.abs(wheel1.positionX + wheel2.positionX) < 0.1 and math.abs(wheel1.positionZ - wheel2.positionZ) < 0.1 and math.abs(wheel1.positionY - wheel2.positionY) < 0.1 then wheel1.oppositeWheelIndex = jWheel; wheel2.oppositeWheelIndex = iWheel; break; end end end end end end end self.dirtParticleSystemDirtyFlag = self:getNextDirtyFlag(); end function GroundResponse:postLoad(savegame) for _,wheel in pairs(self.wheels) do wheel.sink = 0; wheel.sinkTarget = 0; wheel.radiusOriginal = wheel.radius; wheel.sinkFrictionScaleFactor = 1; wheel.sinkLongStiffnessFactor = 1; wheel.sinkLatStiffnessFactor = 1; end end function GroundResponse:loadParticleSystemsForWheel(refNode, wheel) wheel.dirtPS = {}; local psEmitterShape = clone(g_currentMission.wheelDirt.referenceShape, false, false, false); link(refNode, psEmitterShape); local x,y,z; if wheel.wheelTire == nil then x,y,z = localToLocal(wheel.driveNode, refNode, 0, 0, 0); else x,y,z = localToLocal(wheel.wheelTire, refNode, 0, 0, 0); end setTranslation(psEmitterShape, x+wheel.xOffset,y,z); setRotation(psEmitterShape, 0, 0, 0); setScale(psEmitterShape, 2*wheel.width, 2*wheel.radius, 2*wheel.radius); for _,name in pairs( {"soilDry", "soilWet"} ) do if g_currentMission.wheelDirt.referencePS[name] ~= nil then wheel.dirtPS[name] = {}; local psClone = clone(g_currentMission.wheelDirt.referencePS[name].shape, true, false, true); ParticleUtil.loadParticleSystemFromNode(psClone, wheel.dirtPS[name], false, g_currentMission.wheelDirt.referencePS[name].worldSpace, g_currentMission.wheelDirt.referencePS[name].forceFullLifespan); ParticleUtil.setEmitterShape(wheel.dirtPS[name], psEmitterShape); wheel.dirtPS[name].isActive = false; wheel.dirtPS[name].isActiveSend = false; wheel.dirtPS[name].particleSpeed = ParticleUtil.getParticleSystemSpeed(wheel.dirtPS[name]); wheel.dirtPS[name].particleRandomSpeed = ParticleUtil.getParticleSystemSpeedRandom(wheel.dirtPS[name]); --wheel.dirtPS[name].particleNormalSpeed = ParticleUtil.getParticleSystemNormalSpeed(wheel.dirtPS[name]); --wheel.dirtPS[name].particleTangentSpeed = ParticleUtil.getParticleSystemTangentSpeed(wheel.dirtPS[name]); end end end function GroundResponse:delete() for _,wheel in pairs(self.wheels) do ParticleUtil.deleteParticleSystems(wheel.dirtPS); if wheel.additionalWheels ~= nil then for _,additionalWheel in pairs(wheel.additionalWheels) do ParticleUtil.deleteParticleSystems(additionalWheel.dirtPS); end end end end function GroundResponse:mouseEvent(posX, posY, isDown, isUp, button) end function GroundResponse:keyEvent(unicode, sym, modifier, isDown) end function GroundResponse:readUpdateStream(streamId, timestamp, connection) if connection:getIsServer() then local dirty = streamReadBool(streamId); if dirty then for _,wheel in pairs(self.wheels) do wheel.dirtParticlesActive = streamReadBool(streamId); end end end end function GroundResponse:writeUpdateStream(streamId, connection, dirtyMask) if not connection:getIsServer() then if streamWriteBool(streamId, bitAND(dirtyMask, self.dirtParticleSystemDirtyFlag) ~= 0) then for _,wheel in pairs(self.wheels) do streamWriteBool(streamId, wheel.dirtParticlesActive); end end end end function GroundResponse:update(dt) if self.isServer then if self:getIsActive() then self:updateWheelSink(dt); self:applyRandomForceToWheels(dt); end end end function GroundResponse:updateTick(dt) if self.isClient then for _,wheel in pairs(self.wheels) do if wheel.dirtPS ~= nil then if wheel.netInfo.xDriveLast == nil then wheel.netInfo.xDriveLast = wheel.netInfo.xDrive; end local xDriveDiff = wheel.netInfo.xDrive - wheel.netInfo.xDriveLast; if xDriveDiff > math.pi then wheel.netInfo.xDriveLast = wheel.netInfo.xDriveLast + 2*math.pi; elseif xDriveDiff < -math.pi then wheel.netInfo.xDriveLast = wheel.netInfo.xDriveLast - 2*math.pi; end xDriveDiff = wheel.netInfo.xDrive - wheel.netInfo.xDriveLast; wheel.netInfo.xDriveLast = wheel.netInfo.xDrive; local wheelRotSpeed = math.deg(xDriveDiff) / (0.001 * dt); local maxWheelRotSpeed = 1080; local wheelRotFactor = math.abs(wheelRotSpeed) / maxWheelRotSpeed; wheelRotFactor = wheelRotFactor * wheel.radius; self:updateWheelDirtPS(wheel, wheelRotFactor, wheel.steeringAngle); if wheel.additionalWheels ~= nil then for _,additionalWheel in pairs(wheel.additionalWheels) do self:updateWheelDirtPS(additionalWheel, wheelRotFactor, wheel.steeringAngle); end end end end end end function GroundResponse:updateWheelDirtPS(wheel, wheelRotFactor, steeringAngle) if wheel.dirtPS ~= nil then local sizeScale = 2 * wheel.width * wheel.radius; for _,ps in pairs(wheel.dirtPS) do ParticleUtil.setEmittingState(ps, ps.isActive); if ps.isActive then -- emit count local speedEmitScale = wheelRotFactor * sizeScale; local speedEmitScaleWet = math.pow( 2*wheelRotFactor*sizeScale*g_currentMission.environment.groundWetness, 2 ); local emitScale = 0.5 * (speedEmitScale + speedEmitScaleWet); ParticleUtil.setEmitCountScale(ps, emitScale); -- speeds local speedFactor = 0.3 * wheelRotFactor; local speed = ps.particleSpeed * speedFactor; speed = math.min(speed, 0.001); ParticleUtil.setParticleSystemSpeed(ps, speed); --ParticleUtil.setParticleSystemNormalSpeed(ps, 1.0 ); --ParticleUtil.getParticleSystemTangentSpeed(ps, 0.4 ); ParticleUtil.setParticleSystemSpeedRandom(ps, ps.particleRandomSpeed * speedFactor); -- adjust position local dirSign = 1; if self.movingDirection < 0 then dirSign = -1; end local x,y,z; if wheel.wheelTire == nil then x,y,z = localToLocal(wheel.driveNode, getParent(ps.emitterShape), wheel.xOffset, 0, 0); else x,y,z = localToLocal(wheel.wheelTire, getParent(ps.emitterShape), 0, 0, 0); end setTranslation(ps.emitterShape, x,y,z); if self.movingDirection < 0 then setRotation(ps.emitterShape, 0,math.pi+steeringAngle,0); else setRotation(ps.emitterShape, 0,steeringAngle,0); end end end end end function GroundResponse:draw() end function GroundResponse:updateWheelSink(dt) local speed = self:getLastSpeed(); local detailId = g_currentMission.terrainDetailId; for _,wheel in pairs(self.wheels) do local width = 0.25 * wheel.width; local length = 0.25 * wheel.width; local x0,y0,z0; local x1,y1,z1; local x2,y2,z2; if wheel.repr == wheel.driveNode then x0,y0,z0 = localToWorld(wheel.node, wheel.positionX + width, wheel.positionY, wheel.positionZ - length); x1,y1,z1 = localToWorld(wheel.node, wheel.positionX - width, wheel.positionY, wheel.positionZ - length); x2,y2,z2 = localToWorld(wheel.node, wheel.positionX + width, wheel.positionY, wheel.positionZ + length); else local x,_,z = localToLocal(wheel.driveNode, wheel.repr, 0,0,0); x0,y0,z0 = localToWorld(wheel.repr, x + width, 0, z - length); x1,y1,z1 = localToWorld(wheel.repr, x - width, 0, z - length); x2,y2,z2 = localToWorld(wheel.repr, x + width, 0, z + length); end local x,z, widthX,widthZ, heightX,heightZ = Utils.getXZWidthAndHeight(nil, x0,z0, x1,z1, x2,z2); setDensityCompareParams(detailId, "greater", 0); local density, area, _ = getDensityParallelogram(detailId, x,z, widthX,widthZ, heightX,heightZ, g_currentMission.terrainDetailTypeFirstChannel, g_currentMission.terrainDetailTypeNumChannels); setDensityCompareParams(detailId, "greater", -1); local terrainValue = 0; if area > 0 then terrainValue = math.floor(density/area + 0.5); end local minVal, maxVal = 0, 1; local sinkSpeed = (dt/1000) * (speed/10) * (0.01 * math.random(120)); local maxSink = math.min(0.3, wheel.radius*0.7 * (0.8 + 0.5*g_currentMission.environment.groundWetness)) * 1.0; if terrainValue == 1 then -- cultivator_gegrubberter Boden minVal, maxVal = 180, 180; sinkSpeed = sinkSpeed * 0.5; elseif terrainValue == 2 then -- plough_gepflügter Boden minVal, maxVal = 100, 100; sinkSpeed = sinkSpeed * 0.6; elseif terrainValue == 3 then -- sowing_gesähter Boden wie Weizen_Gerste_Raps minVal, maxVal = 110, 110; sinkSpeed = sinkSpeed * 0.3; elseif terrainValue == 4 then -- sowingWidth_gedrillter Boden wie Mais_Sonnenblumen minVal, maxVal = 120, 120; sinkSpeed = sinkSpeed * 0.4; elseif terrainValue == 5 then -- grass_Gras minVal, maxVal = 20, 30; sinkSpeed = sinkSpeed * 0.5; end local ploughEffect = false; if area == 0 then wheel.sinkTarget = 0; else if wheel.sink == wheel.sinkTarget or wheel.lastTerrainValue ~= terrainValue then local currentSink = self:getWheelCurrentSink(wheel); wheel.sinkTarget = math.min(maxSink, 0.01 * math.random(minVal, maxVal) * currentSink); -- 'ploughing effect' if terrainValue == 2 and wheel.oppositeWheelIndex ~= nil then local oppositeWheel = self.wheels[wheel.oppositeWheelIndex]; if oppositeWheel.lastTerrainValue ~= nil and oppositeWheel.lastTerrainValue ~= 2 then wheel.sinkTarget = wheel.sinkTarget * 1.5; ploughEffect = true; end end end end if wheel.sinkTarget ~= wheel.sink then local sinkDelta = maxSink * sinkSpeed; if terrainValue ~= wheel.lastTerrainValue then sinkDelta = maxSink * (dt/250); end if wheel.sink < wheel.sinkTarget then wheel.sink = math.min(wheel.sinkTarget, wheel.sink + sinkDelta); elseif wheel.sink > wheel.sinkTarget then wheel.sink = math.max(wheel.sinkTarget, wheel.sink - sinkDelta); end wheel.radius = wheel.radiusOriginal - wheel.sink; self:updateWheelBase(wheel); local sinkFactor = (wheel.sink/maxSink) * (0.4 + 0.6*g_currentMission.environment.groundWetness); wheel.sinkFrictionScaleFactor = (1.0 - math.min(0.7, sinkFactor)); wheel.sinkLongStiffnessFactor = (1.0 - math.min(0.2, sinkFactor)); wheel.sinkLatStiffnessFactor = (1.0 - math.min(0.0, sinkFactor)); self:updateWheelTireFriction(wheel); end wheel.lastTerrainValue = terrainValue; if wheel.dirtPS ~= nil then local enableSoilPS = false; if terrainValue == 1 then -- cultivator enableSoilPS = true; elseif terrainValue == 2 then -- plough enableSoilPS = true; elseif terrainValue == 3 then -- sowing enableSoilPS = true; elseif terrainValue == 4 then -- sowingWidth enableSoilPS = true; elseif terrainValue == 5 then -- grass enableSoilPS = false; end for _,ps in pairs(wheel.dirtPS) do ps.isActive = false; end if enableSoilPS and (speed > 1) and wheel.sink > 0 then if g_currentMission.environment.groundWetness > 0.2 then if wheel.dirtPS["soilWet"] ~= nil then wheel.dirtPS["soilWet"].isActive = true; end else if wheel.dirtPS["soilDry"] ~= nil then wheel.dirtPS["soilDry"].isActive = true; end end end if wheel.additionalWheels ~= nil then for _,additionalWheel in pairs(wheel.additionalWheels) do for _,ps in pairs(additionalWheel.dirtPS) do ps.isActive = enableSoilPS and (speed > 1) and wheel.sink > 0; end end end end end end function GroundResponse:getWheelCurrentSink(wheel) if wheel.maxDeformation == nil or wheel.wheelTire == nil then return 0; end local gravity = 9.81; local tireLoad = getWheelShapeContactForce(wheel.node, wheel.wheelShape); if tireLoad ~= nil then local nx,ny,nz = getWheelShapeContactNormal(wheel.node, wheel.wheelShape); local dx,dy,dz = localDirectionToWorld(wheel.node, 0,-1,0); tireLoad = -tireLoad*Utils.dotProduct(dx,dy,dz, nx,ny,nz); tireLoad = tireLoad + math.max(ny*gravity, 0.0) * wheel.mass; -- add gravity force of tire else tireLoad = 0; end tireLoad = tireLoad / gravity; local loadFactor = (tireLoad / wheel.restLoad); local totalWidth = wheel.width; if wheel.additionalWheels ~= nil then for _,additionalWheel in pairs(wheel.additionalWheels) do totalWidth = totalWidth + additionalWheel.width; end end local length = 2 * math.pi * (wheel.radius *1.5) * (20/360); local area = totalWidth * length; local loadFactor = math.min(10, loadFactor * 0.4 * (1.0/area) * (0.5 + (0.5*g_currentMission.environment.groundWetness))); maxSink = loadFactor * wheel.maxDeformation; return maxSink; end function GroundResponse:updateWheelTireFriction(superFunc, wheel) if self.isServer and self.isAddedToPhysics then setWheelShapeTireFriction(wheel.node, wheel.wheelShape, wheel.sinkFrictionScaleFactor*wheel.maxLongStiffness, wheel.sinkLatStiffnessFactor*wheel.maxLatStiffness, wheel.maxLatStiffnessLoad, wheel.sinkFrictionScaleFactor*wheel.frictionScale*wheel.tireGroundFrictionCoeff); end; end function GroundResponse:applyRandomForceToWheels(dt) local numWheels = table.getn(self.wheels); if numWheels == 0 or not self:getIsActive() then return; end local speed = self:getLastSpeed(); local forceFactor = 0.001; local totalMass = 0; for _,component in pairs(self.components) do totalMass = totalMass + getMass(component.node); end for i=1, numWheels do local wheel = self.wheels[i]; if speed > 1 then if wheel.nextRandomHitTime == nil then local hitDistance = math.random(1, numWheels) * 0.5; local speedMS = speed / 3.6; local timeDelta = math.min(1000, (1000 * hitDistance / speedMS)); wheel.nextRandomHitTime = g_currentMission.time + timeDelta; end end if wheel.nextRandomHitTime ~= nil and wheel.nextRandomHitTime < g_currentMission.time then if wheel.hasGroundContact and wheel.contact == Vehicle.WHEEL_GROUND_CONTACT and wheel.forcePercentage == nil then local r1, r2 = Utils.getNormallyDistributedRandomVariables(5, 5*0.333); r1 = math.max(0.0001, r1); r2 = math.max(1, r2); -- linear influence of speed results in too intense hits --local force = forceFactor * totalMass * speed * r1; -- square root is better local force = forceFactor * totalMass * math.sqrt(speed) * r1 * 0.2; local maxForce = forceFactor * (totalMass * 0.5); force = math.min(force, maxForce); wheel.timeTillFullForce = r2 * 50; -- max. ~500ms wheel.forcePercentage = 0; wheel.currentForce = force; end end if wheel.currentForce ~= nil then local delta = (dt/wheel.timeTillFullForce) * 0.01 * math.random(20); if speed > 1 and not wheel.maxForceReached then wheel.forcePercentage = math.min(1, wheel.forcePercentage + delta); else wheel.forcePercentage = math.max(0, wheel.forcePercentage - delta); end local currentForce = wheel.forcePercentage * wheel.currentForce; local dx,dy,dz = 0,-currentForce,0; --local dx,dy,dz = localDirectionToWorld(wheel.node, 0,-currentForce,0); local px,py,pz = localToLocal(wheel.driveNode, wheel.node, 0,0,0); addImpulse(wheel.node, dx,dy,dz, px,py,pz, true); --addForce(wheel.node, dx,dy,dz, px,py,pz, true); if wheel.forcePercentage == 1 then wheel.maxForceReached = true; end if wheel.maxForceReached and wheel.forcePercentage == 0 then wheel.forcePercentage = nil; wheel.currentForce = nil; wheel.nextRandomHitTime = nil; wheel.maxForceReached = false end end end end