# Dynamic Cockpit View manager. Tries to simulate the pilot's most likely # deliberate view direction. Doesn't consider forced view changes due to # acceleration. sin = func(a) { math.sin(a * math.pi / 180.0) } cos = func(a) { math.cos(a * math.pi / 180.0) } sigmoid = func(x) { 1 / (1 + math.exp(-x)) } nsigmoid = func(x) { 2 / (1 + math.exp(-x)) - 1 } pow = func(v, w) { math.exp(math.ln(v) * w) } npow = func(v, w) { math.exp(math.ln(abs(v)) * w) * (v < 0 ? -1 : 1) } clamp = func(v, min, max) { v < min ? min : v > max ? max : v } normatan = func(x) { math.atan2(x, 1) * 2 / math.pi } # Class that implements EWMA (Exponentially Weighted Moving Average) # lowpass filter. Initialize with coefficient, set new value when # you fetch one. filter() pushes new value and returns filtered value, # get() only returns filtered value. A filter coefficient of 0 means # no filtering, coeff = 1 generates a factor 0.1, coeff = 2 a facdtor # 0.01, coeff = 3 a factor 0.001, etc. # LowPass = { new : func(coeff) { var m = { parents : [LowPass] }; m.value = nil; m.coeff = 1.0 / pow(10, abs(coeff)); return m; }, filter : func(v) { me.filter = me._filter_; me.value = v; }, _filter_ : func(v) { me.value = v * me.coeff + me.value * (1 - me.coeff); }, get : func { me.value; }, }; # Class that reads a property value, applies factor & offset, clamps to min & max, # and optionally lowpass filters. # Input = { new : func(prop = "/null", factor = 1, offset = 0, filter = 0, min = nil, max = nil) { var m = { parents : [Input] }; m.prop = isa(props.Node, prop) ? prop : props.globals.getNode(prop, 1); m.factor = factor; m.offset = offset; m.min = min; m.max = max; m.lowpass = filter ? LowPass.new(filter) : nil; return m; }, get : func { var v = me.prop.getValue() * me.factor + me.offset; if (me.min != nil and v < me.min) { v = me.min; } if (me.max != nil and v > me.max) { v = me.max; } return me.lowpass == nil ? v : me.lowpass.filter(v); }, set : func(v) { me.prop.setDoubleValue(v); }, }; # Class that maintains one sim/current-view/goal-*-offset-deg property. # ViewAxis = { new : func(prop) { var m = { parents : [ViewAxis] }; m.prop = props.globals.getNode(prop, 0); m.reset(); return m; }, reset : func { me.applied_offset = 0; }, add_offset : func { me.prop.setValue(me.prop.getValue() + me.applied_offset); }, apply : func(v) { var raw = me.prop.getValue() - me.applied_offset; me.applied_offset = v; me.prop.setDoubleValue(raw + me.applied_offset); }, }; # Class that manages a dynamic cockpit view by manipulating # sim/current-view/goal-*-offset-deg properties. # ViewManager = { new : func(c) { var m = { parents : [ViewManager] }; m.elapsedN = props.globals.getNode("/sim/time/elapsed-sec", 1); m.deltaN = props.globals.getNode("/sim/time/delta-realtime-sec", 1); m.headingN = props.globals.getNode("/orientation/heading-deg", 1); m.pitchN = props.globals.getNode("/orientation/pitch-deg", 1); m.rollN = props.globals.getNode("/orientation/roll-deg", 1); m.slipN = props.globals.getNode("/orientation/side-slip-deg", 1); m.wind_dirN = props.globals.getNode("/environment/wind-from-heading-deg", 1); m.wind_speedN = props.globals.getNode("/environment/wind-speed-kt", 1); m.heading_axis = ViewAxis.new("/sim/current-view/goal-heading-offset-deg"); m.pitch_axis = ViewAxis.new("/sim/current-view/goal-pitch-offset-deg"); m.roll_axis = ViewAxis.new("/sim/current-view/goal-roll-offset-deg"); # accerations are converted to G (one G is subtraced from z-accel) m.ax = Input.new("/accelerations/pilot/x-accel-fps_sec", 0.03108095, 0, 1.1, 0); m.ay = Input.new("/accelerations/pilot/y-accel-fps_sec", 0.03108095, 0, 1.3); m.az = Input.new("/accelerations/pilot/z-accel-fps_sec", -0.03108095, -1, 1.0073); # velocities are converted to knots m.vx = Input.new("/velocities/uBody-fps", 0.5924838, 0, 1); m.vy = Input.new("/velocities/vBody-fps", 0.5924838, 0); m.vz = Input.new("/velocities/wBody-fps", 0.5924838, 0); # turn WoW bool into smooth values ranging from -1 to 1 m.wow = Input.new("/gear/gear/wow", 1, 0, 1.2); m.hdg_chg_rate = LowPass.new(1.3); m.last_heading = m.headingN.getValue(); m.size_factor = -getprop("/sim/chase-distance-m") / 25; m.reset(); return m; }, reset : func { me.heading_axis.reset(); me.pitch_axis.reset(); me.roll_axis.reset(); }, add_offset : func { me.heading_axis.add_offset(); me.pitch_axis.add_offset(); me.roll_axis.add_offset(); }, apply : func { var pitch = me.pitchN.getValue(); var roll = me.rollN.getValue(); var wow = me.wow.get(); # var ax = me.ax.get(); # var ay = me.ay.get(); var az = me.az.get(); # var adir = math.atan2(ay, ax) * 180 / math.pi; # var aval = math.sqrt(ax * ax + ay * ay); # var vx = me.vx.get(); # var vy = me.vy.get(); # var vdir = math.atan2(vy, vx) * 180 / math.pi; # var vval = math.sqrt(vx * vx + vy * vy); # var wspd = me.wind_speedN.getValue(); # var wdir = me.headingN.getValue() - me.wind_dirN.getValue(); # var u = vx - wspd * cos(wdir); var hdg = me.headingN.getValue(); var hdiff = me.last_heading - hdg; me.last_heading = hdg; while (hdiff >= 180) { hdiff -= 360; } while (hdiff < -180) { hdiff += 360; } var steering = normatan(me.hdg_chg_rate.filter(hdiff)) * me.size_factor; me.heading_axis.apply( # heading ... -15 * sin(roll) * cos(pitch) # due to roll + 40 * steering * wow # due to ground steering + 0.4 * me.slipN.getValue() * (1 - wow) # due to sideslip (in air) ); me.pitch_axis.apply( # pitch ... 10 * sin(roll) * sin(roll) # due to roll + 30 * (1 / (1 + math.exp(2 - az)) # due to G load - 0.119202922) # [move to origin; 1/(1+exp(2)) ] ); me.roll_axis.apply( # roll ... 0.0 * sin(roll) * cos(pitch) # due to roll (none) ); }, }; # Update loop for the whole dynamic view manager. It only runs if # /sim/view[0]/dynamic/enabled is true. # main_loop = func(id) { if (id != loop_id) { return; } if (cockpit_view and !panel_visible and !mouse_button) { view_manager.apply(); } settimer(func { main_loop(id) }, 0); } # Register new, aircraft specific manager. (Needs to offer the # same methods, of course.) # register = func(mgr) { view_manager = mgr; var p = "/sim/view/dynamic/enabled"; setprop(p, getprop(p)); # fire listener } var original_resetView = nil; var panel_visibilityN = nil; var dynamic_view = nil; var view_manager = nil; var cockpit_view = nil; var panel_visible = nil; var mouse_button = nil; var loop_id = 0; var L = []; # vector of listener ids; allows to remove all listeners (= useless feature :-) # Initialization. # settimer(func { # disable menu entry and return for inappropriate FDMs (see Main/fg_init.cxx) var fdms = { acms:0, ada:0, balloon:0, external:0, jsb:1, larcsim:1, magic:0, network:0, null:0, pipe:0, ufo:0, yasim:1, }; var fdm = getprop("/sim/flight-model"); if (!contains(fdms, fdm) or !fdms[fdm]) { return gui.menuEnable("dynamic-view", 0); } # some properties may still be unavailable or nil props.globals.getNode("/accelerations/pilot/x-accel-fps_sec", 1).setDoubleValue(0); props.globals.getNode("/accelerations/pilot/y-accel-fps_sec", 1).setDoubleValue(0); props.globals.getNode("/accelerations/pilot/z-accel-fps_sec", 1).setDoubleValue(-32); props.globals.getNode("/orientation/side-slip-deg", 1).setDoubleValue(0); props.globals.getNode("/gear/gear/wow", 1).setBoolValue(1); # let listeners keep some variables up-to-date, so that they don't have # to be queried in the loop append(L, setlistener("/sim/current-view/view-number", func { cockpit_view = (cmdarg().getValue() == 0); }, 1)); append(L, setlistener("/sim/signals/reinit", func { cockpit_view = getprop("/sim/current-view/view-number") == 0; }, 0)); append(L, setlistener("/sim/panel/visibility", func { panel_visible = cmdarg().getBoolValue(); }, 1)); append(L, setlistener("/devices/status/mice/mouse/button", func { mouse_button = cmdarg().getBoolValue(); }, 1)); var config = props.Node.new({ "heading-due-to" : { "roll" : -15, "ground-steering" : 40, "side-slip" : 0.4, }, "pitch-due-to" : { "roll" : 10, "g-load" : 30, }, "roll-due-to" : { "roll" : 0, }, }); props.copy(props.globals.getNode("/sim/view/dynamic", 1), config); view_manager = ViewManager.new(config); original_resetView = view.resetView; view.resetView = func { original_resetView(); if (cockpit_view and dynamic_view) { view_manager.add_offset(); } } append(L, setlistener("/sim/view/dynamic/enabled", func { dynamic_view = cmdarg().getBoolValue(); loop_id += 1; view.resetView(); if (dynamic_view) { main_loop(loop_id); } }, 1)); append(L, setlistener("/sim/signals/reinit", func { view_manger.reset(); }, 0)); }, 0);