We are developing a glass cockpit system for (the latest) Prepar3D. Our development team was very excited about the new possibilities with HTML5 gauges. However, during development we ran into some relatively big performance issues. More precisely, in VR, the FPS drops from around 90 to around 40 when an HTML5 gauge is active.
The project is basically a couple of layers of SVGs, animated in JS. Some are declared in the HTML file, some are created by JS. All of them is created only once in constructors, after that they are only transformed by very simple operations (like XY.style.transform = ...). There are less than a 100 svg elements in the webapp. There aren't any heavy calculations, only the value returned from VarGet() is transformed into a translation or a rotation value by multiplying it with a constant.
When it runs in a browser next to P3D, there are no visible issues at all in the sim, but when it runs inside the virtual cockpit, the frame rate drops instantly.
First, we were trying to improve the situation by changing the settings:
- Set the graphics/render distance/etc to very low.
- Texture resolution was set to smaller values in panel.cfg.
- Steam settings, like Motion Smoothing.
- Tried out the VR Tuning Guide (https://www.prepar3d.com/SDKv5/prepar3d ... guide.html).
- We have multiple glass cockpits, which meant multiple $textures on multiple objects. Now it's only one webpage on one $texture (objects' UV's are mapped next to each other).
- We are using async functions for the calculations (this doesn't really matter, since we have to update the DOM very often).
- There was an attempt to reduce the glass cockpit refresh rate to once in every 100 ms but the sim still lagged.
- The app was optimized from the beginning, for example in cases where there could be 100s of visible lines (like an altimeter scale) there are a lot less, just a bit more than the number of the visibles.
Please try it out with the Mooney with G1000 by modifying its panel.cfg like this:
Code: Select all
// gauge00=G1000!MFD_Mooney, 0,0,765,500
// gauge01=G1000!audio_panel, 779,513,97,511
// gauge02=G1000!G1000_PFD, 0,514,765,500
html_file=test.html
Code: Select all
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
body {
margin: 0;
}
svg, div {
position: absolute;
width: 100%;
height: 100%;
}
.marking-line {
stroke: white;
}
.marking-num {
fill: white;
dominant-baseline: central;
text-anchor: middle;
}
.dial {
background-color: black;
border-radius: 50%;
}
#bg {
background-color: gray;
}
#fg {
background-color: white;
opacity: 0.5;
width: 20%;
}
</style>
</head>
<body>
<div id="bg"></div>
<div id="clock-1"></div>
<div id="clock-2"></div>
<div id="clock-3"></div>
<div id="lines-1"></div>
<div id="lines-2"></div>
<div id="fg"></div>
<script>
function deg2rad(deg) {
return deg * (Math.PI / 180.0);
}
function createSvgElement(strTag, parent) {
const elem = document.createElementNS('http://www.w3.org/2000/svg', strTag);
parent.appendChild(elem);
return elem;
}
class Pos {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Clock {
showTime(ms) {
const degMs = 360 * ms / 1000;
const degSec = degMs / 12;
const degMin = degSec / 12;
this.needleMs.style.transform = "rotate(" + degMs + "deg)";
this.needleSec.style.transform = "rotate(" + degSec + "deg)";
this.needleMin.style.transform = "rotate(" + degMin + "deg)";
}
constructor(divMain, posCenter, pxRadius) {
this.divMain = divMain;
this.posCenter = posCenter;
this.pxRadius = pxRadius;
this.pxNeedleMsBase = pxRadius * 0.025;
this.pxNeedleSecBase = pxRadius * 0.05;
this.pxNeedleMinBase = pxRadius * 0.1;
this.pxNeedleAltitude = pxRadius * 0.8;
this.initDial();
this.initMarkings();
this.initNeedles();
}
initDial() {
const divDial = document.createElement("div");
this.divMain.appendChild(divDial);
divDial.style.width = (this.pxRadius * 2) + "px";
divDial.style.height = (this.pxRadius * 2) + "px";
divDial.style.left = (this.posCenter.x - this.pxRadius) + "px";
divDial.style.top = (this.posCenter.y - this.pxRadius) + "px";
divDial.classList.add("dial");
}
initMarkings() {
const svgMarkings = createSvgElement("svg", this.divMain);
for (let i = 1; i <= 12; i += 1) {
this.addMarkingLine(svgMarkings, i * -30 + 90);
this.addMarkingNum(svgMarkings, i * -30 + 90, i);
}
}
addMarkingLine(svgMarkings, degMarkingPos) {
const line = createSvgElement("line", svgMarkings);
const radMarkingPos = deg2rad(degMarkingPos);
line.setAttribute("x1", (this.posCenter.x + Math.cos(radMarkingPos) * this.pxRadius * 0.95) + "px");
line.setAttribute("y1", (this.posCenter.y - Math.sin(radMarkingPos) * this.pxRadius * 0.95) + "px");
line.setAttribute("x2", (this.posCenter.x + Math.cos(radMarkingPos) * this.pxRadius * 0.8) + "px");
line.setAttribute("y2", (this.posCenter.y - Math.sin(radMarkingPos) * this.pxRadius * 0.8) + "px");
line.classList.add("marking-line");
line.setAttribute("stroke-width", this.pxRadius * 0.02);
}
addMarkingNum(svgMarkings, degMarkingPos, nValue) {
const num = createSvgElement("text", svgMarkings);
num.textContent = nValue;
const radMarkingPos = deg2rad(degMarkingPos);
const x = this.posCenter.x + Math.cos(radMarkingPos) * this.pxRadius * 0.7;
const y = this.posCenter.y - Math.sin(radMarkingPos) * this.pxRadius * 0.7;
num.setAttribute("x", x + "px");
num.setAttribute("y", y + "px");
num.classList.add("marking-num");
num.setAttribute("font-size", this.pxRadius * 0.1);
}
initNeedles() {
const svgNeedle = createSvgElement("svg", this.divMain);
this.needleMin = createSvgElement("polygon", svgNeedle);
this.setupNeedle(this.needleMin, this.pxNeedleMinBase, this.pxNeedleAltitude);
this.needleSec = createSvgElement("polygon", svgNeedle);
this.setupNeedle(this.needleSec, this.pxNeedleSecBase, this.pxNeedleAltitude);
this.needleMs = createSvgElement("polygon", svgNeedle);
this.setupNeedle(this.needleMs, this.pxNeedleMsBase, this.pxNeedleAltitude);
}
setupNeedle(needle, pxBase, pxAltitude) {
const posA = new Pos(this.posCenter.x, this.posCenter.y - pxAltitude);
const posB = new Pos(this.posCenter.x - pxBase * 0.5, this.posCenter.y);
const posC = new Pos(this.posCenter.x + pxBase * 0.5, this.posCenter.y);
needle.setAttribute("points", posA.x + "," + posA.y + " " +
posB.x + "," + posB.y + " " +
posC.x + "," + posC.y);
needle.setAttribute("fill", "white");
needle.style.transformOrigin = this.posCenter.x + "px " + this.posCenter.y + "px";
}
}
class Lines {
move(dist) {
this.svgMain.style.transform = "translateY(" + dist + "px)";
}
constructor(divMain, n, posStart, pxDistBetween, pxLen, pxWidth) {
this.svgMain = createSvgElement("svg", divMain);
for (let i = 0; i < n; i += 1) {
const line = createSvgElement("line", this.svgMain);
line.setAttribute("x1", (posStart.x) + "px");
line.setAttribute("y1", (posStart.y + i * pxDistBetween) + "px");
line.setAttribute("x2", (posStart.x + pxLen) + "px");
line.setAttribute("y2", (posStart.y + i * pxDistBetween) + "px");
line.classList.add("marking-line");
line.setAttribute("stroke-width", pxWidth);
}
}
}
const clock1 = new Clock(
document.getElementById("clock-1"),
new Pos(300, 300),
256
);
const clock2 = new Clock(
document.getElementById("clock-2"),
new Pos(100, 100),
128
);
const clock3 = new Clock(
document.getElementById("clock-3"),
new Pos(300, 500),
128
);
const lines1 = new Lines(
document.getElementById("lines-1"),
10,
new Pos(32, 128),
32,
64,
4
);
const lines2 = new Lines(
document.getElementById("lines-2"),
20,
new Pos(512, 128),
32,
64,
4
);
function loop(timestamp) {
clock1.showTime(timestamp);
clock2.showTime(timestamp);
clock3.showTime(timestamp);
lines1.move(Math.cos(timestamp / 100) * 100);
lines2.move(Math.cos(timestamp / 100) * 100);
window.requestAnimationFrame(loop);
}
window.requestAnimationFrame(loop);
</script>
</body>
</html>
What else could we do to improve the performance? Is there anything wrong (from Prepar3D's point of view) with our approach?
Thank you!