Vehicles/VehicleCamera.lua

This is the camera used by VehicleSeat. VehicleCamera can be used both for interior and exterior cameras, depending on which arguments are specified.

  26  
  27  VehicleCamera                       = VehicleCamera or {};
  28  local VehicleCameraClass            = Class(VehicleCamera);

VehicleCamera:load(cameraId, rotationNode, zoomNode, isCarrier, parentId)

Loads a newly created instance using one or three arguments:

  • cameraId (int): id of the camera. This is the only mandatory argument
  • rotationNode (int): if this is an exterior camera, this argument shall contain the id of the transform which represents the rotation. Typically, this will be the parent of zoomNode and cameraId.
  • zoomNode (int): if this is an exterior camera, this argument shall contain the id of the transform which is moved while zooming. Typically, this will be either the camera id or its parent.
  • isCarrier (bool): true when the camera's physics shall be ignored because the camera is mounted on a ropeway carrier
  • parentId (int): the id of the parent, of which all collisions will be ignored for raycasts.
  37  function VehicleCamera:load(cameraId, rotationNode, zoomNode, isCarrier, parentId)
  38      self.cameraId                   = cameraId;
  39      self.trueCameraId               = cameraId; -- used if the object the rotation is applied to is not the same as the camera itself
  40      self.rotationNode               = rotationNode;
  41      
  42      self.zoomLevel                  = 1;
  43      self.zoomFOV                    = GameplaySettings.fieldOfView;
  44      self.cameraFrozen               = false;
  45      self.blockInput                 = false;
  46      self.parentId                   = parentId or 0;
  47      
  48      self.rightMouseDownDuration     = 0;
  49      
  50      self.cx                         = 0;
  51      self.cy                         = 0;
  52      
  53      self.minRotX                    = -85;
  54      self.maxRotX                    = 85;
  55      
  56      if self.rotationNode == nil then
  57          local x,y,z                 = getRotation(cameraId);
  58          self.cx                     = x;
  59          self.cy                     = y;
  60          
  61          self.enableCameraDynamics   = not isCarrier;
  62          
  63      else
  64          self.zoomNode               = zoomNode;
  65          self.zoomTarget             = 6;
  66          self.lastZoomPosition       = 6;
  67          self.raycastIgnoreDistance  = 1;
  68  
  69          self.cx                     = 20;
  70  
  71          -- cx2/cy2 are smoothing rotation one more time and are then applied to the camera
  72          self.cx2                    = 0;
  73          self.cy2                    = 0;
  74  
  75          self.worldReference         = not isCarrier;
  76      end;
  77  
  78      if self.worldReference then
  79          -- create a new game object which holds the position of our rotation node
  80          self.vehicleReferenceId     = createGameObject("cameraReference");
  81  
  82          -- copy its position
  83          setParent(self.vehicleReferenceId,          getParent(self.rotationNode));
  84          setWorldPosition(self.vehicleReferenceId,   getWorldPosition(self.rotationNode));
  85          setRotation(self.vehicleReferenceId,        0,0,0);
  86  
  87          -- create a new game object for the camera
  88          self.worldRotationNode      = createGameObject("worldCameraRotation");
  89          setWorldPosition(self.worldRotationNode,    getWorldPosition(self.vehicleReferenceId));
  90          setWorldRotation(self.worldRotationNode,    getWorldRotation(self.vehicleReferenceId));
  91          
  92          setParent(self.worldRotationNode,           0);
  93          setParent(self.rotationNode,                self.worldRotationNode);
  94          -- now we free the rotation node so that it is in world space
  95  
  96          self.lastCameraWorldPosition    = VectorUtils.getWorldPosition(self.vehicleReferenceId);
  97          self.lastReferenceForwardVector = VectorUtils.transformDirection(self.vehicleReferenceId, Vector3.forward);
  98  
  99          if math.abs(self.lastReferenceForwardVector.x) + math.abs(self.lastReferenceForwardVector.z) < 0.1 then
 100              self.lastReferenceForwardVector = Vector3.forward:clone();
 101          end;
 102  
 103      elseif self.enableCameraDynamics then
 104  
 105          -- again, create a new game object
 106          self.cameraDynamicsRoot         = createGameObject("CameraDynamicsRoot");
 107          self.cameraDynamicsRef          = createGameObject("CameraDynamicsReference");
 108  
 109          -- make it a child of the actual camera id
 110          setParent(self.cameraDynamicsRoot,          getParent(self.cameraId));
 111          setWorldPosition(self.cameraDynamicsRoot,   getWorldPosition(self.cameraId));
 112          setRotation(self.cameraDynamicsRoot,        0,0,0);
 113          
 114          setParent(self.cameraDynamicsRef,           getParent(self.cameraId));
 115          setWorldPosition(self.cameraDynamicsRef,    getWorldPosition(self.cameraId));
 116          setRotation(self.cameraDynamicsRef,         0,0,0);
 117  
 118          setParent(self.cameraDynamicsRoot,          self.cameraDynamicsRef);
 119          setParent(self.cameraId,                    self.cameraDynamicsRoot);
 120  
 121          -- store current speed
 122          self.lastSpeedVector            = Vector3.zero:clone();
 123          self.lastCameraWorldPosition    = Vector3.zero:clone();
 124          self.lastAcceleration           = Vector3:new(0, 0, 0);
 125          self.lastCameraPos              = Vector3:new(0, 0, 0);
 126      end;
 127      
 128      self.enableRotX                 = true;
 129      self.enableRotY                 = true;
 130      self.enableSelector             = true;
 131  end;

VehicleCamera:activate()

Activates this camera.

 135  function VehicleCamera:activate()
 136      if not self.dontActivateCamera then
 137          GameControl.setCamera(self.trueCameraId);
 138      end;
 139      self.cameraFrozen               = false;
 140      self.blockInput                 = false;
 141  
 142      -- set the global "activeCamera" to this instance
 143      -- note that this has to be resetted every time another camera is activated
 144      VehicleCamera.activeCamera      = self;
 145  
 146      self:resetWorldReferenceMovement();
 147  end;

VehicleCamera:saveToTable()

Stores all current camera settings in the savegame.

 150  function VehicleCamera:saveToTable()
 151      return {
 152          zoomLevel                   = self.zoomLevel,
 153          cx                          = self.cx,
 154          cy                          = self.cy,
 155          isActive                    = VehicleCamera.activeCamera == self,
 156      }
 157  end;

VehicleCamera:loadFromTable(tbl)

Restores variables from savegame.

 160  function VehicleCamera:loadFromTable(tbl)
 161      if tbl == nil then return end;
 162      
 163      self.zoomLevel                  = tbl.zoomLevel;
 164      self.cx, self.cy                = tbl.cx, tbl.cy;
 165  
 166      -- directly apply rotation
 167      if self.rotationNode ~= nil then
 168          setRotation(self.rotationNode, self.cx, self.cy, 0);
 169      else
 170          setRotation(self.cameraId, self.cx, self.cy, 0);
 171      end;
 172  
 173      if tbl.isActive then
 174          self:activate();
 175      end;
 176  end;

VehicleCamera:getRotateFactor()

Returns the active rotate factor, depending on zoom level.

 180  function VehicleCamera:getRotateFactor()
 181      if self.rotationNode ~= nil then
 182          -- if this is an exterior camera, the behaviour has to be inversed
 183          return GameplaySettings.mouseSensitivity * 0.5;
 184      end;
 185      return GameplaySettings.mouseSensitivity * lerp(0.2, 1, self.zoomLevel);
 186  end;

VehicleCamera:getCameraFrozen()

Returns whether the camera is currently frozen or not.

 190  function VehicleCamera:getCameraFrozen()
 191      return self.cameraFrozen or (Input.getMouseButton(0) and self.enableSelector);
 192  end;

VehicleCamera:setIsFrozen(isFrozen)

Sets the camera to frozen or unfreezes it, depending on isFrozen (bool).

 195  function VehicleCamera:setIsFrozen(isFrozen)
 196      self.cameraFrozen               = isFrozen;
 197  
 198      GUI:wrapMouse(not self.cameraFrozen);
 199  end;

VehicleCamera:setBlockInput(block)

Specifies whether input shall be blocked or not. This is in use if any MovingParts controls are activated.

 202  function VehicleCamera:setBlockInput(block)
 203      self.blockInput                 = block;
 204      
 205      if block then
 206          -- hide mouse
 207          GUI:wrapMouse(true);
 208      else
 209          GUI:wrapMouse(not self.cameraFrozen);
 210      end;
 211  end;

VehicleCamera:update(dt)

Called every frame to update camera inputs (like rotating or zooming).

If exterior camera is enabled, it will shoot a ray cast to check how much it is allowed to zoom out

 216  function VehicleCamera:update(dt)
 217      -- vehicle cameras may only be updated when they are actually in use
 218  
 219      if Input.getMouseButton(1) then
 220          self.rightMouseDownDuration     = self.rightMouseDownDuration + dt;
 221  
 222          -- as long as right mouse button is down, we can return right here
 223          return;
 224  
 225      elseif self.rightMouseDownDuration > 0 then
 226  
 227          if self.rightMouseDownDuration < 0.2 and not self.blockInput then
 228              self:setIsFrozen(not self.cameraFrozen);
 229          end;
 230  
 231          self.rightMouseDownDuration     = 0;
 232      end;
 233  
 234      if self.blockInput then return end;
 235      
 236      if not GUI:getAnyGuiActive() then
 237          self.zoomLevel              = clamp01(self.zoomLevel - InputMapper:getAxis(InputMapper.Axis_Look_Zoom) * GameplaySettings.zoomSensitivity);
 238  
 239          local rotXLimit             = 0;
 240          
 241          -- apply zoom level
 242          if self.zoomNode ~= nil then
 243              self.zoomTarget         = lerp(6, 40, self.zoomLevel);
 244              
 245              -- find the true minimum distance using raycast
 246              local dist              = self.zoomTarget;
 247  
 248              local limitDistance;
 249              limitDistance, rotXLimit    = self:getZoomLimits(self.zoomTarget);
 250  
 251              -- smoothen out zooming
 252              dist                    = 0.2 * dist + 0.8 * self.lastZoomPosition;
 253              
 254              if limitDistance ~= nil then
 255                  dist                = math.min(dist, limitDistance);
 256              end;
 257  
 258              setPosition(self.zoomNode, 0,0,-dist);
 259              GameControl.setCameraFOV(self.trueCameraId, GameplaySettings.fieldOfView);
 260  
 261              self.lastZoomPosition   = dist;
 262              
 263          else
 264              local targetFOV         = lerp(20, GameplaySettings.fieldOfView, self.zoomLevel);
 265              self.zoomFOV            = 0.5 * (self.zoomFOV + targetFOV);
 266              GameControl.setCameraFOV(self.trueCameraId, self.zoomFOV);
 267          end;
 268          
 269          if not self:getCameraFrozen() then
 270              local movX              = InputMapper:getAxis(InputMapper.Axis_Look_LeftRight);
 271              local movY              = InputMapper:getAxis(InputMapper.Axis_Look_UpDown);
 272              
 273              local rotateFactor      = self:getRotateFactor();
 274              
 275              if self.enableRotX then
 276  
 277                  -- apply rot X limit
 278                  if rotXLimit > 0 then
 279                      movY            = math.min(movY, 0);
 280                  elseif rotXLimit < 0 then
 281                      movY            = math.max(movY, 0);
 282                  end;
 283  
 284                  self.cx             = clamp(self.cx - rotateFactor * movY, self.minRotX, self.maxRotX);
 285              end;
 286              if self.enableRotY then
 287                  self.cy             = self.cy + rotateFactor * movX;
 288              end;
 289  
 290              if self.rotationNode ~= nil then
 291                  -- smooth rotation
 292                  self.cx2            = 0.40 * self.cx2 + 0.60 * self.cx;
 293                  self.cy2            = 0.40 * self.cy2 + 0.60 * self.cy;
 294  
 295                  setRotation(self.rotationNode, self.cx2, self.cy2, 0);
 296              else
 297                  setRotation(self.cameraId, self.cx, self.cy, 0);
 298              end;
 299          end;
 300      end;
 301  end;

VehicleCamera:fixedUpdate(dt)

fixedUpdate is used for the physics-dependent camera simulation of both exterior cameras (using worldReference) and interior cameras (using enableCameraDynamics).

sqrt is a local helper function which allows a signed use of the math.sqrt function.

 307  
 308  local sqrt = function(x)
 309      if x < 0 then
 310          return -math.sqrt(-x);
 311      end;
 312      return x;
 313  end;
 314  
 315  function VehicleCamera:fixedUpdate(dt)
 316      if self.worldReference then
 317  
 318          -- get the camera's world position
 319          local wx,wy,wz                  = getWorldPosition(self.vehicleReferenceId);
 320          local nx,ny,nz;
 321      
 322          -- ignore a movement of more than 15 metres
 323          if (math.abs(self.lastCameraWorldPosition.x - wx) + math.abs(self.lastCameraWorldPosition.y - wy) + math.abs(self.lastCameraWorldPosition.z - wz)) > 15 then
 324              self:resetWorldReferenceMovement();
 325          end;
 326          
 327          nx                              = 0.5 * self.lastCameraWorldPosition.x  + 0.5 * wx;
 328          ny                              = 0.5 * self.lastCameraWorldPosition.y  + 0.5 * wy;
 329          nz                              = 0.5 * self.lastCameraWorldPosition.z  + 0.5 * wz;
 330          
 331          -- apply the position
 332          setWorldPosition(self.worldRotationNode, nx,ny,nz);
 333          self.lastCameraWorldPosition.x  = nx;
 334          self.lastCameraWorldPosition.y  = ny;
 335          self.lastCameraWorldPosition.z  = nz;
 336      
 337          local dx,dy,dz                  = Utils.transformDirection(self.vehicleReferenceId, 0,0,1);
 338          --local dx,dy,dz                        = wx-nx, wy-ny, wz-nz;
 339      
 340          if math.abs(dx) + math.abs(dz) > 0.1 then
 341      
 342              local lx, ly, lz            = Utils.inverseTransformPoint(self.vehicleReferenceId, nx,ny,nz);
 343              
 344              -- invert the term (wx-nx) for reversing for better camera feeling
 345              local revFactor             = 0.3;
 346              if lz > 0 then
 347                  revFactor               = clamp(5*lz, 0.3, 1);
 348              end;
 349      
 350              -- smoothly blend the direction
 351              nx                                  = 0.70 * self.lastReferenceForwardVector.x  + 0.30 * lerp(wx-nx, dx, revFactor);
 352              nz                                  = 0.70 * self.lastReferenceForwardVector.z  + 0.30 * lerp(wz-nz, dz, revFactor);
 353              
 354              self.lastReferenceForwardVector.x   = nx;
 355              self.lastReferenceForwardVector.z   = nz;
 356      
 357              -- make sure LookRotation does never receive a null vector
 358              if math.abs(nx) + math.abs(nz) > 1e-3 then
 359                  local rx,ry,rz                  = Utils.lookRotation(nx,0,nz, 0,1,0);
 360                  setWorldRotation(self.worldRotationNode, 0,ry,0);
 361              end;
 362          end;
 363  
 364      elseif self.enableCameraDynamics then
 365          local wx,wy,wz                  = getWorldPosition(self.cameraDynamicsRef);
 366  
 367          if (math.abs(self.lastCameraWorldPosition.x - wx) + math.abs(self.lastCameraWorldPosition.y - wy) + math.abs(self.lastCameraWorldPosition.z - wz)) > 15 then
 368              self:resetWorldReferenceMovement();
 369          end;
 370  
 371          -- determine speed
 372          local sx,sy,sz;
 373          sx                              = 0.2 * (wx - self.lastCameraWorldPosition.x) / dt  + 0.8 * self.lastSpeedVector.x;
 374          sy                              = 0.2 * (wy - self.lastCameraWorldPosition.y) / dt  + 0.8 * self.lastSpeedVector.y;
 375          sz                              = 0.2 * (wz - self.lastCameraWorldPosition.z) / dt  + 0.8 * self.lastSpeedVector.z;
 376  
 377          -- determine acceleration
 378          local ax,ay,az;
 379          ax                              = 0.02 * (sx - self.lastSpeedVector.x) / dt + 0.98 * self.lastAcceleration.x;
 380          ay                              = 0.02 * (sy - self.lastSpeedVector.y) / dt + 0.98 * self.lastAcceleration.y;
 381          az                              = 0.02 * (sz - self.lastSpeedVector.z) / dt + 0.98 * self.lastAcceleration.z;
 382  
 383          local b                         = -0.01;
 384          local c                         = -0.15;
 385  
 386          -- apply acceleration (multiplied with coefficients b and c)
 387          local x,y,z                     = sqrt(sx)*b + ax*c, sqrt(sy)*b + ay*c, sqrt(sz)*b + az*c;
 388          x, y, z                         = Utils.inverseTransformDirection(self.cameraDynamicsRef, x,y,z);
 389  
 390          -- one more time, dampen it for smooth results
 391          x                               = 0.8 * self.lastCameraPos.x + 0.2 * x;
 392          y                               = 0.8 * self.lastCameraPos.y + 0.2 * y;
 393          z                               = 0.8 * self.lastCameraPos.z + 0.2 * z;
 394  
 395          self.lastCameraPos.x            = x;
 396          self.lastCameraPos.y            = y;
 397          self.lastCameraPos.z            = z;
 398  
 399          setPosition(self.cameraDynamicsRoot, x,y,z);
 400  
 401          -- store values for next frame
 402          self.lastCameraWorldPosition.x  = wx;
 403          self.lastCameraWorldPosition.y  = wy;
 404          self.lastCameraWorldPosition.z  = wz;
 405          self.lastSpeedVector.x          = sx;
 406          self.lastSpeedVector.y          = sy;
 407          self.lastSpeedVector.z          = sz;
 408  
 409      end;
 410  end;

VehicleCamera:resetWorldReferenceMovement()

This is internally called if the vehicle is teleported, which causes physics-dependent cameras to yield fake results. Therefore, their calculation is reset.

 414  function VehicleCamera:resetWorldReferenceMovement()
 415      if self.worldReference then
 416  
 417          -- we need to update the position
 418          self.lastCameraWorldPosition.x, self.lastCameraWorldPosition.y, self.lastCameraWorldPosition.z = getWorldPosition(self.vehicleReferenceId);
 419  
 420          -- and the direction
 421          local dx,dy,dz              = Utils.transformDirection(self.vehicleReferenceId, 0,0,1);
 422          if math.abs(dx) + math.abs(dz) > 0.1 then
 423              self.lastReferenceForwardVector.x   = dx;
 424              self.lastReferenceForwardVector.y   = dy;
 425              self.lastReferenceForwardVector.z   = dz;
 426              -- otherwise the reference stays unchanged, because we can't come up with a better one
 427          end;
 428      
 429      elseif self.enableCameraDynamics then
 430  
 431          self.lastSpeedVector.x          = 0;
 432          self.lastSpeedVector.y          = 0;
 433          self.lastSpeedVector.z          = 0;
 434          self.lastAcceleration.x         = 0;
 435          self.lastAcceleration.y         = 0;
 436          self.lastAcceleration.z         = 0;
 437          self.lastCameraPos.x            = 0;
 438          self.lastCameraPos.y            = 0;
 439          self.lastCameraPos.z            = 0;
 440  
 441          local wx, wy, wz                = getWorldPosition(self.cameraId);
 442  
 443          self.lastCameraWorldPosition.x  = wx;
 444          self.lastCameraWorldPosition.y  = wy;
 445          self.lastCameraWorldPosition.z  = wz;
 446  
 447          setPosition(self.cameraDynamicsRoot, 0,0,0);
 448      end;
 449  end;

VehicleCamera:getZoomLimits(dist)

Returns the zoom limits for the current camera pose, as determined by some raycasts.

 453  function VehicleCamera:getZoomLimits(dist)
 454  
 455      local rayStart          = VectorUtils.getWorldPosition(self.rotationNode);
 456      local rayDirection      = VectorUtils.transformDirection(self.zoomNode, Vector3:new(0,0,-1));
 457      rayStart                = rayStart + rayDirection:multiply(self.raycastIgnoreDistance);
 458  
 459      local distOffset        = self.raycastIgnoreDistance;
 460  
 461      -- there's a max iteration count
 462      for i=1, 32, 1 do
 463  
 464          local hit, normal, raycastDistance, otherId = VectorUtils.raycastGetAll(rayStart, rayDirection, dist + 10 - distOffset);
 465  
 466          -- no single hit
 467          if not hit then
 468              return nil, 0;
 469          end;
 470  
 471          -- there's a hit, check if its relevant
 472          if self.parentId == 0 or not isChildOf(otherId, self.parentId) then
 473              -- we've found what we've been looking for
 474              local returnDist    = distOffset + raycastDistance;
 475  
 476              -- project the normal onto our raycast vector and reduce the returnDist if the surface is almost perpendicular to it
 477              local projectedLen  = rayDirection:dotProduct(normal);
 478  
 479              returnDist          = returnDist - 1.5 * (1-projectedLen*projectedLen);
 480  
 481              -- convert the normal into local space of our rotation node
 482              local localNormal   = VectorUtils.inverseTransformDirection(self.rotationNode, normal);
 483  
 484              -- we want to limit rot x, therefore we are interested in the y component
 485              if math.abs(localNormal.y) > 0.5 then
 486                  -- the normal points up/downwards => don't allow the camera to go down/up
 487                  return returnDist, localNormal.y;
 488              end;
 489  
 490              return returnDist, 0;
 491          end;
 492  
 493          -- prepare for next iteration
 494          distOffset          = distOffset + raycastDistance + 0.1;
 495          rayStart            = rayStart + rayDirection:multiply(raycastDistance + 0.1);
 496  
 497      end;
 498  
 499      -- number of iterations exceeded => nothing found
 500      return nil, 0;
 501  end;

All contents of this page may be used for modding use for Winter Resort Simulator only. Any use exceeding this regulation is not permitted.

Copyright (C) HR Innoways, 2020. All Rights Reserved.