1 # Dynamic Cockpit View manager. Tries to simulate the pilot's most likely
2 # deliberate view direction. Doesn't consider forced view changes due to
5 # To override the default recipes, put something like this into one of
6 # your aircraft's Nasal files:
8 # dynamic_view.register(func {
9 # # me.default_plane(); # uncomment one of these if you want
10 # # me.default_helicopter(); # to base your code on the defaults
12 # # positive values rotate (deg) or move (m)
13 # me.heading_offset = ... # left
14 # me.pitch_offset = ... # up
15 # me.roll_offset = ... # right
16 # me.x_offset = ... # right (transversal axis)
17 # me.y_offset = ... # up (vertical axis)
18 # me.z_offset = ... # back/aft (longitudinal axis)
19 # me.fov_offset = ... # zoom out (field of view)
22 # All offsets are by default 0, and you only need to set them if they should
23 # be non-zero. The registered function is called for each frame and the respective
24 # view parameters are set accordingly. The function can access all internal
25 # variables of the view_manager class, such as me.roll, me.pitch, etc., and it
26 # can, of course, also use module variables from the file where it's defined.
28 # The following commands move smoothly to a fixed view position and back.
29 # All values are relative to aircraft origin (absolute), not relative to
30 # the default cockpit view position. The time and field-of-view argument
33 # dynamic_view.lookat(hdg, pitch, roll, x, y, z [, time=0.2 [, fov=55]]);
34 # dynamic_view.resume();
37 var FREEZE_DURATION = 2;
41 var sin = func(a) { math.sin(a * math.pi / 180.0) }
42 var cos = func(a) { math.cos(a * math.pi / 180.0) }
43 var sigmoid = func(x) { 1 / (1 + math.exp(-x)) }
44 var nsigmoid = func(x) { 2 / (1 + math.exp(-x)) - 1 }
45 var pow = func(v, w) { v < 0 ? nil : v == 0 ? 0 : math.exp(math.ln(v) * w) }
46 var npow = func(v, w) { v == 0 ? 0 : math.exp(math.ln(abs(v)) * w) * (v < 0 ? -1 : 1) }
47 var clamp = func(v, min, max) { v < min ? min : v > max ? max : v }
48 var normatan = func(x) { math.atan2(x, 1) * 2 / math.pi }
50 var normdeg = func(a) {
60 # Class that reads a property value, applies factor & offset, clamps to min & max,
61 # and optionally lowpass filters.
64 new : func(prop = "/null", factor = 1, offset = 0, filter = 0, min = nil, max = nil) {
65 var m = { parents : [Input] };
66 m.prop = isa(props.Node, prop) ? prop : props.globals.getNode(prop, 1);
71 m.lowpass = filter ? aircraft.lowpass.new(filter) : nil;
75 var v = me.prop.getValue() * me.factor + me.offset;
76 if (me.min != nil and v < me.min)
78 if (me.max != nil and v > me.max)
81 return me.lowpass == nil ? v : me.lowpass.filter(v);
84 me.prop.setDoubleValue(v);
90 # Class that maintains one sim/current-view/goal-*-offset-deg property.
94 var m = { parents : [ViewAxis] };
95 m.prop = props.globals.getNode(prop, 1);
96 if (m.prop.getType() == "NONE")
97 m.prop.setDoubleValue(0);
103 me.applied_offset = 0;
106 me.prop.setValue(me.prop.getValue() + me.applied_offset);
109 var raw = me.prop.getValue() - me.applied_offset;
110 me.prop.setValue(raw);
114 var raw = me.prop.getValue() - me.applied_offset;
115 me.applied_offset = v;
116 me.prop.setDoubleValue(raw + me.applied_offset);
119 normdeg(v - me.prop.getValue() + me.applied_offset);
125 # Singleton class that manages a dynamic cockpit view by manipulating
126 # sim/current-view/goal-*-offset-deg properties.
130 me.elapsedN = props.globals.getNode("/sim/time/elapsed-sec", 1);
131 me.deltaN = props.globals.getNode("/sim/time/delta-realtime-sec", 1);
133 me.headingN = props.globals.getNode("/orientation/heading-deg", 1);
134 me.pitchN = props.globals.getNode("/orientation/pitch-deg", 1);
135 me.rollN = props.globals.getNode("/orientation/roll-deg", 1);
136 me.slipN = props.globals.getNode("/orientation/side-slip-deg", 1);
137 me.speedN = props.globals.getNode("velocities/airspeed-kt", 1);
139 me.wind_dirN = props.globals.getNode("/environment/wind-from-heading-deg", 1);
140 me.wind_speedN = props.globals.getNode("/environment/wind-speed-kt", 1);
143 me.heading_axis = ViewAxis.new("/sim/current-view/goal-heading-offset-deg"),
144 me.pitch_axis = ViewAxis.new("/sim/current-view/goal-pitch-offset-deg"),
145 me.roll_axis = ViewAxis.new("/sim/current-view/goal-roll-offset-deg"),
146 me.x_axis = ViewAxis.new("/sim/current-view/x-offset-m"),
147 me.y_axis = ViewAxis.new("/sim/current-view/y-offset-m"),
148 me.z_axis = ViewAxis.new("/sim/current-view/z-offset-m"),
149 me.fov_axis = ViewAxis.new("/sim/current-view/field-of-view"),
152 # accelerations are converted to G (Earth gravitation is omitted)
153 me.ax = Input.new("/accelerations/pilot/x-accel-fps_sec", 0.03108095, 0, 0.58, 0);
154 me.ay = Input.new("/accelerations/pilot/y-accel-fps_sec", 0.03108095, 0, 0.95);
155 me.az = Input.new("/accelerations/pilot/z-accel-fps_sec", -0.03108095, -1, 0.46);
157 # velocities are converted to knots
158 me.vx = Input.new("/velocities/uBody-fps", 0.5924838, 0, 0.45);
159 me.vy = Input.new("/velocities/vBody-fps", 0.5924838, 0);
160 me.vz = Input.new("/velocities/wBody-fps", 0.5924838, 0);
162 # turn WoW bool into smooth values ranging from 0 to 1
163 me.wow = Input.new("/gear/gear/wow", 1, 0, 0.74);
164 me.hdg_change = aircraft.lowpass.new(0.95);
165 me.ubody = aircraft.lowpass.new(0.95);
166 me.last_heading = me.headingN.getValue();
167 me.size_factor = getprop("/sim/chase-distance-m") / -25;
170 me.blendN = props.globals.getNode("/sim/view/dynamic/blend", 1);
171 me.blendN.setDoubleValue(0);
172 me.blendtime = BLEND_TIME;
175 if (props.globals.getNode("rotors", 0) != nil)
176 me.calculate = me.default_helicopter;
178 me.calculate = me.default_plane;
182 me.heading_offset = me.heading = me.target_heading = 0;
183 me.pitch_offset = me.pitch = me.target_pitch = 0;
184 me.roll_offset = me.roll = me.target_roll = 0;
185 me.x_offset = me.x = me.target_x = 0;
186 me.y_offset = me.y = me.target_y = 0;
187 me.z_offset = me.z = me.target_z = 0;
188 me.fov_offset = me.fov = me.target_fov = 0;
190 interpolate(me.blendN);
191 me.blendN.setDoubleValue(0);
192 foreach (var a; me.axes)
198 me.heading_axis.add_offset();
199 me.pitch_axis.add_offset();
200 me.roll_axis.add_offset();
201 me.fov_axis.add_offset();
204 if (me.elapsedN.getValue() < me.frozen)
209 me.pitch = me.pitchN.getValue();
210 me.roll = me.rollN.getValue();
214 var b = me.blendN.getValue();
216 me.heading = me.target_heading * b + me.heading_offset * B;
217 me.pitch = me.target_pitch * b + me.pitch_offset * B;
218 me.roll = me.target_roll * b + me.roll_offset * B;
219 me.x = me.target_x * b + me.x_offset * B;
220 me.y = me.target_y * b + me.y_offset * B;
221 me.z = me.target_z * b + me.z_offset * B;
222 me.fov = me.target_fov * b + me.fov_offset * B;
224 me.heading_axis.apply(me.heading);
225 me.pitch_axis.apply(me.pitch);
226 me.roll_axis.apply(me.roll);
227 me.x_axis.apply(me.x);
228 me.y_axis.apply(me.y);
229 me.z_axis.apply(me.z);
230 me.fov_axis.apply(me.fov);
232 lookat : func(heading, pitch, roll, x, y, z, time, fov) {
233 me.target_heading = me.heading_axis.static(heading);
234 me.target_pitch = me.pitch_axis.static(pitch);
235 me.target_roll = me.roll_axis.static(roll);
236 me.target_x = me.x_axis.static(x);
237 me.target_y = me.y_axis.static(y);
238 me.target_z = me.z_axis.static(z);
239 me.target_fov = me.fov_axis.static(fov);
242 me.blendN.setValue(0);
243 interpolate(me.blendN, 1, me.blendtime);
246 interpolate(me.blendN, 0, me.blendtime);
247 me.blendtime = BLEND_TIME;
251 me.target_heading = me.heading;
252 me.target_pitch = me.pitch;
253 me.target_roll = me.roll;
257 me.target_fov = me.fov;
258 me.blendN.setDoubleValue(1);
260 me.frozen = me.elapsedN.getValue() + FREEZE_DURATION;
272 # default calculations for a plane
274 view_manager.default_plane = func {
275 var wow = me.wow.get();
277 # calculate steering factor
278 var hdg = me.headingN.getValue();
279 var hdiff = normdeg(me.last_heading - hdg);
280 me.last_heading = hdg;
281 var steering = normatan(me.hdg_change.filter(hdiff)) * me.size_factor;
283 var az = me.az.get();
284 var vx = me.vx.get();
286 # calculate sideslip factor (zeroed when no forward ground speed)
287 var wspd = me.wind_speedN.getValue();
288 var wdir = me.headingN.getValue() - me.wind_dirN.getValue();
289 var u = vx - wspd * cos(wdir);
290 var slip = sin(me.slipN.getValue()) * me.ubody.filter(normatan(u / 10));
292 me.heading_offset = # view heading
293 -15 * sin(me.roll) * cos(me.pitch) # due to roll
294 + 40 * steering * wow # due to ground steering
295 + 10 * slip * (1 - wow); # due to sideslip (in air)
297 me.pitch_offset = # view pitch
298 10 * sin(me.roll) * sin(me.roll) # due to roll
299 + 30 * (1 / (1 + math.exp(2 - az)) # due to G load
300 - 0.119202922); # [move to origin; 1/(1+exp(2)) ]
307 # default calculations for a helicopter
309 view_manager.default_helicopter = func {
310 var lowspeed = 1 - normatan(me.speedN.getValue() / 20);
312 me.heading_offset = # view heading due to
313 -50 * npow(sin(me.roll) * cos(me.pitch), 2); # roll
315 me.pitch_offset = # view pitch due to
316 (me.pitch < 0 ? -35 : -40) * sin(me.pitch) * lowspeed # pitch
317 + 15 * sin(me.roll) * sin(me.roll); # roll
319 me.roll_offset = # view roll due to
320 -15 * sin(me.roll) * cos(me.pitch) * lowspeed; # roll
325 # Update loop for the whole dynamic view manager. It only runs if
326 # /sim/view[0]/dynamic/enabled is true.
328 var main_loop = func(id) {
329 id == loop_id or return;
330 if (cockpit_view and !panel_visible) {
334 view_manager.apply();
336 settimer(func { main_loop(id) }, 0);
343 view_manager.freeze();
346 var register = func(f) {
347 view_manager.calculate = f;
351 view_manager.reset();
355 call(view_manager.lookat, arg, view_manager);
359 view_manager.resume();
363 var original_resetView = nil;
364 var panel_visibilityN = nil;
365 var dynamic_view = nil;
367 var cockpit_view = nil;
368 var panel_visible = nil; # whether 2D panel is visible
370 var mouse_mode = nil;
371 var mouse_button = nil;
378 _setlistener("/sim/signals/nasal-dir-initialized", func {
379 # disable menu entry and return for inappropriate FDMs (see Main/fg_init.cxx)
381 acms:0, ada:0, balloon:0, external:0,
382 jsb:1, larcsim:1, magic:0, network:0,
383 null:0, pipe:0, ufo:0, yasim:1,
385 var fdm = getprop("/sim/flight-model");
386 if (!contains(fdms, fdm) or !fdms[fdm])
387 return gui.menuEnable("dynamic-view", 0);
389 # some properties may still be unavailable or nil
390 props.globals.getNode("/accelerations/pilot/x-accel-fps_sec", 1).setDoubleValue(0);
391 props.globals.getNode("/accelerations/pilot/y-accel-fps_sec", 1).setDoubleValue(0);
392 props.globals.getNode("/accelerations/pilot/z-accel-fps_sec", 1).setDoubleValue(-32);
393 props.globals.getNode("/orientation/side-slip-deg", 1).setDoubleValue(0);
394 props.globals.getNode("/gear/gear/wow", 1).setBoolValue(1);
395 elapsedN = props.globals.getNode("/sim/time/elapsed-sec", 1);
397 # let listeners keep some variables up-to-date, so that they don't have
398 # to be queried in the loop
399 setlistener("/sim/panel/visibility", func { panel_visible = cmdarg().getValue() }, 1);
400 setlistener("/sim/current-view/view-number", func { cockpit_view = !cmdarg().getValue() }, 1);
401 setlistener("/devices/status/mice/mouse/button", func { mouse_button = cmdarg().getValue() }, 1);
402 setlistener("/devices/status/mice/mouse/x", freeze);
403 setlistener("/devices/status/mice/mouse/y", freeze);
404 setlistener("/devices/status/mice/mouse/mode", func {
405 if (mouse_mode = cmdarg().getValue())
406 view_manager.unfreeze();
409 setlistener("/sim/signals/reinit", func {
410 cmdarg().getValue() and return;
411 cockpit_view = getprop("/sim/current-view/view-number") == 0;
412 view_manager.reset();
417 original_resetView = view.resetView;
418 view.resetView = func {
419 original_resetView();
420 if (cockpit_view and dynamic_view)
421 view_manager.add_offset();
425 setlistener("/sim/view/dynamic/enabled", func {
426 dynamic_view = cmdarg().getBoolValue();