23 | function Suspensions.initSpecialization() |
24 | local schema = Vehicle.xmlSchema |
25 | schema:setXMLSpecializationType("Suspensions") |
26 | |
27 | schema:register(XMLValueType.NODE_INDEX, "vehicle.suspensions.suspension(?)#node", "Suspension node") |
28 | schema:register(XMLValueType.BOOL, "vehicle.suspensions.suspension(?)#useCharacterTorso", "Use character torso instead of node") |
29 | schema:register(XMLValueType.FLOAT, "vehicle.suspensions.suspension(?)#weight", "Weight in kg", 500) |
30 | schema:register(XMLValueType.VECTOR_ROT, "vehicle.suspensions.suspension(?)#minRotation", "Min. rotation") |
31 | schema:register(XMLValueType.VECTOR_ROT, "vehicle.suspensions.suspension(?)#maxRotation", "Max. rotation") |
32 | schema:register(XMLValueType.VECTOR_TRANS, "vehicle.suspensions.suspension(?)#startTranslationOffset", "Custom translation offset") |
33 | schema:register(XMLValueType.VECTOR_TRANS, "vehicle.suspensions.suspension(?)#minTranslation", "Min. translation") |
34 | schema:register(XMLValueType.VECTOR_TRANS, "vehicle.suspensions.suspension(?)#maxTranslation", "Max. translation") |
35 | schema:register(XMLValueType.FLOAT, "vehicle.suspensions.suspension(?)#maxVelocityDifference", "Max. velocity difference", 0.1) |
36 | schema:register(XMLValueType.VECTOR_2, "vehicle.suspensions.suspension(?)#suspensionParametersX", "Suspension parameters X", "0 0") |
37 | schema:register(XMLValueType.VECTOR_2, "vehicle.suspensions.suspension(?)#suspensionParametersY", "Suspension parameters Y", "0 0") |
38 | schema:register(XMLValueType.VECTOR_2, "vehicle.suspensions.suspension(?)#suspensionParametersZ", "Suspension parameters Z", "0 0") |
39 | schema:register(XMLValueType.BOOL, "vehicle.suspensions.suspension(?)#inverseMovement", "Invert movement", false) |
40 | schema:register(XMLValueType.BOOL, "vehicle.suspensions.suspension(?)#serverOnly", "Suspension is only calculated on server side", false) |
41 | |
42 | schema:register(XMLValueType.FLOAT, "vehicle.suspensions#maxUpdateDistance", "Max. distance to vehicle root to update suspension nodes", Suspensions.DEFAULT_MAX_UPDATE_DISTANCE) |
43 | |
44 | schema:setXMLSpecializationType() |
45 | end |
67 | function Suspensions:onLoad(savegame) |
68 | if self.isClient then |
69 | local spec = self.spec_suspensions |
70 | |
71 | spec.suspensionNodes = {} |
72 | |
73 | local i = 0 |
74 | while true do |
75 | local key = string.format("vehicle.suspensions.suspension(%d)", i) |
76 | if not self.xmlFile:hasProperty(key) then |
77 | break |
78 | end |
79 | |
80 | local entry = {} |
81 | entry.node = self.xmlFile:getValue(key .. "#node", nil, self.components, self.i3dMappings) |
82 | entry.refNodeOffset = {0, 0, 0} |
83 | if entry.node ~= nil then |
84 | local component = self:getParentComponent(entry.node) |
85 | if component ~= nil then |
86 | entry.component = component |
87 | entry.refNodeOffset = {localToLocal(entry.node, component, 0, 0, 0)} |
88 | end |
89 | end |
90 | |
91 | entry.useCharacterTorso = self.xmlFile:getValue(key .. "#useCharacterTorso", false) |
92 | if (entry.node ~= nil and entry.component ~= nil) or entry.useCharacterTorso then |
93 | entry.weight = self.xmlFile:getValue(key .. "#weight", 500) |
94 | |
95 | entry.minRotation = self.xmlFile:getValue(key .. "#minRotation", nil, true) |
96 | entry.maxRotation = self.xmlFile:getValue(key .. "#maxRotation", nil, true) |
97 | entry.isRotational = entry.minRotation ~= nil and entry.maxRotation ~= nil |
98 | |
99 | if not entry.isRotational and not entry.useCharacterTorso then |
100 | entry.baseTranslation = {getTranslation(entry.node)} |
101 | entry.startTranslationOffset = self.xmlFile:getValue(key.."#startTranslationOffset", "0 0 0", true) |
102 | for j=1, 3 do |
103 | entry.baseTranslation[j] = entry.baseTranslation[j] + entry.startTranslationOffset[j] |
104 | end |
105 | |
106 | entry.minTranslation = self.xmlFile:getValue(key.."#minTranslation", nil, true) |
107 | entry.maxTranslation = self.xmlFile:getValue(key.."#maxTranslation", nil, true) |
108 | end |
109 | |
110 | entry.maxVelocityDifference = self.xmlFile:getValue(key .. "#maxVelocityDifference", 0.1) |
111 | |
112 | local suspensionParametersX = self.xmlFile:getValue(key .. "#suspensionParametersX", "0 0", true) |
113 | local suspensionParametersY = self.xmlFile:getValue(key .. "#suspensionParametersY", "0 0", true) |
114 | local suspensionParametersZ = self.xmlFile:getValue(key .. "#suspensionParametersZ", "0 0", true) |
115 | entry.suspensionParameters = {} |
116 | entry.suspensionParameters[1] = {} |
117 | entry.suspensionParameters[2] = {} |
118 | entry.suspensionParameters[3] = {} |
119 | for j=1,2 do |
120 | entry.suspensionParameters[1][j] = suspensionParametersX[j] * 1000 |
121 | entry.suspensionParameters[2][j] = suspensionParametersY[j] * 1000 |
122 | entry.suspensionParameters[3][j] = suspensionParametersZ[j] * 1000 |
123 | end |
124 | |
125 | entry.inverseMovement = self.xmlFile:getValue(key .. "#inverseMovement", false) |
126 | entry.serverOnly = self.xmlFile:getValue(key .. "#serverOnly", false) |
127 | |
128 | entry.lastRefNodePosition = nil |
129 | entry.lastRefNodeVelocity = nil |
130 | |
131 | entry.curRotation = {0, 0, 0} |
132 | entry.curRotationSpeed = {0, 0, 0} |
133 | |
134 | entry.curTranslation = {0, 0, 0} |
135 | entry.curTranslationSpeed = {0, 0, 0} |
136 | |
137 | entry.curAcc = {0, 0, 0} |
138 | end |
139 | |
140 | if not entry.serverOnly or self.isServer then |
141 | table.insert(spec.suspensionNodes, entry) |
142 | end |
143 | |
144 | i = i + 1 |
145 | end |
146 | |
147 | spec.maxUpdateDistance = self.xmlFile:getValue("vehicle.suspensions#maxUpdateDistance", Suspensions.DEFAULT_MAX_UPDATE_DISTANCE) |
148 | |
149 | if #spec.suspensionNodes > 0 then |
150 | spec.suspensionAvailable = true |
151 | end |
152 | end |
153 | |
154 | if not self.spec_suspensions.suspensionAvailable then |
155 | SpecializationUtil.removeEventListener(self, "onUpdate", Suspensions) |
156 | SpecializationUtil.removeEventListener(self, "onEnterVehicle", Suspensions) |
157 | SpecializationUtil.removeEventListener(self, "onLeaveVehicle", Suspensions) |
158 | SpecializationUtil.removeEventListener(self, "onVehicleCharacterChanged", Suspensions) |
159 | end |
160 | end |
164 | function Suspensions:onUpdate(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected) |
165 | local spec = self.spec_suspensions |
166 | if self.currentUpdateDistance < spec.maxUpdateDistance then |
167 | local timeDelta = 0.001 * g_physicsDt |
168 | |
169 | for _,suspension in ipairs(spec.suspensionNodes) do |
170 | if suspension.node ~= nil and entityExists(suspension.node) then |
171 | -- calc velocity diff |
172 | suspension.curAcc[1], suspension.curAcc[2], suspension.curAcc[3] = 0, 0, 0 |
173 | |
174 | if self:getIsSuspensionNodeActive(suspension) then |
175 | local wx, wy, wz = localToWorld(suspension.component, unpack(suspension.refNodeOffset)) |
176 | |
177 | if suspension.lastRefNodePosition == nil then |
178 | suspension.lastRefNodePosition = {wx, wy, wz} |
179 | suspension.lastRefNodeVelocity = {0, 0, 0} |
180 | end |
181 | |
182 | local direction = (suspension.inverseMovement and -1) or 1 |
183 | |
184 | local newVelX, newVelY, newVelZ = (wx - suspension.lastRefNodePosition[1]) / timeDelta * direction, |
185 | (wy - suspension.lastRefNodePosition[2]) / timeDelta * direction, |
186 | (wz - suspension.lastRefNodePosition[3]) / timeDelta * direction |
187 | |
188 | local oldVelX, oldVelY, oldVelZ = unpack(suspension.lastRefNodeVelocity) |
189 | |
190 | local velDiffX, velDiffY, velDiffZ = worldDirectionToLocal(getParent(suspension.node), newVelX-oldVelX, newVelY-oldVelY, newVelZ-oldVelZ) |
191 | |
192 | velDiffX = MathUtil.clamp(velDiffX, -suspension.maxVelocityDifference, suspension.maxVelocityDifference) |
193 | velDiffY = MathUtil.clamp(velDiffY, -suspension.maxVelocityDifference, suspension.maxVelocityDifference) |
194 | velDiffZ = MathUtil.clamp(velDiffZ, -suspension.maxVelocityDifference, suspension.maxVelocityDifference) |
195 | |
196 | if suspension.isRotational then |
197 | if suspension.useCharacterTorso then |
198 | suspension.curAcc[1], suspension.curAcc[2], suspension.curAcc[3] = MathUtil.crossProduct(velDiffX/timeDelta, velDiffY/timeDelta, velDiffZ/timeDelta, 1,0,0) |
199 | else |
200 | suspension.curAcc[1], suspension.curAcc[2], suspension.curAcc[3] = MathUtil.crossProduct(velDiffX/timeDelta, velDiffY/timeDelta, velDiffZ/timeDelta, 0,1,0) |
201 | end |
202 | else |
203 | suspension.curAcc[1], suspension.curAcc[2], suspension.curAcc[3] = -velDiffX/timeDelta, -velDiffY/timeDelta, -velDiffZ/timeDelta |
204 | end |
205 | |
206 | -- prepare for next tick |
207 | suspension.lastRefNodePosition[1] = wx |
208 | suspension.lastRefNodePosition[2] = wy |
209 | suspension.lastRefNodePosition[3] = wz |
210 | |
211 | suspension.lastRefNodeVelocity[1] = newVelX |
212 | suspension.lastRefNodeVelocity[2] = newVelY |
213 | suspension.lastRefNodeVelocity[3] = newVelZ |
214 | end |
215 | |
216 | -- update spring/damper system, F = F_ext - k*x - c*(dx/dt) |
217 | -- using implicit euler for spring and damper, explicit euler for external force |
218 | for i=1, 3 do |
219 | local suspensionParameter = suspension.suspensionParameters[i] |
220 | if suspensionParameter[1] > 0 and suspensionParameter[2] > 0 then |
221 | local f = suspension.weight * suspension.curAcc[i] |
222 | |
223 | local k = suspensionParameter[1] |
224 | local c = suspensionParameter[2] |
225 | |
226 | if suspension.isRotational then |
227 | local x = suspension.curRotation[i] |
228 | local vx = suspension.curRotationSpeed[i] |
229 | |
230 | local force = f - (k*x) - (c*vx) |
231 | |
232 | -- 'Implicit Methods for Differential Equations' (Baraff), formula (4-6) |
233 | local m = suspension.weight |
234 | local h = timeDelta |
235 | local numerator = h * (force + h * (-k) * vx) / m |
236 | local denumerator = 1 - ((-c) + h * (-k)) * h / m |
237 | local curRotationSpeed = vx + numerator / denumerator |
238 | |
239 | local newRotation = x + (curRotationSpeed * timeDelta) |
240 | newRotation = MathUtil.clamp(newRotation, suspension.minRotation[i], suspension.maxRotation[i]) |
241 | |
242 | suspension.curRotationSpeed[i] = (newRotation - x) / timeDelta |
243 | suspension.curRotation[i] = newRotation |
244 | else |
245 | local x = suspension.curTranslation[i] |
246 | local vx = suspension.curTranslationSpeed[i] |
247 | |
248 | local force = f - (k*x) - (c*vx) |
249 | |
250 | -- 'Implicit Methods for Differential Equations' (Baraff), formula (4-6) |
251 | local m = suspension.weight |
252 | local h = timeDelta |
253 | local numerator = h * (force + h * (-k) * vx) / m |
254 | local denumerator = 1 - ((-c) + h * (-k)) * h / m |
255 | local curTranslationSpeed = vx + numerator / denumerator |
256 | |
257 | local newTranslation = x + (curTranslationSpeed * timeDelta) |
258 | newTranslation = MathUtil.clamp(newTranslation, suspension.minTranslation[i], suspension.maxTranslation[i]) |
259 | |
260 | suspension.curTranslationSpeed[i] = (newTranslation - x) / timeDelta |
261 | suspension.curTranslation[i] = newTranslation |
262 | end |
263 | end |
264 | end |
265 | |
266 | if suspension.isRotational then |
267 | setRotation(suspension.node, suspension.curRotation[1], suspension.curRotation[2], suspension.curRotation[3]) |
268 | else |
269 | setTranslation(suspension.node, suspension.baseTranslation[1] + suspension.curTranslation[1], suspension.baseTranslation[2] + suspension.curTranslation[2], suspension.baseTranslation[3] + suspension.curTranslation[3]) |
270 | end |
271 | |
272 | if self.setMovingToolDirty ~= nil then |
273 | self:setMovingToolDirty(suspension.node) |
274 | end |
275 | elseif suspension.node ~= nil then |
276 | Logging.xmlError(self.xmlFile, "Failed to update suspension node %d. Node does not exist anymore!", suspension.node) |
277 | suspension.node = nil |
278 | end |
279 | end |
280 | end |
281 | end |