Custom skin creation tutorial

(version française ici)

It’s pretty easy for anyone to add  their own skins for BOMB and here’s a tutorial detailing how to do it.

Notice: This tutorial is not made from an artistic point of view, but  solely a technical how-to.

The list below links all the planes from the game to its a template containing a full texture set. The second name, the one with the link is the internal (in engine) name of the plane.

VB-42 Calamari : AI_PLANE_10

MM.3803 Bilby: AI_PLANE_11

CK50 Kani: AI_PLANE_12


VB-37 Astore: AI_PLANE_HF

S.N.A.F Rorqual: P_PLANE_B


S&E P.10 Libelle: AI_PLANE_1

Their respective folders are found in game_installation_directory/BOMB/Media/objets/aircrafts

Use your favorite image editor software (we are using GIMP here because it’s freely available for everyone, but basically any editor with layer and TGA support can get the job done).

Let’s start with a skin for the King KAC-B, which is the main plane in the game.

Download and extract the P_PLANE_F.7z archive anywhere you choose. It contains several TGA files, always organized as:

  • mask.tga : a general mask showing the plane’s parts (for editing help purpose only, it’s not used in game)
  • lines.tga : structure lines (the separations between parts)
  • rivets.tga : plane’s rivets (you know… lots of them)
  • structlines.tga: Not used directly for skin creation. We’ll speak more about that file later.
  • internals.tga (optional): radiators, glass and landing gear parts.

Open the  mask.tga  file and save it as a GIMP workspace file (.xcf extension).


In this workspace, open lines.tga et rivets.tga as new layers (file > open as layer). For better display, set them in multiply mode:


Duplicate the mask layer, put the new one on top of the stack and set it in multiply mode.


Just above the first layer, create a new one labeled as “camo”.

As I said, this tutorial is not for artistic purpose, so assume that I choose a few colors and paint my skin with them, leading to:


We can achieve quickly an used look.

  • duplicate the rivets layer
  • on the original one, apply a 2 pixels gaussian blur: filters > blur > gaussian blur


  • set the duplicate one in divide mode with a 75% opacity:


  • duplicate the structure lines layer
  • apply a 10 pixels gaussian blur on
  • set the original one with a 20% opacity:


Open internals.tga as a new layer if it exists and move it on top of the stack:


Voilà ! Our first skin is done.

Now we need to export the image as a TGA file named P_PLANE_F_MySkin.tga, in 32 bit mode. It’s very important that the skin has an alpha layer because it contains a specular mask (i.e. light reflection mask). By default, setting this alpha layer to white or gray is sufficient, but to provide the best example, here’s what a worked out specular mask looks like:


Convert the TGA file in DDS file format with DXT5 compression using one of the many existing tools (e.g. a GIMP export plugin).

Even an online converter exists. In this one, choose DXT5 mode and click on generate mip maps.

Copy the dds file in BOMB/Media/objects/aircrafts/[MODEL]/skins, in our example it is: BOMB/Media/objects/aircrafts/P_PLANE_F/skins


Finally, launch viewer.exe, which is a minimal BOMB build to quickly start a skirmish. This program is located next to the BOMB main executable, in BOMB/bin/release.

You can switch between available skins by pressing the up and down keyboard keys.


To better understand how it works, you can find below the purpose of every color channel in every texture.


  • The R channel contains structure lines.
  • The G channel contains the baked ambiant occlusion, also used for lighting.
  • The B channel contains the canon’s lightmaps, used to light them in a convincing way while shooting.
  • The A channel contains reflective zones i.e. metallic or glass looking parts. A 255 value means no reflection at all and a 0 value means a total one.


The normal map


Used when the plane is damaged.

  • The R channel contains the minor damages.
  • The G channel contains minor + major damages.
  • The B and A channels are currently unused.

NOTE: The [MODEL] is unique for all the skins of one plane. However it’s possible to surcharge it for a particular skin by creating a new one named as the skin with the _StructLines suffix.

For example, Bilby’s skin:

has its own StructLines map:

This feature is also available for the normal map but is not currently for the damage map.

MkIVd update !

This update mainly adds a cockpit for the VB-37 Astore, and also two new single missions.

On of these (Can of Worms) is kinda “experimental”, not that it’s unfinished, but rather that we’re experimenting some ideas. Both of them deepen a little bit the backtround of BOMB, which is always cool 🙂

We remind that the game is available on Steam, but it’s now available on Humblestore too !


The changelog

New content:

  • The Astore now has it’s own cockpit.
  • New single mission “The Wreck”
  • New single mission “Can of Worms”


  • Fixed ocean rendering on DirectX 9 mode.
  •  Fixed various input related issues on linux.

Annex 1: possible characters and mood values

The list of available character names, and the moods. Using a blank string (“”) as mood will use the default one.

Radio characters

Marcel GastonMarcel
Takeshi SakaiTakeshi
Lady CrimsonLady
Belle KingBelle
Souleyman KingSouleyman
El TigreTigre
Martus PenningtonMartus
Manus PenningtonManus
Perry NunezPerry
The emirEmir
Valery G. FateValery
An Amazon pilotAmazon
A generic pilotPILOT1
Radio speakerunknown

Mission Editor #3 Staging

Disclaimer: This last part is the hardest one to understand at once as it involves a lot of scripting.

You can find the mission created following this tutorial in the game, under the name “Sample mission 3“. The complete script file can be found in :

[BOMB install folder]\Media\extmissions\Sample_mission_3\Scripts\

Now we have a mission with multiple objectives, asking the player to destroy both vehicles and aircraft. But this is still harsh. We’ll see in this tutorial how to add more staging to make the mission more interesting.

 Explosions !

Open the mission you’ve created following the previous tutorial.

First, let’s imagine the mission is about destroying an howitzer that is bombing the airbase.

As the howitzers in BOMB are not actually firing anything, we need to add some fake explosions around the airfield.

To do so, there is a special object type called “Explosion Zone”, which is created by script.

To create it, simply add these lines at the end of the “onInit()” function

self.explosionZone =createExplosionZone()

This will create an explosion zone, located around the player_spawn, with a size of 500 meters x 500 meters, making an explosion every 2 seconds.

The kind of particle effect used is “P_GROUND_EXPLOSION” and the sound set used is “SFX_EXPLOSION_“.

In the very first mission of the game “Spam”, we used this to create the flak explosions when approaching the village, but with different particles and sound:


Imagine now you want to use this on water, IE if you want to make a mission featuring a ship attacked by an howitzer, then you could do this:


Don’t worry for player safety though, the explosions created with ExplosionZones are harmless.

Explosions showing the bombing of the airfield by the howitzer.
Explosions showing the bombing of the airfield by the howitzer.


As the explosions are related to the howitzer, we need to disable them as soon as it is destroyed. Go back to the “onObjectDestroyed” function and change it’s content to :

function Sample_mission_3:onObjectDestroyed(object)
if(object:getName() == "howitzer_0") then

Now, the explosions will be disabled when howitzer is destroyed.

Adding dialogs

Things are getting better, but we still need to add some dialog to give more identity to the mission.

Go to the “onInit” function

Add this line at the end :

self.HUD:addRadioCommunication("unknown","","Scramble ! We need to destroy this howitzer or we'll be doomed !",nil)

The first parameter is the name of the character currently speaking. The second one is the “mood”, here left blank, the third parameter is the dialog itself, and the last one is the object the camera will look at  (using a flyby camera if the object is moving) during this dialog.

If left to nil, the last parameter won’t change the camera at all, leaving it to the default in game camera.

There are multiple versions of this function

  • The flyby :


You can use nil as the object parameter to automatically look at the player aircraft during this dialog.

  •  The focus:


This will change the camera so that it will look at the object 2 from the object1.

You can use nil as the first object to look automatically from the player aircraft. If both objects are set to nil, this will use the default in game camera.

  • The point of view:

addRadioCommunication("[character]","[mood]","[text]",object, exterior, yawAngle, pitchAngle)

The object parameter is still the object we’d like to look at, but now, if the targeted object is an aircraft having a cockpit, you’ll have the choice to force the point of view to cockpit or external, by setting the “external” parameter to true or false.

The two last parameters are the angles (in degres ) from which we’ll look at the object.

Let’s use the second function in this mission.

Replace the line we’ve added before by this one:

self.HUD:addRadioCommunication("unknown","","Scramble ! We need to destroy this howitzer or we'll be doomed !",nil,self.gameManager:getLevel():getObject("howitzer_0"))

Now, the camera will look from the player aircraft to the howitzer position.

Also add this one to introduce the pilot we’re playing in this mission:

self.HUD:addRadioCommunication("PILOT2","","Consider it done !",nil)

You can find the list of usable characters and moods on this page.

Here is the result:


Then we need to add a dialog when enemy flight is awaken to warn the player about this new threat.

Go again to the “onObjectDestroyed” function, and change it to:

function Sample_mission_3:onObjectDestroyed(object)
if(object:getName() == "howitzer_0") then
local leader = self.missionManager:getFlight("enemy_flight_0"):getLeaderAircraft()
self.HUD:addRadioCommunication("PILOT1","","How allowed you to destroy our guns ? You'll pay for that !",nil,leader)
self.HUD:addRadioCommunication("PILOT2","","How original...",nil)

This will introduce the enemies as they arrive in the arena.

Mission ending.

Once every vehicle and aircraft has been destroyed,  we can consider the mission as completed, and we need to tell it to the player.

As you may have found it by yourself, we need to add some code in the “onObjectiveCompleted()” function.

Add the following lines to the function body:

if(self.playerInterceptObjective~= nil and id == self.playerInterceptObjective:getId()) then
GameMenuObject.missionSuccess = true


This will display the “objective completed” text and enable the “next” button in the in game menu (the one you can access by pressing escape)

Bonus: landing objective

Bonus: You can also add a final “Return To Base” objective for players who’d want to land at the mission end, and automatically quit the mission when successfully landing.

First, we need to add the RTBObjective. Change the last piece of code to this:

if(self.playerInterceptObjective~= nil and id == self.playerInterceptObjective:getId()) then
GameMenuObject.missionSuccess = true
self.RTBObjective = self.missionManager:addRTBObjective(1,self.gameManager:getLevel():getObject("player_spawn"))

The new lines create a RTB objective telling the player to land near the player_spawn (located here on the runway, but you could also create another waypoint object located close to the hangars) .

Now, we need to track the completion of this objective to start a fade out.

In the OnObjectiveCompleted function, add this block:
if(self.RTBObjective ~=nil and id == self.RTBObjective:getId()) then
PostProcessManager.getSingleton():fadeOut(5, ColourValue(0,0,0,0))
self.endFadeStarted = true
self.endTimer  = 0
The highlighted line will ask the game to start a fade to black lasting 5 seconds

The default script will automatically quit the level 5 seconds after the fade has been started. The piece of code doing this is located in the [MissionName]:update() function, you can have a look if you want to  but it’s not necessary.
Et voilà !