63 | function VehicleCamera:loadFromXML(xmlFile, key) |
64 | local camIndexStr = getXMLString(xmlFile, key .. "#index"); |
65 | self.cameraNode = Utils.indexToObject(self.vehicle.components, camIndexStr); |
66 | if self.cameraNode == nil or not getHasClassId(self.cameraNode, ClassIds.CAMERA) then |
67 | print("Error loading camera "..key..": invalid camera node"); |
68 | return false; |
69 | end; |
70 | |
71 | self.fovy = calculateFovy(self.cameraNode) |
72 | setFovy(self.cameraNode, self.fovy) |
73 | |
74 | self.isRotatable = Utils.getNoNil(getXMLBool(xmlFile, key .. "#rotatable"), false); |
75 | self.limit = Utils.getNoNil(getXMLBool(xmlFile, key .. "#limit"), false); |
76 | if self.limit then |
77 | self.rotMinX = getXMLFloat(xmlFile, key .. "#rotMinX"); |
78 | self.rotMaxX = getXMLFloat(xmlFile, key .. "#rotMaxX"); |
79 | |
80 | self.transMin = getXMLFloat(xmlFile, key .. "#transMin"); |
81 | self.transMax = getXMLFloat(xmlFile, key .. "#transMax"); |
82 | if self.rotMinX == nil or self.rotMaxX == nil or self.transMin == nil or self.transMax == nil then |
83 | print("Error loading camera "..key..": missing limit values"); |
84 | return false; |
85 | end; |
86 | end; |
87 | |
88 | self.isInside = Utils.getNoNil(getXMLBool(xmlFile, key .. "#isInside"), false); |
89 | if self.isInside then |
90 | self.defaultLowPassGain = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#defaultLowPassGain"), 0.5); |
91 | self.defaultVolume = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#defaultVolume"), 0.9); |
92 | else |
93 | self.defaultLowPassGain = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#defaultLowPassGain"), 1.0); |
94 | self.defaultVolume = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#defaultVolume"), 1.0); |
95 | end |
96 | self.allowHeadTracking = Utils.getNoNil(getXMLBool(xmlFile, key .. "#allowHeadTracking"), self.isInside); |
97 | |
98 | |
99 | if self.isRotatable then |
100 | self.rotateNode = Utils.indexToObject(self.vehicle.components, getXMLString(xmlFile, key .. "#rotateNode")); |
101 | end; |
102 | self.allowTranslation = (self.rotateNode ~= nil and self.rotateNode ~= self.cameraNode); |
103 | |
104 | self.useMirror = Utils.getNoNil(getXMLBool(xmlFile, key .. "#useMirror"), false); |
105 | self.useWorldXZRotation = getXMLBool(xmlFile, key .. "#useWorldXZRotation"); -- overrides the ingame setting |
106 | self.resetCameraOnVehicleSwitch = getXMLBool(xmlFile, key .. "#resetCameraOnVehicleSwitch"); -- overrides the ingame setting |
107 | |
108 | self.positionSmoothingParameter = 0; |
109 | self.lookAtSmoothingParameter = 0; |
110 | local useDefaultPositionSmoothing = Utils.getNoNil(getXMLBool(xmlFile, key .. "#useDefaultPositionSmoothing"), true); |
111 | if useDefaultPositionSmoothing then |
112 | if self.isInside then |
113 | self.positionSmoothingParameter = 0.128; -- 0.095; |
114 | self.lookAtSmoothingParameter = 0.176; -- 0.12; |
115 | else |
116 | self.positionSmoothingParameter = 0.016; |
117 | self.lookAtSmoothingParameter = 0.022; |
118 | end |
119 | end |
120 | self.positionSmoothingParameter = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#positionSmoothingParameter"), self.positionSmoothingParameter); |
121 | self.lookAtSmoothingParameter = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#lookAtSmoothingParameter"), self.lookAtSmoothingParameter); |
122 | |
123 | local useHeadTracking = g_gameSettings:getValue("isHeadTrackingEnabled") and isHeadTrackingAvailable() and self.allowHeadTracking |
124 | if useHeadTracking then |
125 | self.positionSmoothingParameter = 0; |
126 | self.lookAtSmoothingParameter = 0; |
127 | end |
128 | |
129 | self.cameraPositionNode = self.cameraNode; |
130 | if self.positionSmoothingParameter > 0 then |
131 | -- create a node which indicates the target position of the camera |
132 | self.cameraPositionNode = createTransformGroup("cameraPositionNode"); |
133 | local camIndex = getChildIndex(self.cameraNode); |
134 | link(getParent(self.cameraNode), self.cameraPositionNode, camIndex); |
135 | local x,y,z = getTranslation(self.cameraNode); |
136 | local rx,ry,rz = getRotation(self.cameraNode); |
137 | setTranslation(self.cameraPositionNode, x,y,z); |
138 | setRotation(self.cameraPositionNode, rx,ry,rz); |
139 | |
140 | unlink(self.cameraNode); |
141 | end; |
142 | self.rotYSteeringRotSpeed = math.rad(Utils.getNoNil(getXMLFloat(xmlFile, key .. "#rotYSteeringRotSpeed"), 0)); |
143 | |
144 | if self.rotateNode == nil or self.rotateNode == self.cameraNode then |
145 | self.rotateNode = self.cameraPositionNode; |
146 | end; |
147 | |
148 | if useHeadTracking then |
149 | local dx,_,dz = localDirectionToLocal(self.cameraPositionNode, getParent(self.cameraPositionNode), 0,0,-1); |
150 | self.headTrackingNode = createTransformGroup("headTrackingNode"); |
151 | link(getParent(self.cameraPositionNode), self.headTrackingNode); |
152 | setTranslation(self.headTrackingNode, 0,0,0); |
153 | if math.abs(dx)+math.abs(dz) > 0.0001 then |
154 | setDirection(self.headTrackingNode, dx, 0, dz, 0,1,0); |
155 | else |
156 | setRotation(self.headTrackingNode, 0,0,0); |
157 | end |
158 | end |
159 | |
160 | self.origRotX, self.origRotY, self.origRotZ = getRotation(self.rotateNode); |
161 | self.rotX = self.origRotX; |
162 | self.rotY = self.origRotY; |
163 | self.rotZ = self.origRotZ; |
164 | |
165 | self.origTransX, self.origTransY, self.origTransZ = getTranslation(self.cameraPositionNode); |
166 | self.transX = self.origTransX; |
167 | self.transY = self.origTransY; |
168 | self.transZ = self.origTransZ; |
169 | |
170 | local transLength = Utils.vector3Length(self.origTransX, self.origTransY, self.origTransZ); |
171 | self.zoom = transLength |
172 | self.zoomTarget = transLength; |
173 | self.zoomLimitedTarget = -1; |
174 | |
175 | local trans1OverLength = 1.0/transLength; |
176 | self.transDirX = trans1OverLength*self.origTransX; |
177 | self.transDirY = trans1OverLength*self.origTransY; |
178 | self.transDirZ = trans1OverLength*self.origTransZ; |
179 | if self.allowTranslation then |
180 | if transLength <= 0.01 then |
181 | print("Warning: Invalid camera translation in "..key..". Distance needs to be bigger than 0.01"); |
182 | end |
183 | end |
184 | |
185 | table.insert(self.raycastNodes, self.rotateNode); |
186 | local i=0; |
187 | while true do |
188 | local raycastKey = key..string.format(".raycastNode(%d)", i); |
189 | if not hasXMLProperty(xmlFile, raycastKey) then |
190 | break; |
191 | end; |
192 | |
193 | local node = Utils.indexToObject(self.vehicle.components, getXMLString(xmlFile, raycastKey .. "#index")); |
194 | if node ~= nil then |
195 | table.insert(self.raycastNodes, node); |
196 | end; |
197 | |
198 | i=i+1; |
199 | end; |
200 | |
201 | local sx,sy,sz = getScale(self.cameraNode); |
202 | if sx ~= 1 or sy ~= 1 or sz ~= 1 then |
203 | print("Warning: Vehicle camera with scale found in "..self.vehicle.configFileName.." ("..key.."). Resetting to scale 1"); |
204 | setScale(self.cameraNode, 1,1,1); |
205 | end |
206 | |
207 | self.headTrackingPositionOffset = {0, 0, 0}; |
208 | self.headTrackingRotationOffset = {0, 0, 0}; |
209 | |
210 | return true; |
211 | end; |
285 | function VehicleCamera:update(dt) |
286 | local target = self.zoomTarget; |
287 | if self.zoomLimitedTarget >= 0 then |
288 | target = math.min(self.zoomLimitedTarget, self.zoomTarget); |
289 | end; |
290 | self.zoom = target + math.pow(0.99579, dt)*(self.zoom-target); |
291 | if self.isActivated and not g_gui:getIsGuiVisible() then |
292 | if self.isRotatable then |
293 | |
294 | local rotSpeed = 0.001*dt; |
295 | local inputW = InputBinding.getDigitalInputAxis(InputBinding.AXIS_LOOK_UPDOWN_VEHICLE); |
296 | local inputZ = InputBinding.getDigitalInputAxis(InputBinding.AXIS_LOOK_LEFTRIGHT_VEHICLE); |
297 | if InputBinding.isAxisZero(inputW) then |
298 | inputW = InputBinding.getAnalogInputAxis(InputBinding.AXIS_LOOK_UPDOWN_VEHICLE); |
299 | end; |
300 | if InputBinding.isAxisZero(inputZ) then |
301 | inputZ = InputBinding.getAnalogInputAxis(InputBinding.AXIS_LOOK_LEFTRIGHT_VEHICLE); |
302 | end; |
303 | if g_gameSettings:getValue("invertYLook") then |
304 | inputW = -inputW; |
305 | end |
306 | |
307 | inputW = inputW*g_gameSettings:getValue("cameraSensitivity") |
308 | inputZ = inputZ*g_gameSettings:getValue("cameraSensitivity") |
309 | |
310 | if self.limitRotXDelta > 0.001 then |
311 | self.rotX = math.min(self.rotX - rotSpeed*inputW, self.rotX); |
312 | elseif self.limitRotXDelta < -0.001 then |
313 | self.rotX = math.max(self.rotX - rotSpeed*inputW, self.rotX); |
314 | else |
315 | self.rotX = self.rotX - rotSpeed*inputW; |
316 | end; |
317 | self.rotY = self.rotY - rotSpeed*inputZ; |
318 | end; |
319 | end; |
320 | |
321 | if self.limit then |
322 | self.rotX = math.min(self.rotMaxX, math.max(self.rotMinX, self.rotX)); |
323 | end; |
324 | |
325 | if g_gameSettings:getValue("isHeadTrackingEnabled") and isHeadTrackingAvailable() and self.allowHeadTracking and self.headTrackingNode ~= nil then |
326 | local tx,ty,tz = getHeadTrackingTranslation(); |
327 | local roll,pitch,yaw = getHeadTrackingRotation(); |
328 | if roll ~= nil then |
329 | pitch = math.max(-math.pi/2, math.min(math.pi/2, pitch)); |
330 | -- roll is dimsissed due to a 'bug' in the trackIr software: https://forums.naturalpoint.com/viewtopic.php?f=60&t=13466&p=61293 |
331 | |
332 | --setRotation(self.headTrackingNode, pitch,yaw,0); |
333 | |
334 | local camParent = getParent(self.cameraNode); |
335 | local ctx, cty, ctz = localToLocal(self.headTrackingNode, camParent, tx/100, ty/100, tz/100); |
336 | local crx, cry, crz = localRotationToLocal(self.headTrackingNode, camParent, pitch,yaw,0); |
337 | |
338 | setRotation(self.cameraNode, crx, cry, crz); |
339 | rotateAboutLocalAxis(self.cameraNode, math.pi, 0,1,0); |
340 | |
341 | setTranslation(self.cameraNode, self.origTransX + ctx, self.origTransY + cty, self.origTransZ - ctz); |
342 | end |
343 | else |
344 | |
345 | self:updateRotateNodeRotation(); |
346 | |
347 | |
348 | if self.limit then |
349 | -- adjust rotation to avoid clipping with terrain |
350 | if self.isRotatable and ((self.useWorldXZRotation == nil and g_gameSettings:getValue("useWorldCamera")) or self.useWorldXZRotation) then |
351 | |
352 | local numIterations = 4; |
353 | for i=1, numIterations do |
354 | local transX, transY, transZ = self.transDirX*self.zoom, self.transDirY*self.zoom, self.transDirZ*self.zoom; |
355 | local x,y,z = localToWorld(getParent(self.cameraPositionNode), transX, transY, transZ); |
356 | |
357 | local terrainHeight = TipUtil.getHeightAtWorldPos(x,0,z) |
358 | |
359 | local minHeight = terrainHeight + 0.9; |
360 | if y < minHeight then |
361 | local h = math.sin(self.rotX)*self.zoom; |
362 | local h2 = h-(minHeight-y); |
363 | self.rotX = math.asin(Utils.clamp(h2/self.zoom, -1, 1)); |
364 | self:updateRotateNodeRotation(); |
365 | else |
366 | break; |
367 | end; |
368 | end; |
369 | end; |
370 | |
371 | -- adjust zoom to avoid collision with objects |
372 | if self.allowTranslation then |
373 | |
374 | self.limitRotXDelta = 0; |
375 | local hasCollision, collisionDistance, nx,ny,nz, normalDotDir = self:getCollisionDistance(); |
376 | if hasCollision then |
377 | local distOffset = 0.1; |
378 | if normalDotDir ~= nil then |
379 | local absNormalDotDir = math.abs(normalDotDir) |
380 | distOffset = Utils.lerp(1.2, 0.1, absNormalDotDir*absNormalDotDir*(3-2*absNormalDotDir)); |
381 | end |
382 | collisionDistance = math.max(collisionDistance-distOffset, 0.01); |
383 | self.disableCollisionTime = g_currentMission.time+400; |
384 | self.zoomLimitedTarget = collisionDistance; |
385 | if collisionDistance < self.zoom then |
386 | self.zoom = collisionDistance; |
387 | end; |
388 | if self.isRotatable and nx ~= nil and collisionDistance < self.transMin then |
389 | local lnx,lny,lnz = worldDirectionToLocal(self.rotateNode, nx,ny,nz); |
390 | if lny > 0.5 then |
391 | self.limitRotXDelta = 1; |
392 | elseif lny < -0.5 then |
393 | self.limitRotXDelta = -1; |
394 | end; |
395 | end; |
396 | else |
397 | if self.disableCollisionTime <= g_currentMission.time then |
398 | self.zoomLimitedTarget = -1; |
399 | end; |
400 | end; |
401 | end; |
402 | |
403 | end; |
404 | self.transX, self.transY, self.transZ = self.transDirX*self.zoom, self.transDirY*self.zoom, self.transDirZ*self.zoom; |
405 | setTranslation(self.cameraPositionNode, self.transX, self.transY, self.transZ); |
406 | |
407 | if self.positionSmoothingParameter > 0 then |
408 | |
409 | local interpDt = g_physicsDt; |
410 | if g_server == nil then |
411 | -- on clients, we interpolate the vehicles with g_physicsDtUnclamped, thus we need to use the same for camera interpolation |
412 | interpDt = g_physicsDtUnclamped; |
413 | end |
414 | if interpDt > 0 then |
415 | local xlook,ylook,zlook = getWorldTranslation(self.rotateNode); |
416 | local lookAtPos = self.lookAtPosition; |
417 | local lookAtLastPos = self.lookAtLastTargetPosition; |
418 | lookAtPos[1],lookAtPos[2],lookAtPos[3] = self:getSmoothed(self.lookAtSmoothingParameter, lookAtPos[1],lookAtPos[2],lookAtPos[3], xlook,ylook,zlook, lookAtLastPos[1],lookAtLastPos[2],lookAtLastPos[3], interpDt); |
419 | lookAtLastPos[1],lookAtLastPos[2],lookAtLastPos[3] = xlook,ylook,zlook; |
420 | |
421 | local x,y,z = getWorldTranslation(self.cameraPositionNode); |
422 | local pos = self.position; |
423 | local lastPos = self.lastTargetPosition; |
424 | pos[1],pos[2],pos[3] = self:getSmoothed(self.positionSmoothingParameter, pos[1],pos[2],pos[3], x,y,z, lastPos[1],lastPos[2],lastPos[3], interpDt) |
425 | lastPos[1],lastPos[2],lastPos[3] = x,y,z; |
426 | |
427 | self:setSeparateCameraPose(); |
428 | end |
429 | end; |
430 | |
431 | end |
432 | |
433 | end; |
556 | function VehicleCamera:updateRotateNodeRotation() |
557 | local rotY = self.rotY; |
558 | if self.rotYSteeringRotSpeed ~= nil and self.rotYSteeringRotSpeed ~= 0 and self.vehicle.interpolatedRotatedTime ~= nil then |
559 | rotY = rotY + self.vehicle.interpolatedRotatedTime*self.rotYSteeringRotSpeed; |
560 | end; |
561 | |
562 | if (self.useWorldXZRotation == nil and g_gameSettings:getValue("useWorldCamera")) or self.useWorldXZRotation then |
563 | local upx,upy,upz = 0,1,0; |
564 | |
565 | local dx,_,dz = localDirectionToWorld(getParent(self.rotateNode), 0,0,1); |
566 | local invLen = 1/math.sqrt(dx*dx + dz*dz); |
567 | dx = dx*invLen; |
568 | dz = dz*invLen; |
569 | |
570 | |
571 | |
572 | local newDx = math.cos(self.rotX) * (math.cos(rotY)*dx + math.sin(rotY)*dz); |
573 | local newDy = -math.sin(self.rotX); |
574 | local newDz = math.cos(self.rotX) * (-math.sin(rotY)*dx + math.cos(rotY)*dz); |
575 | |
576 | |
577 | newDx,newDy,newDz = worldDirectionToLocal(getParent(self.rotateNode), newDx,newDy,newDz); |
578 | upx,upy,upz = worldDirectionToLocal(getParent(self.rotateNode), upx,upy,upz); |
579 | |
580 | -- worst case check |
581 | if math.abs(Utils.dotProduct(newDx,newDy,newDz, upx,upy,upz)) > ( 0.99 * Utils.vector3Length(newDx,newDy,newDz) * Utils.vector3Length(upx,upy,upz) ) then |
582 | setRotation(self.rotateNode, self.rotX, rotY, self.rotZ); |
583 | else |
584 | setDirection(self.rotateNode, newDx,newDy,newDz, upx,upy,upz); |
585 | end |
586 | else |
587 | setRotation(self.rotateNode, self.rotX, rotY, self.rotZ); |
588 | end; |
589 | end; |
620 | function VehicleCamera:getCollisionDistance() |
621 | if not self.isCollisionEnabled then |
622 | return false, nil, nil, nil, nil, nil; |
623 | end |
624 | |
625 | local raycastMask = 32+64+128+256+4096; |
626 | |
627 | local targetCamX, targetCamY, targetCamZ = localToWorld(self.rotateNode, self.transDirX*self.zoomTarget, self.transDirY*self.zoomTarget, self.transDirZ*self.zoomTarget); |
628 | |
629 | local hasCollision = false; |
630 | local collisionDistance = -1; |
631 | local normalX,normalY,normalZ; |
632 | local normalDotDir; |
633 | for _, raycastNode in ipairs(self.raycastNodes) do |
634 | |
635 | hasCollision = false; |
636 | |
637 | local nodeX, nodeY, nodeZ = getWorldTranslation(raycastNode); |
638 | local dirX, dirY, dirZ = targetCamX-nodeX, targetCamY-nodeY, targetCamZ-nodeZ; |
639 | local dirLength = Utils.vector3Length(dirX, dirY, dirZ); |
640 | dirX = dirX / dirLength; |
641 | dirY = dirY / dirLength; |
642 | dirZ = dirZ / dirLength; |
643 | |
644 | local startX = nodeX; |
645 | local startY = nodeY; |
646 | local startZ = nodeZ; |
647 | local currentDistance = 0; |
648 | local minDistance = self.transMin; |
649 | |
650 | while (true) do |
651 | if (dirLength-currentDistance) <= 0 then |
652 | break; |
653 | end; |
654 | self.raycastDistance = 0; |
655 | raycastClosest(startX, startY, startZ, dirX, dirY, dirZ, "raycastCallback", dirLength-currentDistance, self, raycastMask, true); |
656 | |
657 | if self.raycastDistance ~= 0 then |
658 | currentDistance = currentDistance + self.raycastDistance+0.001; |
659 | local ndotd = Utils.dotProduct(self.normalX, self.normalY, self.normalZ, dirX, dirY, dirZ); |
660 | |
661 | if self.vehicle.getIsAttachedVehicleNode == nil or self.vehicle.getIsDynamicallyMountedNode == nil then |
662 | break; |
663 | end |
664 | |
665 | if self.vehicle:getIsAttachedVehicleNode(self.raycastTransformId) or self.vehicle:getIsDynamicallyMountedNode(self.raycastTransformId) then |
666 | if ndotd > 0 then |
667 | minDistance = math.max(minDistance, currentDistance); |
668 | end; |
669 | else |
670 | hasCollision = true; |
671 | -- we take the distance from the rotate node |
672 | if raycastNode == self.rotateNode then |
673 | normalX,normalY,normalZ = self.normalX, self.normalY, self.normalZ; |
674 | collisionDistance = math.max(self.transMin, currentDistance); |
675 | normalDotDir = ndotd |
676 | end; |
677 | break; |
678 | end; |
679 | startX = nodeX+dirX*currentDistance; |
680 | startY = nodeY+dirY*currentDistance; |
681 | startZ = nodeZ+dirZ*currentDistance; |
682 | else |
683 | break; |
684 | end; |
685 | end; |
686 | if not hasCollision then |
687 | break; |
688 | end; |
689 | end; |
690 | return hasCollision, collisionDistance, normalX,normalY,normalZ, normalDotDir; |
691 | end; |