Howto:Dynamic Liveries via Canvas

From FlightGear wiki
Jump to: navigation, search


Objective

Demonstrate how to modify/animate liveries using Nasal/Canvas.

Cquote1.png The code is already in place for the "dirtying" effect - it's a fairly simple linear introduction of the dirt texture, becoming less transparent the more the aircraft flies without being washed. Being washed also includes rain - continued exposure to precipitation will "wash away" dirt by making the dirt layer more transparent according to the heaviness of the weather. It's been tried on the APU smudge, and whilst the values will need to be tweaked, it already works pretty much to my expectations and satisfaction.
— Algernon (Wed Sep 17). Re: Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png But in the long-term, I would prefer turning this into a Nasal class that can manage multiple Canvases per aircraft, so that we can also re-implement the existing livery system accordingly, so that it stops interfering with canvas. Which would also mean that people can reuse the same class for placing bullet holes or doing other fancy things (e.g. Immatriculation)
— Hooray (Wed Sep 17). Re: Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png

Proof of Concept: Exhaust dirt

by Algernon (talk) 17:09, 17 September 2014 (UTC)

To document the implementation on a FlightGear aircraft from the aircraft developer's point of view, I've chosen one of my pet projects, the Eurofighter EF2000 V2.0 which will be released in early 2015. The intention is to use Canvas to allow multiple textures per livery, in this case those textures being the original livery paintwork and another, alpha-transparent texture consisting of dirt streaks specifically added to the livery paintwork to match with vents, seams and other sources of grimy build-ups. The intention is to allow dynamic management of the transparency of the dirt layer according to time and exposure to various elements, maintaining compatibility with the standard livery switching method and working similarly whether Rembrandt is enabled or not.


This section will be updated as the implementation continues.

Model Work

To start with, I cloned the EF2000's model and made a copy called "EF2000-canvas.ac", cloned the model XML file in the same way, and changed the paths appropriately to create a completely separate model for this experiment. The model objects which will be subject to the dynamic effects - effectively, the painted surfaces - were isolated from items like the canopy glass, gear and engines and given the same material, named CanvasPaint and consisting of an image texture: a livery in Air Superiority Grey but without the dirt layer already included. My first experiment will be to create a canvas and try to apply it to the mesh.


Canvas Code

From the code snippets above and Howto:Using raster images and nested canvases, I've put together my first attempt at some Canvas code. It appears that the canvas is the right size, and both .jpg and .png textures are loaded. But the placement line did not seem to make any difference to the texture on the named model object ("Fuselage"). When the native XML/Nasal livery object was commented out, the image was successfully displayed on the fuselage, but incorrectly sized and apparently without UV mapping (see image below).

# Create a standalone Canvas (not attached to any GUI dialog/aircraft etc) 
var myCanvas = canvas.new({
  "name": "Livery Test",   # The name is optional but allow for easier identification
  "size": [2048, 2048], # Size of the underlying texture (should be a power of 2, required) [Resolution]
  "view": [2048, 2048],  # Virtual resolution (Defines the coordinate system of the canvas [Dimensions]
                        # which will be stretched the size of the texture, required)
  "mipmapping": 1       # Enable mipmapping (optional)
});

# create our top-level/root group that contains all other Canvas elements
var root = myCanvas.createGroup();

# Add a placement by replacing the textured face "Fuselage" in the 3D model
# This replaces the texture on the aircraft and attaches the Canvas texture
myCanvas.addPlacement({"node": "Fuselage"});

# hash with all images added
var layers = {};
# texture path
var path="Aircraft/EF2000/Models/";
foreach(var image; ['EF2000.png','EF2000-dirt.png']) {
if(contains(layers, image)) print("Warning: replacing texture (added twice): ", image);
# Put a raster image into the canvas and save the image in a hash: layers['EF2000.png].hide();
layers[image] = root.createChild("image")
     .setFile( path~image )
     .setSize(2048,2048)
}

# object-oriented settimer() replacement
# see: http://wiki.flightgear.org/List_of_Nasal_extension_functions#maketimer.28.29_.282.11.2B.29 
var timer_hide = maketimer(3.0, func() {
  layers['EF2000-dirt.png'].hide();
});

var timer_show = maketimer(5.0, func() {
  layers['EF2000-dirt.png'].show();
});

# start those two timers to hide/show the dirt texture with 2 second delays
timer_hide.start();
timer_show.start();

# Create a Canvas dialog window to hold the canvas and show that it's working
# the Canvas is now standalone, i.e. continues to live once the dialog is closed!
var window = canvas.Window.new([512,512],"dialog");
window.setCanvas(myCanvas);

Here's another slightly-restructured version (untested), to better encapsulate the concept of a "managed texture" so that the code can be reused for other aircraft/purposes, e.g. for placing bullet holes etc:

var ManagedTexture = {

# this create a new TextureManager object
new: func(name, size, path) {
	# create a temporary object that inherits from TextureManager
	var m = {parents:[ManagedTexture]};

	# Create a standalone Canvas (not attached to any GUI dialog/aircraft etc) 
	m.canvas = canvas.new({
		"name": name,	# The name is optional but allow for easier identification
		"size": [size[0], size[1]],	# Size of the underlying texture (should be a power of 2, required) [Resolution]
		"view": [size[0], size[1]],	# Virtual resolution (Defines the coordinate system of the canvas [Dimensions]
                        			# which will be stretched the size of the texture, required)
		"mipmapping": 1       		# Enable mipmapping (optional)
});

# create our top-level/root group that contains all other Canvas elements
m.root = m.canvas.createGroup();

# hash with all layers/images added
m.layers = {};
m.basepath = path;

# return the new object to the caller
return m;
},

replaceTexture: func(name) {
# Add a placement by replacing the textured face specified (name) in the 3D model
# This replaces the texture on the aircraft and attaches the Canvas texture
m.canvas.addPlacement({"node": name});
},

addDynamicLayer: func(filename, callback=nil) {
if(contains(layers, image)) print("Warning: replacing texture (added twice): ", image);
# Put a raster image into the canvas and save the image in a hash: layers['EF2000.png].hide();
m.layers[image] = root.createChild("image")
     .setFile( m.basepath~image )
     .setSize(2048,2048)
return m.layers[image];
},
}; # of ManagedTexture


# now, create a new managed texture, specifying the path to use for texture lookups
var ExhaustDirt = ManagedTexture.new(	name:"ExhaustDirt", 
					size:[2048,2048], 
					path:'Aircraft/EF2000/Models/' );

ExhaustDirt.replaceTexture('Fuselage');

# Create a Canvas dialog window to hold the canvas and show that it's working
# the Canvas is now standalone, i.e. continues to live once the dialog is closed!
var window = canvas.Window.new([512,512],"dialog");
window.setCanvas(ExhaustDirt.canvas);

foreach(var layer; [	{file:'EF2000.png'},
			{file:'EF2000-dirt.png'} ]) {
	ExhaustDirt.addDynamicLayer(layer.file);
}


var timer_hide = maketimer(3.0, func() {
  ExhaustDirt.layers['EF2000-dirt.png'].hide();
});

var timer_show = maketimer(5.0, func() {
  ExhaustDirt.layers['EF2000-dirt.png'].show();
});

# start those two timers to hide/show the dirt texture with 2 second delays
timer_hide.start();
timer_show.start();

Original Example

Note  This assumes that you're using the ogel aircraft and that the face texture can be looked up using the name Head.

The map shown here is just an example, this could obviously be anything, including raster/svg images, custom OpenVG/ShivaVG paths, osgText etc. To implement a dirt texture, you'd want to use the corresponding texture ID and then overlay a static dirt texture using some kind of random scheme, e.g. by adding texture maps/sub regions to the exhaust area. See Canvas Image for details. In its current form, this will simply apply the texture mapping used in the 3D mode "as is". In other words, all texture-mapped faces should be accessible from Nasal/Canvas and can be modified/animated. In comparison to a pure shader-based approach this is likely to have a little more overhead, while not requiring effects/shaders (but RTT/FBOs due to Canvas) - also, this approach is agnostic to the underlying rendering pipeline, i.e. should also work for Rembrandt aircraft, without having to be customized.

A screen shot showing the ogel aircraft whose livery is dynamically changed using the Canvas system - in this case, we're simply rendering a MapStructure map onto the face of the pilot using ~10 lines of Nasal/Canvas code.


# create a new window, dimensions are 320 x 160, using the dialog decoration (i.e. titlebar)
var window = canvas.Window.new([320,160],"dialog");

# adding a canvas to the new window and setting up background colors/transparency
var myCanvas = window.createCanvas();

# creating the top-level/root group which will contain all other elements/group
var root = myCanvas.createGroup();

# add a new placement using a texture identifier (see $FG_ROOT/ogel/Models/SinglePiston.ac or existing liveries) 
myCanvas.addPlacement({"node": "Head"});


# now, let's add some content to our canvas (this could be really anything)
# animations are handled via timers/listeners (not shown here, hidden in the depths of MapStructure)
var TestMap = root.createChild("map");
TestMap.setController("Aircraft position");
TestMap.setRange(25); 
 
TestMap.setTranslation(    myCanvas.get("view[0]")/2,
                           myCanvas.get("view[1]")/2
                        );
var r = func(name,vis=1,zindex=nil) return caller(0)[0];

foreach(var type; [r('APT'), r('VOR') ] )
 TestMap.addLayer(factory: canvas.SymbolLayer, type_arg: type.name, visible: type.vis, priority: type.zindex,);

Background

Cquote1.png Does anyone know if it's possible for FG to work with a texture which consists of two images, and manipulate those images seperately? The application here is airframe dirt - I'm trying to find out if I can have one texture for the aircraft livery, and another for accumulated dirt, which would gradually become less transparent as the airframe hours increase. I am aware of the questionable advisability of using alpha channels excessively in FG, this is just an experiment at the moment.
— Algernon (Mon Sep 01). Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png there are probably several ways to do something like this, effects/shaders and even Canvas can be used to combine textures and adjusting alpha via Nasal.

So it's mainly a matter of your exact requirements and what kind of solution you'd prefer.


— Hooray (Mon Sep 01). Re: Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png The aircraft gets dirtier with more flights hours. Some code to do this is already in place, and tested on the other dirt application, the APU exhaust blackening on the port wing root. This is a localised and distinctive stain which you see on most Typhoons, so there I have just used a duplicate of the vertices and faces in that locality, placed a little away from the airframe, and used an alpha texture and the material transparency animation. It works a charm, and doesn't seem to have problems with alpha flashing. For the airframe, this probably isn't a viable approach. Part of the textures set for the airframe has a transparent dirt layer which has been tailored to the model to feature streaks from particular vents or around extensions like underwing pylons, so it would be desirable to be able to use this. This additional layer must remain unchanged through livery switches, and then be gradually introduced over time according to the property updated by the same Nasal function that dirties the APU exhaust while it's running (the airframe starts to gets dirtier over 80kts or something for the all over dirt). And if at all possible, it should be Rembrandt compatible.
— Algernon (Wed Sep 03). Re: Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png The idea of a canvas airframe texture is a fascinating one, and might be considered almost heretical over at FGUK! ;) But although I'm finding Canvas a bit inaccessible at the moment, if I can achieve combined textures with controllable transparency that works equally well with or without Rembrandt, I'd certainly be interested in using this idea as an entry point.
— Algernon (Wed Sep 03). Re: Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png Canvas should work fairly well actually - Tom added support for pretty much "arbitrary" placements, so that canvas textures can be placed anywhere on the aircraft, but also in the scenery (and obviously also in GUI dialogs/windows). We once toyed with the idea of also making effects and shaders available to Canvas - Tom also mentioned this once on the devel list, and we have code doing this for the whole canvas - even though Tom said that it would be better to support this "per-element". Obviously, that would bring a ton of flexibility to Canvas-based efforts like yours.

— Hooray (Wed Sep 03). Re: Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png most code snippets recently added to the wiki are about different use-cases - i.e. not specifically about airframe textures/liveries, but then again - the Canvas doesn't care at all where you use a certain texture - in other words, you could prototype the whole thing using a GUI dialog and once you are satisifed with the outcode you would use another placement to show the texture elsewhere. The good thing about your particular use case is that it low-bandwidth, i.e. the texture probably doesn't need to be updated per frame - so performance issues due to Nasal/Canvas overhead should not be a factor.

I think you could certainly prototype the whole thing fairly quickly - and if/once effects and shaders become available to Canvas, your approach could be updated accordingly.


— Hooray (Wed Sep 03). Re: Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png For starters, you could look at this: Howto:Using_raster_images_and_nested_canvases#Example:_Showing_a_Splash_Screen


This will open an existing splash screen image and show it in a Canvas dialog.
You can copy/paste the code to load a few different images, to learn more about Canvas image handling, see: Canvas_Image

Note that you can also use/apply sub-textures and overlay/transform those arbitrarily - i.e. a lot of fancy stuff without having to know anything about effects or shaders  :)

You can also overlay individual images using the "z-index" attribute: Canvas_Element#z-index
And then, you can basically do pretty much anything using the core Nasal APIs documented at: Canvas_Nasal_API
To adjust transparency, you'll want to use the alpha channel.

To animate the livery/texture, you'd basically use a timer/listener to change the texture's canvas property tree - i.e. by hiding/showing some sub-texture, or transforming groups/elements.


— Hooray (Wed Sep 03). Re: Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png the whole method came up as an idea - this isn't currently being used anywhere as far as I know. But it would seem that we're mainly missing people who want to experiment a bit.

Canvas was never designed for creating/modifying "liveries" obviously - but canvas is very good at one thing: creating/modifying arbitrary textures at run-time and adjusting pretty much arbitrary parameters by setting a few properties.

Thus, I'd consider Canvas more like a "toolbox" whose tools may not be directly designed for this particular purpose, but which are sufficiently generic and which can still be used "creatively" - e.g. there are some things that are only possible per-canvas, and not per "element" (group, image, paths, text, map) - in such cases, you typically need to use a workaround by using multiple canvas and referencing them via the canvas:/// markup as another raster image. We used this method a few times to implement "workarounds" - so in general, the implementation is pretty generic and even "recursive".

We don't currently have any support for effects/shaders in 3.2 (or git) - but whenever you add a canvas-based texture to any of your models, you're adding a layer of indirection - so while some static textures may not be easily changed at run-time in some C++ subsystems, Canvas is all about managing dynamic textures - which in turn may consist of static textures (or variations of those).


— Hooray (Wed Sep 03). Re: Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png


Cquote1.png I don't know if we have placement-related docs anywhere, but you could take a look at any aircraft using a dynamic Canvas-based texture, the c172p-canvas should be a fairly lighweight example compared to the 777/747 probably
— Hooray (Wed Sep 03). Re: Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png
Cquote1.png I guess with some more feedback/brainstorming, we can probably explore and see what's possible already, and what might be missing to support this particular "livery" use-case in the most generic fashion. Like I said, support for shaders/effects is relatively straightforward to add - Tom and I both have experimental code doing this kind of thing (per canvas only here) -and this would be useful for other purposes, too (think FLIR/night vision support etc).
— Hooray (Wed Sep 03). Re: Two Images to a Texture.
(powered by Instant-Cquotes)
Cquote2.png