Howto:Dynamic Liveries via Canvas
Caution Depending on your livery size this could use many MBs of CPU+GPU ram. Eg at 4096x4096, you’d be looking at 64Mb (input texture decompressed into RAM) CPU + 64 MB (input texture uploaded to GPU) + 128MB (Canvas texture + FBO depth buffer) . If you’re using 8192x8192, you’d be burning 3/4 of GB just on the livery Canvas. (Although ‘only’ 512MG on the GPU side, maybe it fits…)[1] |
The FlightGear forum has a subforum related to: Canvas |
Objective
Demonstrate how to modify/animate liveries using Nasal/Canvas.
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)
|
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. |
# 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
For starters, you could look at this: Howto:Using_raster_images_and_nested_canvases#Example:_Showing_a_Splash_Screen
|