Skip to content

Commit 10e70ee

Browse files
committed
Write custom shaders for circular gauges and arc gauges
Animating the Shape-based gauges is too expensive. So reimplement them as single-pass shaders.
1 parent fa63174 commit 10e70ee

File tree

13 files changed

+484
-128
lines changed

13 files changed

+484
-128
lines changed

CMakeLists.txt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ if("${CMAKE_SYSTEM_NAME}" STREQUAL "Emscripten")
4949
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
5050
add_compile_definitions(VENUS_WEBASSEMBLY_BUILD)
5151
add_compile_definitions(MQTT_WEBSOCKETS_ENABLED)
52-
find_package(Qt6 6.5.2 COMPONENTS Core Qml Quick Svg Xml LinguistTools Mqtt WebSockets REQUIRED) # require at least qt 6.5.2 for qt_add_qml_module to work properly
52+
find_package(Qt6 6.5.2 COMPONENTS Core Qml Quick Svg Xml LinguistTools Mqtt WebSockets ShaderTools REQUIRED) # require at least qt 6.5.2 for qt_add_qml_module to work properly
5353
else()
5454
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
5555
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
5656
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
57-
find_package(Qt6 6.5.2 COMPONENTS Core Qml Quick Svg Xml DBus LinguistTools Mqtt REQUIRED) # require at least qt 6.5.2 for qt_add_qml_module to work properly
57+
find_package(Qt6 6.5.2 COMPONENTS Core Qml Quick Svg Xml DBus LinguistTools Mqtt ShaderTools REQUIRED) # require at least qt 6.5.2 for qt_add_qml_module to work properly
5858
endif()
5959

6060
# This has to go after 'find_package(Qt6 COMPONENTS Core)', and before 'qt_add_qml_module(... QML_FILES ${VENUS_QML_MODULE_SOURCES})'
@@ -121,6 +121,8 @@ set (VENUS_QML_MODULE_SOURCES
121121
components/RadioButtonControlValue.qml
122122
components/SegmentedButtonRow.qml
123123
components/SeparatorBar.qml
124+
components/ShaderCircularGauge.qml
125+
components/ShaderProgressArc.qml
124126
components/ShinyProgressArc.qml
125127
components/SideGauge.qml
126128
components/SolarDetailBox.qml
@@ -454,6 +456,17 @@ qt_add_qml_module(VenusQMLModule
454456
OUTPUT_DIRECTORY Victron/VenusOS
455457
QML_FILES ${VENUS_QML_MODULE_SOURCES}
456458
)
459+
460+
qt6_add_shaders(VenusQMLModule "shaders"
461+
BATCHABLE
462+
PRECOMPILE
463+
OPTIMIZED
464+
PREFIX
465+
"/qt/qml/Victron/VenusOS/components"
466+
FILES
467+
"shaders/circulargauge.frag"
468+
"shaders/progressarc.frag"
469+
)
457470
# end VENUS_QML_MODULE
458471

459472
# Dbus_QML_MODULE

components/ArcGauge.qml

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,44 +8,59 @@ import QtQuick.Window
88
import Victron.VenusOS
99
import Victron.Gauges
1010

11-
// A progress gauge running an on arc, where 0° is at the top, and positive is clockwise
11+
// A progress gauge running an on arc, where 0° is at the top.
1212
Item {
1313
id: gauge
1414

15-
property alias value: arc.value
15+
implicitWidth: arc.implicitWidth
16+
implicitHeight: arc.implicitHeight
1617

1718
property int valueType: VenusOS.Gauges_ValueType_FallingPercentage
19+
property int alignment: Qt.AlignTop | Qt.AlignLeft
20+
property int direction: ((alignment & Qt.AlignLeft && alignment & Qt.AlignVCenter)
21+
|| (alignment & Qt.AlignLeft && alignment & Qt.AlignTop)
22+
|| (alignment & Qt.AlignRight && alignment & Qt.AlignBottom))
23+
? PathArc.Clockwise : PathArc.Counterclockwise
24+
25+
property alias arcWidth: arc.width
26+
property alias arcHeight: arc.height
27+
property alias arcX: arc.x
28+
property alias arcY: arc.y
29+
property alias value: arc.value
1830
property alias startAngle: arc.startAngle
1931
property alias endAngle: arc.endAngle
2032
property alias radius: arc.radius
21-
property alias useLargeArc: arc.useLargeArc
2233
property alias strokeWidth: arc.strokeWidth
23-
property alias direction: arc.direction
34+
property alias progressColor: arc.progressColor
35+
property alias remainderColor: arc.remainderColor
2436
property alias animationEnabled: arc.animationEnabled
25-
property int alignment: Qt.AlignLeft
26-
property var arcX
27-
property var arcY
28-
29-
Item {
30-
id: antialiased
31-
anchors.fill: parent
32-
33-
// Antialiasing without requiring multisample framebuffers.
34-
layer.enabled: true
35-
layer.smooth: true
36-
layer.textureSize: Qt.size(antialiased.width*2, antialiased.height*2)
37-
38-
ProgressArc {
39-
id: arc
40-
41-
readonly property int status: Gauges.getValueStatus(gauge.value, gauge.valueType)
42-
43-
width: radius*2
44-
height: width
45-
x: arcX !== undefined ? arcX : (gauge.alignment & Qt.AlignRight ? (gauge.width - 2*radius) : 0)
46-
y: arcY !== undefined ? arcY : ((gauge.height - height) / 2)
47-
progressColor: Theme.statusColorValue(status)
48-
remainderColor: Theme.statusColorValue(status, true)
49-
}
37+
38+
readonly property int _status: Gauges.getValueStatus(value, valueType)
39+
readonly property real _maxAngle: alignment & Qt.AlignVCenter
40+
? Theme.geometry.briefPage.largeEdgeGauge.maxAngle
41+
: Theme.geometry.briefPage.smallEdgeGauge.maxAngle
42+
43+
ShaderProgressArc {
44+
id: arc
45+
46+
implicitWidth: Theme.geometry.briefPage.edgeGauge.width
47+
implicitHeight: (alignment & Qt.AlignVCenter)
48+
? Theme.geometry.briefPage.largeEdgeGauge.height
49+
: Theme.geometry.briefPage.smallEdgeGauge.height
50+
51+
x: (alignment & Qt.AlignLeft) ? 0 : (parent.width - width)
52+
y: (alignment & Qt.AlignTop) ? (parent.height - height)
53+
: (alignment & Qt.AlignBottom) ? 0
54+
: (parent.height - height)/2
55+
56+
startAngle: (alignment & Qt.AlignVCenter) ? 270 - _maxAngle/2
57+
: (alignment & Qt.AlignTop) ? 270
58+
: 90
59+
endAngle: startAngle + _maxAngle
60+
radius: Theme.geometry.briefPage.edgeGauge.radius - 2*strokeWidth
61+
strokeWidth: Theme.geometry.arc.strokeWidth
62+
progressColor: Theme.statusColorValue(_status)
63+
remainderColor: Theme.statusColorValue(_status, true)
64+
clockwise: direction === PathArc.Clockwise
5065
}
5166
}

components/CircularMultiGauge.qml

Lines changed: 36 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -21,56 +21,46 @@ Item {
2121
// Step change in the size of the bounding boxes of successive gauges
2222
readonly property real _stepSize: 2 * (strokeWidth + Theme.geometry.circularMultiGauge.spacing)
2323

24-
Item {
25-
id: antialiased
26-
anchors.fill: parent
2724

28-
// Antialiasing without requiring multisample framebuffers.
29-
layer.enabled: true
30-
layer.smooth: true
31-
layer.textureSize: Qt.size(antialiased.width*2, antialiased.height*2)
25+
Repeater {
26+
id: arcRepeater
27+
width: parent.width
28+
delegate: Loader {
29+
id: loader
30+
property int gaugeStatus: Gauges.getValueStatus(model.value, model.valueType)
31+
property real value: model.value
32+
width: parent.width - (index*_stepSize)
33+
height: width
34+
anchors.centerIn: parent
35+
visible: model.index < Theme.geometry.briefPage.centerGauge.maximumGaugeCount
36+
sourceComponent: model.tankType === VenusOS.Tank_Type_Battery ? shinyProgressArc : progressArc
37+
onStatusChanged: if (status === Loader.Error) console.warn("Unable to load circular multi gauge progress arc:", errorString())
3238

33-
Repeater {
34-
id: arcRepeater
35-
width: parent.width
36-
delegate: Loader {
37-
id: loader
38-
property int gaugeStatus: Gauges.getValueStatus(model.value, model.valueType)
39-
property real value: model.value
40-
width: parent.width - (index*_stepSize)
41-
height: width
42-
anchors.centerIn: parent
43-
visible: model.index < Theme.geometry.briefPage.centerGauge.maximumGaugeCount
44-
sourceComponent: model.tankType === VenusOS.Tank_Type_Battery ? shinyProgressArc : progressArc
45-
onStatusChanged: if (status === Loader.Error) console.warn("Unable to load circular multi gauge progress arc:", errorString())
46-
47-
Component {
48-
id: shinyProgressArc
49-
ShinyProgressArc {
50-
radius: width/2
51-
startAngle: 0
52-
endAngle: 270
53-
value: loader.value
54-
progressColor: Theme.statusColorValue(loader.gaugeStatus)
55-
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
56-
strokeWidth: gauges.strokeWidth
57-
animationEnabled: gauges.animationEnabled
58-
shineAnimationEnabled: Global.batteries.system.mode === VenusOS.Battery_Mode_Charging
59-
}
39+
Component {
40+
id: shinyProgressArc
41+
ShaderCircularGauge {
42+
startAngle: 0
43+
endAngle: 270
44+
value: loader.value
45+
progressColor: Theme.statusColorValue(loader.gaugeStatus)
46+
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
47+
strokeWidth: gauges.strokeWidth
48+
animationEnabled: gauges.animationEnabled
49+
shineAnimationEnabled: Global.batteries.system.mode === VenusOS.Battery_Mode_Charging
6050
}
51+
}
6152

62-
Component {
63-
id: progressArc
64-
ProgressArc {
65-
radius: width/2
66-
startAngle: 0
67-
endAngle: 270
68-
value: loader.value
69-
progressColor: Theme.statusColorValue(loader.gaugeStatus)
70-
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
71-
strokeWidth: gauges.strokeWidth
72-
animationEnabled: gauges.animationEnabled
73-
}
53+
Component {
54+
id: progressArc
55+
ShaderCircularGauge {
56+
startAngle: 0
57+
endAngle: 270
58+
value: loader.value
59+
progressColor: Theme.statusColorValue(loader.gaugeStatus)
60+
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
61+
strokeWidth: gauges.strokeWidth
62+
animationEnabled: gauges.animationEnabled
63+
shineAnimationEnabled: false
7464
}
7565
}
7666
}

components/CircularSingleGauge.qml

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,18 @@ Item {
2222
property alias animationEnabled: arc.animationEnabled
2323
property alias shineAnimationEnabled: arc.shineAnimationEnabled
2424

25-
Item {
26-
id: antialiased
27-
anchors.fill: parent
28-
29-
// Antialiasing without requiring multisample framebuffers.
30-
layer.enabled: true
31-
layer.smooth: true
32-
layer.textureSize: Qt.size(antialiased.width*2, antialiased.height*2)
33-
34-
// The single circular gauge is always the battery gauge :. shiny.
35-
ShinyProgressArc {
36-
id: arc
37-
38-
width: gauges.width
39-
height: width
40-
anchors.centerIn: parent
41-
radius: width/2
42-
startAngle: 0
43-
endAngle: 359 // "Note that a single PathArc cannot be used to specify a circle."
44-
progressColor: Theme.statusColorValue(gauges.status)
45-
remainderColor: Theme.statusColorValue(gauges.status, true)
46-
strokeWidth: Theme.geometry.circularSingularGauge.strokeWidth
47-
}
25+
// The single circular gauge is always the battery gauge :. shiny.
26+
ShaderCircularGauge {
27+
id: arc
28+
29+
width: gauges.width
30+
height: width
31+
anchors.centerIn: parent
32+
startAngle: 0
33+
endAngle: 359 // "Note that a single PathArc cannot be used to specify a circle."
34+
progressColor: Theme.statusColorValue(gauges.status)
35+
remainderColor: Theme.statusColorValue(gauges.status, true)
36+
strokeWidth: Theme.geometry.circularSingularGauge.strokeWidth
4837
}
4938

5039
Column {

components/ShaderCircularGauge.qml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import QtQuick
2+
import Victron.VenusOS
3+
4+
Item {
5+
id: gauge
6+
7+
property real value // from 0.0 to 100.0
8+
property color remainderColor: "gray"
9+
property color progressColor: "blue"
10+
property color shineColor: Qt.rgba(1.0, 1.0, 1.0, 0.80)
11+
property real startAngle: 0
12+
property real endAngle: 270
13+
property real progressAngle: startAngle + ((endAngle - startAngle) * Math.min(Math.max(gauge.value, 0.0), 100.0) / 100.0)
14+
property real strokeWidth: width/25
15+
property real radius: (width - strokeWidth - smoothing)/2
16+
property real smoothing: 1 // how many pixels of antialiasing to apply.
17+
property bool clockwise: true
18+
property bool shineAnimationEnabled: true
19+
property bool animationEnabled: true
20+
21+
property real _normalizedRadiansFactor: (Math.PI/180) / (2*Math.PI)
22+
property real _maxRadius: width/2
23+
24+
onProgressAngleChanged: {
25+
if (!progressAnimator.running) {
26+
progressAnimator.from = shader.progressAngle
27+
progressAnimator.to = gauge.progressAngle * _normalizedRadiansFactor
28+
progressAnimator.start()
29+
}
30+
}
31+
32+
Timer {
33+
running: gauge.shineAnimationEnabled
34+
interval: Theme.animation.briefPage.centerGauge.shine.duration + (Theme.animation.briefPage.centerGauge.shine.duration * Theme.animation.briefPage.centerGauge.shine.pauseRatio)
35+
repeat: true
36+
onTriggered: {
37+
shineAnimator.duration = (gauge.progressAngle / gauge.endAngle) * Theme.animation.briefPage.centerGauge.shine.duration
38+
shineAnimator.from = 0.0
39+
shineAnimator.to = Math.min(gauge.endAngle, gauge.progressAngle+5) * _normalizedRadiansFactor
40+
shineAnimator.start()
41+
}
42+
}
43+
44+
ShaderEffect {
45+
id: shader
46+
anchors.fill: parent
47+
fragmentShader: "shaders/circulargauge.frag.qsb"
48+
49+
property color remainderColor: gauge.remainderColor
50+
property color progressColor: gauge.progressColor
51+
property color shineColor: gauge.shineColor
52+
// transform angles to radians and then normalize
53+
property real startAngle: gauge.startAngle * gauge._normalizedRadiansFactor
54+
property real endAngle: gauge.endAngle * gauge._normalizedRadiansFactor
55+
property real progressAngle: -1.0
56+
property real shineAngle: -1.0
57+
// transform radii to uv coords
58+
property real innerRadius: (gauge.radius - (gauge.strokeWidth/2)) / (gauge._maxRadius)
59+
property real radius: gauge.radius / (gauge._maxRadius)
60+
property real outerRadius: (gauge.radius + (gauge.strokeWidth/2)) / (gauge._maxRadius)
61+
// transform smoothing pixels to uv distance
62+
property real smoothing: gauge.smoothing / gauge._maxRadius
63+
property real clockwise: gauge.clockwise ? 1.0 : 0.0
64+
65+
UniformAnimator {
66+
id: progressAnimator
67+
target: shader
68+
uniform: "progressAngle"
69+
duration: Theme.animation.progressArc.duration
70+
easing.type: Easing.InOutQuad
71+
}
72+
73+
UniformAnimator {
74+
id: shineAnimator
75+
target: shader
76+
uniform: "shineAngle"
77+
easing.type: Easing.InQuad
78+
onRunningChanged: if (!running) shader.shineAngle = -1.0
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)