Skip to content

Tutorial 2 ‐ Aircraft (javascript)

Gombaris edited this page Apr 17, 2024 · 11 revisions

Tutorial on how to make basic aircraft (Diamond DA40-NG) using javascript.

You can find the files belonging to this tutorial and complete code in Outerra/anteworld "Code" section in "Tutorials/example_models_js/packages/tutorial_aircraft_js" folder.

You can find information about aircraft interface on Outerra/anteworld wiki in aircraft interface.

You can also find more information in previous JavaScript tutorial (Tutorial 1 ‐ car (javascript) ).

Aircraft configuration files

If you are unfamiliar with creating flight model configuration files for your aircraft, you can use Aeromatic.

For more information about using aeromatic, see JSBSim & Aeromatic.

Note: using Aeromatic is not recommended, as it doesn't provide accurate aircraft configurations, but for now, it may serve as a useful starting point for generating aircraft configurations, especially for beginners or those looking to quickly get started with aircraft in Outerra.

Mod files

For a tutorial on mod files, please refer to mod files tutorial.


In .js script

Define a constant for PI, will be used for joint rotation

const PI = 3.14159265358979323846;

Define global variables

//Bones
let bones = {
    propeller : -1,
    wheel_front : -1,
    wheel_right : -1,
    wheel_left : -1,
    elevator_right : -1,
    elevator_left : -1,
    rudder : -1,
    aileron_right : -1,
    aileron_left : -1,
    rudder_pedal_left : -1,
    rudder_pedal_right : -1,
    brake_pedal_left : -1,
    brake_pedal_right : -1,
    throttle_handle : -1,
    flap_left : -1,
    flap_right : -1,  
};

//Meshes
let meshes = {
    prop_blur : -1,
    blade_one : -1,
    blade_two : -1,
    blade_three : -1,
};

//Sounds 
let sounds = {
    rumble : -1,
    eng_int : -1,
    eng_ext : -1,
    prop_int : -1,
    prop_ext : -1,
};

//Sound sources
let sources = {
    rumble_int : -1,
    rumble_ext : -1,
    eng_int : -1,
    eng_ext : -1,
    prop_int : -1,
    prop_ext : -1,
};

Create function to clamp values

function clamp(val, min, max) 
{
	if(val < min) return min; 
	else if(val > max) return max; 
	else return val;
}

Create function, that will be used, to start/stop the engine

function engine()
{
    //toggle the value every time this function is called 
    this.started ^= 1;
    
    if(this.started === 1)
    {
        //For JSBSim aircraft to start, the starter and magneto needs to be activated
        //Turn on engine (0 - turn off, 1 - turn on)
        this.jsbsim['propulsion/starter_cmd'] = 1;
        this.jsbsim['propulsion/magneto_cmd'] = 1;
    }
    else
    {
        this.jsbsim['propulsion/starter_cmd'] = 0;
        this.jsbsim['propulsion/magneto_cmd'] = 0;
    }
}

"this.jsbsim['property']" is used, to set/get property belonging to JSBSim interface.

For information on JSBSim, visit the JSBSim & Aeromatic page.

For a list of usable JSBSim properties, refer to the JSBSim properties page.

Debug tip: when the magneto isn't activated, it results in decreased piston engine power, with the rpm not exceeding 1000.

In function init_chassis

Get joints/bones

bones.propeller = this.get_joint_id('propel');
bones.wheel_front = this.get_joint_id('front_wheel');
bones.wheel_right = this.get_joint_id('right_wheel');
bones.wheel_left = this.get_joint_id('left_wheel');
bones.elevator_right = this.get_joint_id('elevator_right'); 
bones.elevator_left = this.get_joint_id('elevator_left');
bones.rudder = this.get_joint_id('rudder');
bones.aileron_right = this.get_joint_id('aileron_right'); 
bones.aileron_left = this.get_joint_id('aileron_left');
bones.flap_left = this.get_joint_id('flap_left');
bones.flap_right = this.get_joint_id('flap_right');
bones.rudder_pedal_left = this.get_joint_id('rudder_pedal_left');
bones.rudder_pedal_right = this.get_joint_id('rudder_pedal_right');
bones.brake_pedal_left = this.get_joint_id('brake_pedal_left');
bones.brake_pedal_right = this.get_joint_id('brake_pedal_right');
bones.throttle_handle = this.get_joint_id('throttle_lever');

Get meshes, for that use function get_mesh_id

1.Parameter: - mesh name.

Note: you can find model meshes in "Scene editor"->"Entity properties"->"Visual LODs".

meshes.prop_blur = this.get_mesh_id('propel_blur');
meshes.blade_one = this.get_mesh_id('main_blade_01#0@0');
meshes.blade_two = this.get_mesh_id('main_blade_02#0@0');
meshes.blade_three = this.get_mesh_id('main_blade_03#0@0');

Add lights

//Landing lights
let light_params = {color:{x:1,y:1,z:1}, angle:100, size:0.1, edge:0.25, intensity:5, fadeout:0.05};
this.add_spot_light({x:4.5, y:1.08, z:0.98}, {x:-0.1, y:1, z: 0.3}, light_params);
this.add_spot_light({x:-4.5, y:1.08, z:0.98}, {x:0.1, y:1, z: 0.3}, light_params);

//Navigation lights
light_params = {color:{x:0,y:1,z:0}, size:0.035, edge:1, range:0.0001, intensity:20, fadeout:0.1};
let nav_light_offset =
//Right green navigation light
this.add_point_light({x:5.08, y:0.18, z:1.33}, light_params);
//Left red navigation light
light_params.color = {x:1,y:0,z:0};
this.add_point_light({x:-5.08, y:0.18, z:1.33}, light_params);

Load sounds

//engine rumble - in this case it's used for the outside sounds and also the inside
sounds.rumble = this.load_sound("sounds/engine/engn1.ogg");
	
//Interior sounds - will be heard from inside the plane
  //engine
  sounds.eng_ext = this.load_sound("sounds/engine/engn1_out.ogg");
  //propeller
  sounds.prop_ext = this.load_sound("sounds/engine/prop1_out.ogg");

//Exterior sounds - will be heard from outside the plane 
  //engine
  sounds.eng_int = this.load_sound("sounds/engine/engn1_inn.ogg"); 
  //propeller
  sounds.prop_int = this.load_sound("sounds/engine/prop1_out.ogg");

Add sound emitters

//Interior emitters
  //For interior rumbling sound
  sources.rumble_int = this.add_sound_emitter_id(bones.propeller, -1, 0.5);
  //For interior engine sounds
  sources.eng_int = this.add_sound_emitter_id(bones.propeller, -1, 0.5);
  //For interior propeller sounds
  sources.prop_int = this.add_sound_emitter_id(bones.propeller, -1, 0.5);
    
//Exterior emitters
  //For exterior rumbling sound
  sources.rumble_ext = this.add_sound_emitter_id(bones.propeller, 1, 3); 
  //For exterior engine sounds 
  sources.eng_ext = this.add_sound_emitter_id(bones.propeller, 1, 3);
  //For exterior propeller sounds 
  sources.prop_ext = this.add_sound_emitter_id(bones.propeller, 1, 3);

Note: it is possible, to use one emitter for interior and exterior, like in previous tutorial.

Add action handlers

this.register_axis("air/lights/landing_lights", {minval: 0, maxval: 1, vel: 10, center: 0 }, function(v) { this.light_mask( 0x3,v > 0); });
this.register_axis("air/lights/nav_lights", {minval: 0, maxval: 1, vel: 10, center: 0 }, function(v) { this.light_mask( 0x3,v > 0, nav_light_offset); });
this.register_event("air/engines/on", engine);
this.register_axis("air/controls/aileron", { minval: -1, maxval: 1, cenvel: 0.5, vel: 0.5, positions: 0 }, function(v){
    this.jsbsim['fcs/aileron-cmd-norm'] = v;
});
this.register_axis("air/controls/elevator", { minval: -1, maxval: 1, cenvel: 0.5, vel: 0.5, positions: 0 }, function(v){
    this.jsbsim['fcs/elevator-cmd-norm'] = -v;
});

Note: by default, JSBSim actions are handled internally, allowing gameplay without direct user intervention. However, sometimes it is needed to customize or fine-tune aircraft behavior, therefore in this case, the engine, ailerons and elevator functionality is handled in script

In function initialize

Function initialize is used to define per-instance parameters (same as init_vehicle in vehicle interface)

Get geomob interface

this.geom = this.get_geomob(0);

Get JSBSim interface, to be able to work with JSBSim properties

this.jsbsim = this.jsb();

Get sound interface

this.snd = this.sound();

Set first person camera position

this.set_fps_camera_pos({x:0, y:1, z:1.4});

Set initial value for JSBSim properties.

//Turn off engine (commands are normalized)
this.jsbsim['propulsion/starter_cmd'] = 0;
//Turn off magneto
this.jsbsim['propulsion/magneto_cmd'] = 0;
//Set initial throttle value to 0 
this.jsbsim['fcs/throttle-cmd-norm[0]'] = 0;
//Set initial throttle value to 0 
this.jsbsim['fcs/mixture-cmd-norm'] = 0;
//Set initial right brake value to 0 
this.jsbsim['fcs/right-brake-cmd-norm'] = 0;
//Set initial left brake value to 0 
this.jsbsim['fcs/left-brake-cmd-norm'] = 0;

Set default pitch value for sound emitters

this.snd.set_pitch(sources.rumble_ext, 1);
this.snd.set_pitch(sources.rumble_int, 1);
this.snd.set_pitch(sources.eng_ext, 1);
this.snd.set_pitch(sources.eng_int, 1);
this.snd.set_pitch(sources.prop_ext, 1);
this.snd.set_pitch(sources.prop_int, 1);

Set default gain value for sound emitters

this.snd.set_gain(sources.rumble_ext, 1);
this.snd.set_gain(sources.rumble_int, 1);
this.snd.set_gain(sources.eng_ext, 1);
this.snd.set_gain(sources.eng_int, 1);
this.snd.set_gain(sources.prop_ext, 1);
this.snd.set_gain(sources.prop_int, 1);

In function update_frame

Get values from JSBSim properties

//Get engine rpm from JSBSim
let propeller_rpm = this.jsbsim['propulsion/engine[0]/propeller-rpm'];
//Get wheel speed from JSBSim
let wheel_speed = this.jsbsim['gear/unit[0]/wheel-speed-fps'];
//Get elevator rotation in radians
let elev_pos_rad = this.jsbsim['fcs/elevator-pos-rad'];

Rotate & move joints

//rotate_joint - bone rotates by given angle every time (incremental, given angle is added to joint current orientation)
this.geom.rotate_joint(bones.propeller, dt * (2 * PI) * (propeller_rpm ), {y:1});
this.geom.rotate_joint(bones.wheel_front, dt * PI * (wheel_speed / 5), {x:-1});
this.geom.rotate_joint(bones.wheel_right, dt * PI * (wheel_speed / 5), {x:-1});
this.geom.rotate_joint(bones.wheel_left, dt * PI * (wheel_speed / 5), {x:-1});
//rotate_joint_orig - rotate to given angle (from default orientation)
this.geom.rotate_joint_orig(bones.elevator_right, elev_pos_rad, {x:1});
this.geom.rotate_joint_orig(bones.elevator_left, elev_pos_rad, {x:1});
this.geom.rotate_joint_orig(bones.rudder, this.jsbsim['fcs/rudder-pos-rad'], {z:-1});
this.geom.rotate_joint_orig(bones.aileron_right, this.jsbsim['fcs/right-aileron-pos-rad'], {x:-1});
this.geom.rotate_joint_orig(bones.aileron_left, this.jsbsim['fcs/left-aileron-pos-rad'], {x:1});
this.geom.rotate_joint_orig(bones.flap_left, this.jsbsim['fcs/flap-pos-rad'], {x:1,y:0,z:0});
this.geom.rotate_joint_orig(bones.flap_right, this.jsbsim['fcs/flap-pos-rad'], {x:1,y:0,z:0});
this.geom.rotate_joint_orig(bones.brake_pedal_left, this.jsbsim['fcs/left-brake-cmd-norm'] * 0.7, {x:-1});
this.geom.rotate_joint_orig(bones.brake_pedal_right, this.jsbsim['fcs/right-brake-cmd-norm'] * 0.7, {x:-1});

//Rudder has 2 pedals for turning, therefore depending on the rudder position value from JSBSim, the left or right pedal will move
if(this.jsbsim['fcs/rudder-pos-rad'] > 0)
{
	this.geom.move_joint_orig(bones.rudder_pedal_left, {y:(this.jsbsim['fcs/rudder-pos-rad']/5)} );
}
else if(this.jsbsim['fcs/rudder-pos-rad'] < 0)
{
	this.geom.move_joint_orig(bones.rudder_pedal_right, {y:-(this.jsbsim['fcs/rudder-pos-rad']/5)} );
}

Move throttle handle, depending on the throttle command value

this.geom.move_joint_orig(bones.throttle_handle, {y:(this.jsbsim['fcs/throttle-cmd-norm[0]'] / 7)});

Note: in this case the pitch/roll handle rotates without the need of scripting, because it's binded with inputs in the model.

Use set_mesh_visible_id function, to set propeller blur mesh visible, when condition is true and invisible otherwise.

1.parameter - mesh ID

2.parameter - condition

this.geom.set_mesh_visible_id(meshes.prop_blur, propeller_rpm > 200.0);
this.geom.set_mesh_visible_id(meshes.blade_one, propeller_rpm < 300.0);
this.geom.set_mesh_visible_id(meshes.blade_two, propeller_rpm < 300.0);
this.geom.set_mesh_visible_id(meshes.blade_three, propeller_rpm < 300.0);

When engine runs, control sounds, based on the camera position & sound effects, based on engine RPM.

if(propeller_rpm > 0)
{
	//Interior
	if (this.get_camera_mode() === 0 ) 
	{	
		//Turn off the exterior emitters, when player camera is inside the plane
		this.snd.stop(sources.eng_ext); 
            	this.snd.stop(sources.prop_ext);
           	this.snd.stop(sources.rumble_ext);
           
		//Calculate and set pitch for interior engine emitter (clamped between 1 and 2)
		this.snd.set_pitch(sources.eng_int, clamp(1 + propeller_rpm /4000, 1, 2));
		//Calculate and set gain for interior engine emitter (clamped between 0 and 0.5)
		this.snd.set_gain(sources.eng_int, clamp(propeller_rpm /5000, 0, 0.5));
			
                //If there is no sound already playing on given emitter, play loop
		if(!this.snd.is_playing(sources.eng_int)) 
		{
			//Play interior engine sound in a loop
			this.snd.play_loop(sources.eng_int, sounds.eng_int);
		}
            
		//Set gain for interior propeller emitter
		this.snd.set_gain(sources.prop_int, clamp(propeller_rpm /7000, 0, 0.5));
			
		if(!this.snd.is_playing(sources.prop_int)) 
		{
			//Play interior propeller sound in a loop
			this.snd.play_loop(sources.prop_int, sounds.prop_int);
		}
			
		//Set gain for interior rumble emitter
		this.snd.set_gain (sources.rumble_int, clamp(propeller_rpm /6000, 0, 0.5));
			
            	if(!this.snd.is_playing(sources.rumble_int)) 
		{
			//Play rumble sound in a loop on interior emitter
			this.snd.play_loop(sources.rumble_int, sounds.rumble);
		}
        }
	//Exterior
	else 
	{
		//Turn off the interior emitters, when camera is outside the plane
		this.snd.stop (sources.eng_int);
		this.snd.stop (sources.prop_int);
		this.snd.stop (sources.rumble_int);
            
		//Set pitch for exterior engine emitter
		this.snd.set_pitch(sources.eng_ext, clamp(1 + propeller_rpm /1000, 1, 3));
		//Set gain for exterior engine emitter
		this.snd.set_gain (sources.eng_ext, clamp(propeller_rpm /450, 0.05, 2));
			
            	if(!this.snd.is_playing(sources.eng_ext)) 
		{
			//Play exterior engine sound in a loop
			this.snd.play_loop(sources.eng_ext, sounds.eng_ext);
		}
            
            	//Set gain for exterior propeller emitter
		this.snd.set_gain (sources.prop_ext, clamp(propeller_rpm /900, 0, 2));
           
            	if(!this.snd.is_playing(sources.prop_ext)) 
		{
			//Play exterior propeller sound in a loop
			this.snd.play_loop(sources.prop_ext, sounds.prop_ext);
		}
			
		//Set gain for exterior rumble emitter
		this.snd.set_gain (sources.rumble_ext, clamp(propeller_rpm /1200, 0, 2));
		
		if(!this.snd.is_playing(sources.rumble_ext)) 
		{
			//Play rumble sound in a loop on exterior emitter
			this.snd.play_loop(sources.rumble_ext, sounds.rumble);
		}
	}
}
else 
{
	//Stop all sounds playing on given emitters, when engine is turned off and propeller stops rotating
	this.snd.stop(sources.eng_ext);
	this.snd.stop(sources.eng_int);
       	this.snd.stop(sources.prop_ext);
	this.snd.stop(sources.prop_int);
	this.snd.stop(sources.rumble_ext);
	this.snd.stop(sources.rumble_int);
}