Skip to content

Commit

Permalink
WIP: write customer shaders for circular gauges
Browse files Browse the repository at this point in the history
Animating the Shape-based circular gauges is too expensive.
So reimplement it as a single-pass shader.
  • Loading branch information
chriadam committed Dec 8, 2023
1 parent fa63174 commit 6bc678f
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 48 deletions.
15 changes: 13 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,7 @@ set (VENUS_QML_MODULE_SOURCES
components/RadioButtonControlValue.qml
components/SegmentedButtonRow.qml
components/SeparatorBar.qml
components/ShaderCircularGauge.qml
components/ShinyProgressArc.qml
components/SideGauge.qml
components/SolarDetailBox.qml
Expand Down Expand Up @@ -454,6 +455,16 @@ 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"
)
# end VENUS_QML_MODULE

# Dbus_QML_MODULE
Expand Down
84 changes: 38 additions & 46 deletions components/CircularMultiGauge.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,56 +21,48 @@ 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
//ShinyProgressArc {
ShaderCircularGauge {
radius: (width-strokeWidth-smoothing)/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: 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
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
}
}
}
Expand Down
78 changes: 78 additions & 0 deletions components/ShaderCircularGauge.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import QtQuick
import Victron.VenusOS

Item {
id: gauge

property real value // from 0.0 to 1.0
property color remainderColor: "gray"
property color progressColor: "blue"
property color shineColor: Qt.rgba(1.0, 1.0, 1.0, 0.85)
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/2 - 2*strokeWidth
property real smoothing: 1 // how many pixels of antialiasing to apply.
property bool clockwise: true
property bool shineAnimationEnabled: true
property bool animationEnabled: true

onProgressAngleChanged: {
if (!progressAnimator.running) {
progressAnimator.from = shader.progressAngle
progressAnimator.to = (gauge.progressAngle * (Math.PI/180)) / (2*Math.PI)
progressAnimator.start()
}
}

Timer {
running: gauge.shineAnimationEnabled
interval: 4040
repeat: true
onTriggered: {
shineAnimator.from = 0.0
shineAnimator.to = (gauge.endAngle * (Math.PI/180)) / (2*Math.PI)
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 * (Math.PI/180)) / (2*Math.PI)
property real endAngle: (gauge.endAngle * (Math.PI/180)) / (2*Math.PI)
property real progressAngle: -1.0
property real shineAngle: -1.0
// transform radii to uv coords
property real innerRadius: (gauge.radius - (gauge.strokeWidth/2)) / (gauge.height/2)
property real radius: gauge.radius / (gauge.height/2)
property real outerRadius: (gauge.radius + (gauge.strokeWidth/2)) / (gauge.height/2)
// transform smoothing pixels to uv distance
property real smoothing: gauge.smoothing / height
property real clockwise: gauge.clockwise ? 1.0 : 0.0

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

UniformAnimator {
id: shineAnimator
target: shader
uniform: "shineAngle"
duration: 1200
easing.type: Easing.InOutQuad
onRunningChanged: if (!running) shader.shineAngle = -1.0
}
}
}
107 changes: 107 additions & 0 deletions shaders/circulargauge.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#version 440
#define CONSTANT_PI 3.141592653589793
#define CONSTANT_TAU 6.283185307179586
layout(location = 0) in vec2 coord;
layout(location = 0) out vec4 fragColor;

layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;

vec4 remainderColor;
vec4 progressColor;
vec4 shineColor;
float startAngle;
float endAngle;
float progressAngle;
float shineAngle;
float innerRadius;
float radius;
float outerRadius;
float smoothing;
float clockwise;
} ubuf;

// atan2 which isn't undefined at x == 0
float my_atan2(float y, float x) {
return x == 0.0 ? sign(y) * CONSTANT_PI : atan(y, x);
}

// angle expressed in normal radians, but with orientation.
float denormalizedAngle(float a, float clockwise) {
float angle = clockwise*(1.0 - a) + (1.0 - clockwise)*a;
return (angle * CONSTANT_TAU) - CONSTANT_PI;
}

// angle must be in radians [-PI, PI].
// return value is normalized to [0.0, 1.0],
// inverted if necessary to adjust for clockwise vs anticlockwise.
float normalizedAngle(float angle, float clockwise) {
float a = (angle + CONSTANT_PI) / CONSTANT_TAU;
a = clockwise*(1.0 - a) + (1.0 - clockwise)*a;
return a;
}

// converts from cartesian to polar coordinates (angle, radius).
// angle is normalized to [0.0, 1.0].
vec2 toPolar(vec2 point, float clockwise) {
// note that we have flipped y/x to x/y to get angle from y axis (i.e. vertical).
return vec2(normalizedAngle(atan(point.x, point.y), clockwise), length(point));
}

// converts from polar to cartesian coordinates (x, y).
vec2 toCartesian(vec2 polar, float clockwise) {
float angle = denormalizedAngle(polar.x, clockwise);
// note that we have flipped cos/sin to sin/cos since angle is from y axis (i.e. vertical).
return vec2(polar.y * sin(angle), polar.y * cos(angle));
}

// if you move the specified distance along the arc, how much angle has been traversed?
// the error grows larger and larger the bigger distanceMoved is (since we assume straight-line movement)...
float angleDelta(float startAngle, float endAngle, float radius, float distanceMoved) {
float arcLength = (endAngle - startAngle) * CONSTANT_TAU * radius;
return distanceMoved / arcLength;
}

void main() {
vec2 uv = coord * 2.0 - 1.0;
float uvDistance = length(uv); // distance from the center

// note: we want angle from y axis rather than x axis, so flip args of atan2.
float uvAngle = normalizedAngle(my_atan2(uv.x, uv.y), ubuf.clockwise);
float withinGaugeAngle = (uvAngle < ubuf.startAngle || uvAngle > ubuf.endAngle) ? 0.0 : 1.0;

// calculate the rounded caps.
float capRadius = ubuf.outerRadius - ubuf.radius;
float capAngleDelta = angleDelta(ubuf.startAngle, ubuf.endAngle, ubuf.radius, capRadius);

// the startAngle cap.
vec2 startCapCenter = toCartesian(vec2(ubuf.startAngle, ubuf.radius), ubuf.clockwise);
float startCapAlpha = 1.0 - smoothstep(capRadius, capRadius + ubuf.smoothing, distance(uv, startCapCenter));

// the endAngle cap.
vec2 endCapCenter = toCartesian(vec2(ubuf.endAngle, ubuf.radius), ubuf.clockwise);
float endCapAlpha = 1.0 - smoothstep(capRadius, capRadius + ubuf.smoothing, distance(uv, endCapCenter));

// the progress cap.
vec2 progressCapCenter = toCartesian(vec2(ubuf.progressAngle, ubuf.radius), ubuf.clockwise);
float progressCapMix = 1.0 - smoothstep(capRadius, capRadius + ubuf.smoothing, distance(uv, progressCapCenter));

// calculate shine animation. we have to adjust the angles because of the startCap.
float adjustedAngle = (uvAngle + 0.08);
adjustedAngle = adjustedAngle > 1.0 ? adjustedAngle - 1.0 : adjustedAngle;
float adjustedShineAngle = ubuf.shineAngle + 0.08;
float shineMix = smoothstep(adjustedShineAngle - 0.08, adjustedShineAngle, adjustedAngle)
* (1.0 - smoothstep(adjustedShineAngle, adjustedShineAngle + 0.02, adjustedAngle));

// antialiasing.
float gaugeAngleAlpha = max(endCapAlpha, max(startCapAlpha, withinGaugeAngle));
float gaugeStrokeAlpha = smoothstep(ubuf.innerRadius - ubuf.smoothing, ubuf.innerRadius, uvDistance)
* (1.0 - smoothstep(ubuf.outerRadius, ubuf.outerRadius + ubuf.smoothing, uvDistance));

float isProgressBar = ((uvAngle > (ubuf.endAngle + capAngleDelta)) || uvAngle <= ubuf.progressAngle) ? 1.0 : 0.0;
float progressColorMix = max(isProgressBar, progressCapMix);

// Qt expects pre-multiplied output, so don't just set w-channel.
fragColor = mix(ubuf.remainderColor, mix(ubuf.progressColor, ubuf.shineColor, shineMix), progressColorMix) * gaugeAngleAlpha * gaugeStrokeAlpha * ubuf.qt_Opacity;
}

0 comments on commit 6bc678f

Please sign in to comment.