Skip to content

Commit

Permalink
Write custom shaders for circular gauges and arc gauges
Browse files Browse the repository at this point in the history
Animating the Shape-based gauges is too expensive.
So reimplement them as single-pass shaders.
  • Loading branch information
chriadam committed Dec 14, 2023
1 parent fa63174 commit 10e70ee
Show file tree
Hide file tree
Showing 13 changed files with 484 additions and 128 deletions.
17 changes: 15 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ if("${CMAKE_SYSTEM_NAME}" STREQUAL "Emscripten")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
add_compile_definitions(VENUS_WEBASSEMBLY_BUILD)
add_compile_definitions(MQTT_WEBSOCKETS_ENABLED)
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
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
else()
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
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
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
endif()

# This has to go after 'find_package(Qt6 COMPONENTS Core)', and before 'qt_add_qml_module(... QML_FILES ${VENUS_QML_MODULE_SOURCES})'
Expand Down Expand Up @@ -121,6 +121,8 @@ set (VENUS_QML_MODULE_SOURCES
components/RadioButtonControlValue.qml
components/SegmentedButtonRow.qml
components/SeparatorBar.qml
components/ShaderCircularGauge.qml
components/ShaderProgressArc.qml
components/ShinyProgressArc.qml
components/SideGauge.qml
components/SolarDetailBox.qml
Expand Down Expand Up @@ -454,6 +456,17 @@ qt_add_qml_module(VenusQMLModule
OUTPUT_DIRECTORY Victron/VenusOS
QML_FILES ${VENUS_QML_MODULE_SOURCES}
)

qt6_add_shaders(VenusQMLModule "shaders"
BATCHABLE
PRECOMPILE
OPTIMIZED
PREFIX
"/qt/qml/Victron/VenusOS/components"
FILES
"shaders/circulargauge.frag"
"shaders/progressarc.frag"
)
# end VENUS_QML_MODULE

# Dbus_QML_MODULE
Expand Down
73 changes: 44 additions & 29 deletions components/ArcGauge.qml
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,59 @@ import QtQuick.Window
import Victron.VenusOS
import Victron.Gauges

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

property alias value: arc.value
implicitWidth: arc.implicitWidth
implicitHeight: arc.implicitHeight

property int valueType: VenusOS.Gauges_ValueType_FallingPercentage
property int alignment: Qt.AlignTop | Qt.AlignLeft
property int direction: ((alignment & Qt.AlignLeft && alignment & Qt.AlignVCenter)
|| (alignment & Qt.AlignLeft && alignment & Qt.AlignTop)
|| (alignment & Qt.AlignRight && alignment & Qt.AlignBottom))
? PathArc.Clockwise : PathArc.Counterclockwise

property alias arcWidth: arc.width
property alias arcHeight: arc.height
property alias arcX: arc.x
property alias arcY: arc.y
property alias value: arc.value
property alias startAngle: arc.startAngle
property alias endAngle: arc.endAngle
property alias radius: arc.radius
property alias useLargeArc: arc.useLargeArc
property alias strokeWidth: arc.strokeWidth
property alias direction: arc.direction
property alias progressColor: arc.progressColor
property alias remainderColor: arc.remainderColor
property alias animationEnabled: arc.animationEnabled
property int alignment: Qt.AlignLeft
property var arcX
property var arcY

Item {
id: antialiased
anchors.fill: parent

// Antialiasing without requiring multisample framebuffers.
layer.enabled: true
layer.smooth: true
layer.textureSize: Qt.size(antialiased.width*2, antialiased.height*2)

ProgressArc {
id: arc

readonly property int status: Gauges.getValueStatus(gauge.value, gauge.valueType)

width: radius*2
height: width
x: arcX !== undefined ? arcX : (gauge.alignment & Qt.AlignRight ? (gauge.width - 2*radius) : 0)
y: arcY !== undefined ? arcY : ((gauge.height - height) / 2)
progressColor: Theme.statusColorValue(status)
remainderColor: Theme.statusColorValue(status, true)
}

readonly property int _status: Gauges.getValueStatus(value, valueType)
readonly property real _maxAngle: alignment & Qt.AlignVCenter
? Theme.geometry.briefPage.largeEdgeGauge.maxAngle
: Theme.geometry.briefPage.smallEdgeGauge.maxAngle

ShaderProgressArc {
id: arc

implicitWidth: Theme.geometry.briefPage.edgeGauge.width
implicitHeight: (alignment & Qt.AlignVCenter)
? Theme.geometry.briefPage.largeEdgeGauge.height
: Theme.geometry.briefPage.smallEdgeGauge.height

x: (alignment & Qt.AlignLeft) ? 0 : (parent.width - width)
y: (alignment & Qt.AlignTop) ? (parent.height - height)
: (alignment & Qt.AlignBottom) ? 0
: (parent.height - height)/2

startAngle: (alignment & Qt.AlignVCenter) ? 270 - _maxAngle/2
: (alignment & Qt.AlignTop) ? 270
: 90
endAngle: startAngle + _maxAngle
radius: Theme.geometry.briefPage.edgeGauge.radius - 2*strokeWidth
strokeWidth: Theme.geometry.arc.strokeWidth
progressColor: Theme.statusColorValue(_status)
remainderColor: Theme.statusColorValue(_status, true)
clockwise: direction === PathArc.Clockwise
}
}
82 changes: 36 additions & 46 deletions components/CircularMultiGauge.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,56 +21,46 @@ Item {
// Step change in the size of the bounding boxes of successive gauges
readonly property real _stepSize: 2 * (strokeWidth + Theme.geometry.circularMultiGauge.spacing)

Item {
id: antialiased
anchors.fill: parent

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

Repeater {
id: arcRepeater
width: parent.width
delegate: Loader {
id: loader
property int gaugeStatus: Gauges.getValueStatus(model.value, model.valueType)
property real value: model.value
width: parent.width - (index*_stepSize)
height: width
anchors.centerIn: parent
visible: model.index < Theme.geometry.briefPage.centerGauge.maximumGaugeCount
sourceComponent: model.tankType === VenusOS.Tank_Type_Battery ? shinyProgressArc : progressArc
onStatusChanged: if (status === Loader.Error) console.warn("Unable to load circular multi gauge progress arc:", errorString())

Component {
id: shinyProgressArc
ShinyProgressArc {
radius: width/2
startAngle: 0
endAngle: 270
value: loader.value
progressColor: Theme.statusColorValue(loader.gaugeStatus)
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
strokeWidth: gauges.strokeWidth
animationEnabled: gauges.animationEnabled
shineAnimationEnabled: Global.batteries.system.mode === VenusOS.Battery_Mode_Charging
}
Component {
id: shinyProgressArc
ShaderCircularGauge {
startAngle: 0
endAngle: 270
value: loader.value
progressColor: Theme.statusColorValue(loader.gaugeStatus)
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
strokeWidth: gauges.strokeWidth
animationEnabled: gauges.animationEnabled
shineAnimationEnabled: Global.batteries.system.mode === VenusOS.Battery_Mode_Charging
}
}

Component {
id: progressArc
ProgressArc {
radius: width/2
startAngle: 0
endAngle: 270
value: loader.value
progressColor: Theme.statusColorValue(loader.gaugeStatus)
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
strokeWidth: gauges.strokeWidth
animationEnabled: gauges.animationEnabled
}
Component {
id: progressArc
ShaderCircularGauge {
startAngle: 0
endAngle: 270
value: loader.value
progressColor: Theme.statusColorValue(loader.gaugeStatus)
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
strokeWidth: gauges.strokeWidth
animationEnabled: gauges.animationEnabled
shineAnimationEnabled: false
}
}
}
Expand Down
35 changes: 12 additions & 23 deletions components/CircularSingleGauge.qml
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,18 @@ Item {
property alias animationEnabled: arc.animationEnabled
property alias shineAnimationEnabled: arc.shineAnimationEnabled

Item {
id: antialiased
anchors.fill: parent

// Antialiasing without requiring multisample framebuffers.
layer.enabled: true
layer.smooth: true
layer.textureSize: Qt.size(antialiased.width*2, antialiased.height*2)

// The single circular gauge is always the battery gauge :. shiny.
ShinyProgressArc {
id: arc

width: gauges.width
height: width
anchors.centerIn: parent
radius: width/2
startAngle: 0
endAngle: 359 // "Note that a single PathArc cannot be used to specify a circle."
progressColor: Theme.statusColorValue(gauges.status)
remainderColor: Theme.statusColorValue(gauges.status, true)
strokeWidth: Theme.geometry.circularSingularGauge.strokeWidth
}
// The single circular gauge is always the battery gauge :. shiny.
ShaderCircularGauge {
id: arc

width: gauges.width
height: width
anchors.centerIn: parent
startAngle: 0
endAngle: 359 // "Note that a single PathArc cannot be used to specify a circle."
progressColor: Theme.statusColorValue(gauges.status)
remainderColor: Theme.statusColorValue(gauges.status, true)
strokeWidth: Theme.geometry.circularSingularGauge.strokeWidth
}

Column {
Expand Down
81 changes: 81 additions & 0 deletions components/ShaderCircularGauge.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import QtQuick
import Victron.VenusOS

Item {
id: gauge

property real value // from 0.0 to 100.0
property color remainderColor: "gray"
property color progressColor: "blue"
property color shineColor: Qt.rgba(1.0, 1.0, 1.0, 0.80)
property real startAngle: 0
property real endAngle: 270
property real progressAngle: startAngle + ((endAngle - startAngle) * Math.min(Math.max(gauge.value, 0.0), 100.0) / 100.0)
property real strokeWidth: width/25
property real radius: (width - strokeWidth - smoothing)/2
property real smoothing: 1 // how many pixels of antialiasing to apply.
property bool clockwise: true
property bool shineAnimationEnabled: true
property bool animationEnabled: true

property real _normalizedRadiansFactor: (Math.PI/180) / (2*Math.PI)
property real _maxRadius: width/2

onProgressAngleChanged: {
if (!progressAnimator.running) {
progressAnimator.from = shader.progressAngle
progressAnimator.to = gauge.progressAngle * _normalizedRadiansFactor
progressAnimator.start()
}
}

Timer {
running: gauge.shineAnimationEnabled
interval: Theme.animation.briefPage.centerGauge.shine.duration + (Theme.animation.briefPage.centerGauge.shine.duration * Theme.animation.briefPage.centerGauge.shine.pauseRatio)
repeat: true
onTriggered: {
shineAnimator.duration = (gauge.progressAngle / gauge.endAngle) * Theme.animation.briefPage.centerGauge.shine.duration
shineAnimator.from = 0.0
shineAnimator.to = Math.min(gauge.endAngle, gauge.progressAngle+5) * _normalizedRadiansFactor
shineAnimator.start()
}
}

ShaderEffect {
id: shader
anchors.fill: parent
fragmentShader: "shaders/circulargauge.frag.qsb"

property color remainderColor: gauge.remainderColor
property color progressColor: gauge.progressColor
property color shineColor: gauge.shineColor
// transform angles to radians and then normalize
property real startAngle: gauge.startAngle * gauge._normalizedRadiansFactor
property real endAngle: gauge.endAngle * gauge._normalizedRadiansFactor
property real progressAngle: -1.0
property real shineAngle: -1.0
// transform radii to uv coords
property real innerRadius: (gauge.radius - (gauge.strokeWidth/2)) / (gauge._maxRadius)
property real radius: gauge.radius / (gauge._maxRadius)
property real outerRadius: (gauge.radius + (gauge.strokeWidth/2)) / (gauge._maxRadius)
// transform smoothing pixels to uv distance
property real smoothing: gauge.smoothing / gauge._maxRadius
property real clockwise: gauge.clockwise ? 1.0 : 0.0

UniformAnimator {
id: progressAnimator
target: shader
uniform: "progressAngle"
duration: Theme.animation.progressArc.duration
easing.type: Easing.InOutQuad
}

UniformAnimator {
id: shineAnimator
target: shader
uniform: "shineAngle"
easing.type: Easing.InQuad
onRunningChanged: if (!running) shader.shineAngle = -1.0
}
}
}
Loading

0 comments on commit 10e70ee

Please sign in to comment.