Skip to content

Commit aeefc11

Browse files
authored
Add Hybrid RGB Smoothing Interpolator (#1379)
* Add Hybrid RGB Smoothing Interpolator * Add Hybrid RGB Smoothing Interpolator (2) * Add Hybrid RGB Smoothing Interpolator (3) * Add Hybrid RGB Smoothing Interpolator (4)
1 parent d2900bd commit aeefc11

File tree

7 files changed

+315
-17
lines changed

7 files changed

+315
-17
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#pragma once
2+
3+
#ifndef PCH_ENABLED
4+
#include <QJsonDocument>
5+
#include <QMutex>
6+
#include <QVector>
7+
8+
#include <vector>
9+
#endif
10+
11+
#include <infinite-color-engine/InfiniteInterpolator.h>
12+
13+
class InfiniteHybridRgbInterpolator : public InfiniteInterpolator
14+
{
15+
public:
16+
InfiniteHybridRgbInterpolator();
17+
18+
void setTargetColors(std::vector<linalg::aliases::float3>&& new_rgb_targets, float startTimeMs, bool debug = false) override;
19+
void updateCurrentColors(float currentTimeMs) override;
20+
SharedOutputColors getCurrentColors() override;
21+
22+
void setTransitionDuration(float durationMs) override;
23+
void setSpringiness(float stiffness, float damping) override;
24+
void setSmoothingFactor(float factor) override;
25+
26+
void resetToColors(std::vector<linalg::aliases::float3> colors, float startTimeMs);
27+
static void test();
28+
29+
private:
30+
std::vector<linalg::aliases::float3> _targetColorsRGB;
31+
std::vector<linalg::aliases::float3> _currentColorsRGB;
32+
std::vector<linalg::aliases::float3> _velocitiesRGB;
33+
34+
float _initialDuration = 150.0f;
35+
float _startAnimationTimeMs = 0.0f;
36+
float _targetTime = 0.0f;
37+
float _lastUpdate = 0.0f;
38+
float _stiffness = 150.0f;
39+
float _damping = 26.0f;
40+
float _smoothingFactor = 0.0f;
41+
};

include/infinite-color-engine/InfiniteSmoothing.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private slots:
6666

6767
bool _continuousOutput;
6868

69-
enum class SmoothingType { Stepper = 0, RgbInterpolator = 1, YuvInterpolator = 2, HybridInterpolator = 3, ExponentialInterpolator = 4};
69+
enum class SmoothingType { Stepper = 0, RgbInterpolator = 1, YuvInterpolator = 2, HybridInterpolator = 3, ExponentialInterpolator = 4, HybridRgbInterpolator = 5};
7070
static QString EnumSmoothingTypeToString(SmoothingType type);
7171
static SmoothingType StringToEnumSmoothingType(QString name);
7272

sources/base/schema/schema-smoothing.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
{
1717
"type" : "string",
1818
"title" : "edt_conf_smooth_type_title",
19-
"enum" : ["Stepper", "YuvInterpolator", "RgbInterpolator", "HybridInterpolator", "ExponentialInterpolator"],
20-
"default" : "Stepper",
19+
"enum" : ["Stepper", "YuvInterpolator", "RgbInterpolator", "HybridInterpolator", "HybridRgbInterpolator", "ExponentialInterpolator"],
20+
"default" : "HybridRgbInterpolator",
2121
"options" : {
22-
"enum_titles" : ["edt_conf_enum_interpolator_Stepper_title", "edt_conf_enum_interpolator_YuvInterpolator_title", "edt_conf_enum_interpolator_RgbInterpolator_title", "edt_conf_enum_interpolator_HybridInterpolator_title", "edt_conf_enum_interpolator_ExponentialInterpolator_title"]
22+
"enum_titles" : ["edt_conf_enum_interpolator_Stepper_title", "edt_conf_enum_interpolator_YuvInterpolator_title", "edt_conf_enum_interpolator_RgbInterpolator_title", "edt_conf_enum_interpolator_HybridInterpolator_title", "edt_conf_enum_interpolator_HybridRgbInterpolator_title", "edt_conf_enum_interpolator_ExponentialInterpolator_title"]
2323
},
2424
"required" : true,
2525
"propertyOrder" : 2
@@ -62,7 +62,7 @@
6262
"required" : true,
6363
"options": {
6464
"dependencies": {
65-
"type": ["YuvInterpolator", "RgbInterpolator"]
65+
"type": ["YuvInterpolator", "RgbInterpolator", "HybridRgbInterpolator"]
6666
}
6767
},
6868
"propertyOrder" : 5
@@ -79,7 +79,7 @@
7979
"required" : true,
8080
"options": {
8181
"dependencies": {
82-
"type": "HybridInterpolator"
82+
"type": ["HybridInterpolator", "HybridRgbInterpolator"]
8383
}
8484
},
8585
"propertyOrder" : 6
@@ -96,7 +96,7 @@
9696
"required" : true,
9797
"options": {
9898
"dependencies": {
99-
"type": "HybridInterpolator"
99+
"type": ["HybridInterpolator", "HybridRgbInterpolator"]
100100
}
101101
},
102102
"propertyOrder" : 7
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/* InfiniteHybridRgbInterpolator.cpp
2+
*
3+
* MIT License
4+
*
5+
* Copyright (c) 2020-2025 awawa-dev
6+
*
7+
* Project homesite: https://github.com/awawa-dev/HyperHDR
8+
*
9+
* Permission is hereby granted, free of charge, to any person obtaining a copy
10+
* of this software and associated documentation files (the "Software"), to deal
11+
* in the Software without restriction, including without limitation the rights
12+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
* copies of the Software, and to permit persons to whom the Software is
14+
* furnished to do so, subject to the following conditions:
15+
*
16+
* The above copyright notice and this permission notice shall be included in all
17+
* copies or substantial portions of the Software.
18+
19+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25+
* SOFTWARE.
26+
*/
27+
28+
#ifndef PCH_ENABLED
29+
#include <QTimer>
30+
#include <QThread>
31+
32+
#include <cmath>
33+
#include <algorithm>
34+
#include <chrono>
35+
#include <iomanip>
36+
#include <iostream>
37+
#include <limits>
38+
#include <tuple>
39+
#include <vector>
40+
#include <cassert>
41+
#include <cstdint>
42+
#endif
43+
44+
#include <infinite-color-engine/InfiniteHybridRgbInterpolator.h>
45+
#include <infinite-color-engine/ColorSpace.h>
46+
47+
using namespace linalg::aliases;
48+
49+
InfiniteHybridRgbInterpolator::InfiniteHybridRgbInterpolator() = default;
50+
51+
void InfiniteHybridRgbInterpolator::setTransitionDuration(float durationMs)
52+
{
53+
_initialDuration = std::max(1.0f, durationMs);
54+
}
55+
56+
void InfiniteHybridRgbInterpolator::setSpringiness(float stiffness, float damping)
57+
{
58+
_stiffness = std::max(0.1f, stiffness);
59+
_damping = std::max(0.1f, damping);
60+
}
61+
62+
void InfiniteHybridRgbInterpolator::setSmoothingFactor(float factor)
63+
{
64+
_smoothingFactor = std::max(0.0f, std::min(factor, 1.0f));
65+
}
66+
67+
void InfiniteHybridRgbInterpolator::resetToColors(std::vector<float3> colors, float startTimeMs)
68+
{
69+
_currentColorsRGB.clear();
70+
setTargetColors(std::move(colors), startTimeMs);
71+
}
72+
73+
void InfiniteHybridRgbInterpolator::setTargetColors(std::vector<float3>&& new_rgb_targets, float startTimeMs, bool debug) {
74+
if (new_rgb_targets.empty())
75+
return;
76+
77+
const float delta = (!_isAnimationComplete) ? std::max(startTimeMs - _lastUpdate, 0.f) : 0.f;
78+
79+
if (debug)
80+
{
81+
printf(" | Δ%.0f | time: %.0f | smoothFactor: %.3f| stiffness: %.3f| damping: %.3f\n", delta, _initialDuration, _smoothingFactor, _stiffness, _damping);
82+
}
83+
84+
startTimeMs -= delta;
85+
86+
if (_currentColorsRGB.size() != new_rgb_targets.size())
87+
{
88+
_lastUpdate = startTimeMs;
89+
_currentColorsRGB = new_rgb_targets;
90+
_targetColorsRGB = std::move(new_rgb_targets);
91+
_velocitiesRGB.assign(_currentColorsRGB.size(), float3{ 0,0,0 }); // NOWE
92+
_isAnimationComplete = true;
93+
}
94+
else
95+
{
96+
if (_smoothingFactor > 0.f)
97+
{
98+
const float inv = 1.f - _smoothingFactor;
99+
for (auto it_oldTargetColorsRGB = _targetColorsRGB.begin(),
100+
it_newTargetColorsRGB = new_rgb_targets.begin();
101+
it_oldTargetColorsRGB != _targetColorsRGB.end();
102+
++it_oldTargetColorsRGB, ++it_newTargetColorsRGB)
103+
{
104+
*it_oldTargetColorsRGB = *it_oldTargetColorsRGB * _smoothingFactor
105+
+ *it_newTargetColorsRGB * inv;
106+
}
107+
}
108+
else
109+
{
110+
_targetColorsRGB = std::move(new_rgb_targets);
111+
}
112+
_isAnimationComplete = false;
113+
}
114+
115+
_startAnimationTimeMs = startTimeMs;
116+
_targetTime = startTimeMs + _initialDuration;
117+
}
118+
119+
void InfiniteHybridRgbInterpolator::updateCurrentColors(float currentTimeMs) {
120+
if (_isAnimationComplete)
121+
return;
122+
123+
float dt = std::max(currentTimeMs - _lastUpdate, 0.001f);
124+
_lastUpdate = currentTimeMs;
125+
126+
auto computeChannelVec = [&](float3& cur, const float3& tgt, const float3& diff, float3& vel) -> bool {
127+
const float FINISH_COMPONENT_THRESHOLD = 0.0013732906f / 10.f;
128+
const float VELOCITY_THRESHOLD = 0.0005f;
129+
130+
if (linalg::maxelem(linalg::abs(diff)) < FINISH_COMPONENT_THRESHOLD && // color match
131+
linalg::maxelem(linalg::abs(vel)) < VELOCITY_THRESHOLD) // speed should be almost zero
132+
{
133+
cur = tgt;
134+
vel = float3{ 0,0,0 };
135+
return false;
136+
}
137+
else
138+
{
139+
float3 acc = _stiffness * diff - _damping * vel;
140+
141+
// integracja (Euler semi-implicit)
142+
vel += acc * (dt * 0.001f);
143+
cur += vel * (dt * 0.001f);
144+
145+
return true;
146+
}
147+
};
148+
149+
_isAnimationComplete = true;
150+
151+
for (auto cur = _currentColorsRGB.begin(), tgt = _targetColorsRGB.begin(), vel = _velocitiesRGB.begin();
152+
cur != _currentColorsRGB.end() && tgt != _targetColorsRGB.end() && vel != _velocitiesRGB.end();
153+
++cur, ++tgt, ++vel)
154+
{
155+
if (computeChannelVec(*cur, *tgt, *tgt - *cur, *vel))
156+
{
157+
_isAnimationComplete = false;
158+
}
159+
}
160+
}
161+
162+
SharedOutputColors InfiniteHybridRgbInterpolator::getCurrentColors()
163+
{
164+
auto result = std::make_shared<std::vector<linalg::vec<float, 3>>>();
165+
result->reserve(_currentColorsRGB.size());
166+
167+
for (const auto& rgb : _currentColorsRGB)
168+
result->push_back(linalg::clamp(rgb, 0.f, 1.f));
169+
170+
return result;
171+
}
172+
173+
void InfiniteHybridRgbInterpolator::test() {
174+
using TestTuple = std::tuple<std::string, float3, float3, float3, float3>;
175+
176+
auto run_test_lambda = [](InfiniteHybridRgbInterpolator& interpolator, const TestTuple& test) {
177+
const auto& name = std::get<0>(test);
178+
const auto& start_A = std::get<1>(test);
179+
const auto& interrupt_C = std::get<2>(test);
180+
const auto& interrupt_D = std::get<3>(test);
181+
const auto& final_B = std::get<4>(test);
182+
183+
std::cout << "\n--- TEST: " << name << " ---\n";
184+
std::cout << "--------------------------------------------------\n";
185+
186+
interpolator.resetToColors({ start_A }, 0.f);
187+
interpolator.setTransitionDuration(150.0f);
188+
189+
bool retargeted_to_D = false;
190+
bool retargeted_to_B = false;
191+
192+
for (float time_ms = 0; time_ms <= 1000; time_ms += 25) {
193+
if (time_ms == 0) {
194+
interpolator.setTargetColors({ interrupt_C }, time_ms);
195+
}
196+
if (time_ms >= 200 && !retargeted_to_D) {
197+
std::cout << "--- ZMIANA CELU 1 @ " << time_ms << "ms (-> D) ---\n";
198+
interpolator.setTargetColors({ interrupt_D }, time_ms);
199+
retargeted_to_D = true;
200+
}
201+
if (time_ms >= 350 && !retargeted_to_B) {
202+
std::cout << "--- ZMIANA CELU 2 @ " << time_ms << "ms (-> B) ---\n";
203+
interpolator.setTargetColors({ final_B }, time_ms);
204+
retargeted_to_B = true;
205+
}
206+
207+
interpolator.updateCurrentColors(time_ms);
208+
209+
auto temp_color = interpolator.getCurrentColors();
210+
const auto& current_color = *(temp_color);
211+
if (current_color.empty()) continue;
212+
213+
std::cout << std::setw(4) << static_cast<int>(time_ms) << "ms: { R: "
214+
<< std::fixed << std::setprecision(3) << current_color[0].x
215+
<< ", G: " << current_color[0].y
216+
<< ", B: " << current_color[0].z
217+
<< " }\n";
218+
}
219+
std::cout << "--------------------------------------------------\n";
220+
};
221+
222+
InfiniteHybridRgbInterpolator interpolator;
223+
interpolator.setSpringiness(200.0f, 26.0f);
224+
225+
std::cout << "\n###########################\n";
226+
std::cout << "### URUCHAMIANIE TESTÓW ###\n";
227+
std::cout << "#############################\n";
228+
interpolator.setMaxLuminanceChangePerFrame(0);
229+
230+
std::vector<TestTuple> test_cases_unlimited = {
231+
{"Test: Czarny -> Ciemny Nieb. -> Jasny Żółty -> Biały", {0,0,0}, {0.1f, 0.1f, 0.4f}, {0.9f, 0.9f, 0.2f}, {1,1,1} },
232+
{"Test: Czerwony -> Żółty -> Cyjan -> Niebieski", {1,0,0}, {1,1,0}, {0,1,1}, {0,0,1}},
233+
{"Test: Niebieski -> Zielony -> Czerwony -> Złoty", {0,0,1}, {0,1,0}, {1,0,0}, {1,1,0}},
234+
{"Test: Magenta -> Cyjan -> Żółty -> Zielony", {1,0,1}, {0,1,1}, {1,1,0}, {0,1,0}},
235+
{"Test: Cyjan -> Żółty -> Magenta -> Czerwony", {0,1,1}, {1,1,0}, {1,0,1}, {1,0,0}},
236+
{"Test: Niebieski (50%) -> Czerwony -> Zielony -> Złoty (50%)", {0.25f, 0.25f, 0.75f}, {1,0,0}, {0,1,0}, {0.75f, 0.75f, 0.25f}},
237+
{"Test: Magenta (50%) -> Niebieski -> Czerwony -> Zielony (50%)", {0.75f, 0.25f, 0.75f}, {0,0,1}, {1,0,0}, {0.25f, 0.75f, 0.25f}},
238+
{"Test: Niebieski (25%) -> Szary -> Jasny Szary -> Złoty (25%)", {0.0f, 0.0f, 0.25f}, {0.2f,0.2f,0.2f}, {0.8f,0.8f,0.8f}, {0.25f, 0.25f, 0.0f}},
239+
{"Test: Magenta (25%) -> Ciemny Cyjan -> Jasny Żółty -> Zielony (25%)", {0.25f, 0.0f, 0.25f}, {0.0f,0.2f,0.2f}, {0.8f,0.8f,0.0f}, {0.0f, 0.25f, 0.0f}},
240+
{"Test: Cyjan (25%) -> Fiolet -> Pomarańcz -> Czerwony (25%)", {0.0f, 0.25f, 0.25f}, {0.5f,0.0f,0.5f}, {1.0f,0.5f,0.0f}, {0.25f, 0.0f, 0.0f}},
241+
{"Test: Ciemny Nieb. -> Biały -> Czarny -> Jasny Żółty", {0,0,0.5f}, {1,1,1}, {0,0,0}, {1,1,0.8f}},
242+
{"Test: Jasny Czerw. -> Ciemny Nieb. -> Jasny Zielony -> Ciemny Cyjan", {1,0,0}, {0,0,0.2f}, {0.5f,1.0f,0.5f}, {0.1f, 0.4f, 0.4f}},
243+
{"Test: Zielony -> Fiolet -> Cyjan -> Czerwony", {0,1,0}, {0.5f,0,1.0f}, {0,1,1}, {1,0,0}},
244+
{"Test: Zielony -> Żółty -> Niebieski -> Magenta", {0,1,0}, {1,1,0}, {0,0,1}, {1,0,1}},
245+
{"Test: Biały -> Czerwony -> Zielony -> Niebieski", {1,1,1}, {1,0,0}, {0,1,0}, {0,0,1}},
246+
{"Test: Zielony -> Cyjan -> Czerwony -> Niebieski", {0,1,0}, {0,1,1}, {1,0,0}, {0,0,1}},
247+
{"Test: Test Odwrócenia (Biały -> Czarny -> Biały -> Czarny)", {1,1,1}, {0,0,0}, {1,1,1}, {0,0,0}},
248+
{"Test: Test Oscylacji (Żółty -> Niebieski -> Żółty -> Niebieski)", {1,1,0}, {0,0,1}, {1,1,0}, {0,0,1}},
249+
{"Test: Test Oscylacji (Magenta -> Zielony -> Magenta -> Zielony)", {1,0,1}, {0,1,0}, {1,0,1}, {0,1,0}}
250+
};
251+
for (const auto& test : test_cases_unlimited) {
252+
run_test_lambda(interpolator, test);
253+
}
254+
}

sources/infinite-color-engine/InfiniteSmoothing.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include <infinite-color-engine/InfiniteYuvInterpolator.h>
4949
#include <infinite-color-engine/InfiniteRgbInterpolator.h>
5050
#include <infinite-color-engine/InfiniteHybridInterpolator.h>
51+
#include <infinite-color-engine/InfiniteHybridRgbInterpolator.h>
5152
#include <infinite-color-engine/InfiniteExponentialInterpolator.h>
5253
#include <base/HyperHdrInstance.h>
5354

@@ -113,6 +114,10 @@ void InfiniteSmoothing::clearQueuedColors(bool deviceEnabled, bool restarting)
113114
{
114115
_interpolator = std::make_unique<InfiniteHybridInterpolator>();
115116
}
117+
else if (cfg->type == SmoothingType::HybridRgbInterpolator)
118+
{
119+
_interpolator = std::make_unique<InfiniteHybridRgbInterpolator>();
120+
}
116121
else if (cfg->type == SmoothingType::YuvInterpolator)
117122
{
118123
_interpolator = std::make_unique<InfiniteYuvInterpolator>();
@@ -375,6 +380,8 @@ QString InfiniteSmoothing::EnumSmoothingTypeToString(SmoothingType type)
375380
return QString("HybridInterpolator");
376381
else if (type == SmoothingType::ExponentialInterpolator)
377382
return QString("ExponentialInterpolator");
383+
else if (type == SmoothingType::HybridRgbInterpolator)
384+
return QString("HybridRgbInterpolator");
378385

379386
return QString("Stepper");
380387
}
@@ -389,6 +396,8 @@ InfiniteSmoothing::SmoothingType InfiniteSmoothing::StringToEnumSmoothingType(QS
389396
return SmoothingType::HybridInterpolator;
390397
else if (name == QString("ExponentialInterpolator"))
391398
return SmoothingType::ExponentialInterpolator;
399+
else if (name == QString("HybridRgbInterpolator"))
400+
return SmoothingType::HybridRgbInterpolator;
392401

393402
return SmoothingType::Stepper;
394403
}

www/i18n/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1278,8 +1278,10 @@
12781278
"edt_conf_enum_interpolator_RgbInterpolator_expl": "This algorithm smoothly animates RGB colors over a set duration, offering two distinct modes for the transition. The first is a direct linear interpolation for a straight path between colors, while the second is a smoothed mode where the current color gracefully 'chases' the target to create an ease-in/ease-out effect. A key feature is its ability to intelligently rescale an animation's duration when interrupted, ensuring a perceptually constant speed of change.",
12791279
"edt_conf_enum_interpolator_YuvInterpolator_title": "YUV Infinite Interpolator",
12801280
"edt_conf_enum_interpolator_YuvInterpolator_expl": "This algorithm smoothly interpolates colors by operating in the YUV color space for more perceptually uniform transitions. Its main feature is limiting the rate of luminance change in each step, preventing sudden, jarring flashes of brightness. This ensures a visually pleasing effect, even if it extends the animation beyond its initially set duration to maintain that smoothness.",
1281-
"edt_conf_enum_interpolator_HybridInterpolator_title": "Hybrid Physics Infinite Interpolator",
1281+
"edt_conf_enum_interpolator_HybridInterpolator_title": "Hybrid Physics Infinite Interpolator (YUV)",
12821282
"edt_conf_enum_interpolator_HybridInterpolator_expl": "This algorithm smoothly transitions between colors using a hybrid physical model. A linear 'pacer' defines the direct path and timing to the target color, while the actual output color follows this pacer like an object attached to a damped spring. This two-part approach creates fluid, natural-looking animations with customizable inertia and overshoot, all while operating in the perceptually-uniform YUV color space.",
1283+
"edt_conf_enum_interpolator_HybridRgbInterpolator_title": "Hybrid Physics Infinite Interpolator (RGB)",
1284+
"edt_conf_enum_interpolator_HybridRgbInterpolator_expl": "This algorithm smoothly transitions between colors using a hybrid physical model. A linear 'pacer' defines the direct path and timing to the target color, while the actual output color follows this pacer like an object attached to a damped spring. This two-part approach creates fluid, natural-looking animations with customizable inertia and overshoot, all while operating in the RGB color space.",
12831285
"edt_conf_enum_interpolator_smoothingFactor_title": "Smoothing factor",
12841286
"edt_conf_enum_interpolator_smoothingFactor_expl": "The parameter dictates the animation's feel, controlling how it transitions between colors. It accepts a value from 0.0 to 1.0, where 0.0 results in a direct, linear change. Higher values introduce more smoothing, creating a fluid 'chasing' motion as the current color lags slightly behind the ideal position.",
12851287
"edt_conf_enum_interpolator_stiffness_title": "Stiffness",

0 commit comments

Comments
 (0)