Addressable light transition (#750)

* Improve addressable light transition behavior

Fixes https://github.com/esphome/issues/issues/555

* Improve addressable flicker effect

See also https://github.com/esphome/feature-requests/issues/348

* Update addressable_light_effect.h

* Refactor

* Format

* Prevent divide by zero

* Fixes
This commit is contained in:
Otto Winter 2019-10-18 16:27:36 +02:00 committed by GitHub
parent b78b28ea0e
commit 95a74a7f19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 117 additions and 42 deletions

View file

@ -179,5 +179,101 @@ void AddressableLight::call_setup() {
#endif
}
ESPColor esp_color_from_light_color_values(LightColorValues val) {
auto r = static_cast<uint8_t>(roundf(val.get_red() * 255.0f));
auto g = static_cast<uint8_t>(roundf(val.get_green() * 255.0f));
auto b = static_cast<uint8_t>(roundf(val.get_blue() * 255.0f));
auto w = static_cast<uint8_t>(roundf(val.get_white() * val.get_state() * 255.0f));
return ESPColor(r, g, b, w);
}
void AddressableLight::write_state(LightState *state) {
auto val = state->current_values;
auto max_brightness = static_cast<uint8_t>(roundf(val.get_brightness() * val.get_state() * 255.0f));
this->correction_.set_local_brightness(max_brightness);
this->last_transition_progress_ = 0.0f;
this->accumulated_alpha_ = 0.0f;
if (this->is_effect_active())
return;
// don't use LightState helper, gamma correction+brightness is handled by ESPColorView
if (state->transformer_ == nullptr || !state->transformer_->is_transition()) {
// no transformer active or non-transition one
this->all() = esp_color_from_light_color_values(val);
} else {
// transition transformer active, activate specialized transition for addressable effects
// instead of using a unified transition for all LEDs, we use the current state each LED as the
// start. Warning: ugly
// We can't use a direct lerp smoothing here though - that would require creating a copy of the original
// state of each LED at the start of the transition
// Instead, we "fake" the look of the LERP by using an exponential average over time and using
// dynamically-calculated alpha values to match the look of the
float new_progress = state->transformer_->get_progress();
float prev_smoothed = LightTransitionTransformer::smoothed_progress(last_transition_progress_);
float new_smoothed = LightTransitionTransformer::smoothed_progress(new_progress);
this->last_transition_progress_ = new_progress;
auto end_values = state->transformer_->get_end_values();
ESPColor target_color = esp_color_from_light_color_values(end_values);
// our transition will handle brightness, disable brightness in correction.
this->correction_.set_local_brightness(255);
uint8_t orig_w = target_color.w;
target_color *= static_cast<uint8_t>(roundf(end_values.get_brightness() * end_values.get_state() * 255.0f));
// w is not scaled by brightness
target_color.w = orig_w;
float denom = (1.0f - new_smoothed);
float alpha = denom == 0.0f ? 0.0f : (new_smoothed - prev_smoothed) / denom;
// We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length
// We solve this by accumulating the fractional part of the alpha over time.
float alpha255 = alpha * 255.0f;
float alpha255int = floorf(alpha255);
float alpha255remainder = alpha255 - alpha255int;
this->accumulated_alpha_ += alpha255remainder;
float alpha_add = floorf(this->accumulated_alpha_);
this->accumulated_alpha_ -= alpha_add;
alpha255 += alpha_add;
alpha255 = clamp(alpha255, 0.0f, 255.0f);
auto alpha8 = static_cast<uint8_t>(alpha255);
if (alpha8 != 0) {
uint8_t inv_alpha8 = 255 - alpha8;
ESPColor add = target_color * alpha8;
for (auto led : *this)
led = add + led.get() * inv_alpha8;
}
}
this->schedule_show();
}
void ESPColorCorrection::calculate_gamma_table(float gamma) {
for (uint16_t i = 0; i < 256; i++) {
// corrected = val ^ gamma
auto corrected = static_cast<uint8_t>(roundf(255.0f * gamma_correct(i / 255.0f, gamma)));
this->gamma_table_[i] = corrected;
}
if (gamma == 0.0f) {
for (uint16_t i = 0; i < 256; i++)
this->gamma_reverse_table_[i] = i;
return;
}
for (uint16_t i = 0; i < 256; i++) {
// val = corrected ^ (1/gamma)
auto uncorrected = static_cast<uint8_t>(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma)));
this->gamma_reverse_table_[i] = uncorrected;
}
}
} // namespace light
} // namespace esphome

View file

@ -189,23 +189,7 @@ class ESPColorCorrection {
ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {}
void set_max_brightness(const ESPColor &max_brightness) { this->max_brightness_ = max_brightness; }
void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; }
void calculate_gamma_table(float gamma) {
for (uint16_t i = 0; i < 256; i++) {
// corrected = val ^ gamma
auto corrected = static_cast<uint8_t>(roundf(255.0f * gamma_correct(i / 255.0f, gamma)));
this->gamma_table_[i] = corrected;
}
if (gamma == 0.0f) {
for (uint16_t i = 0; i < 256; i++)
this->gamma_reverse_table_[i] = i;
return;
}
for (uint16_t i = 0; i < 256; i++) {
// val = corrected ^ (1/gamma)
auto uncorrected = static_cast<uint8_t>(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma)));
this->gamma_reverse_table_[i] = uncorrected;
}
}
void calculate_gamma_table(float gamma);
inline ESPColor color_correct(ESPColor color) const ALWAYS_INLINE {
// corrected = (uncorrected * max_brightness * local_brightness) ^ gamma
return ESPColor(this->color_correct_red(color.red), this->color_correct_green(color.green),
@ -468,23 +452,7 @@ class AddressableLight : public LightOutput, public Component {
}
bool is_effect_active() const { return this->effect_active_; }
void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; }
void write_state(LightState *state) override {
auto val = state->current_values;
auto max_brightness = static_cast<uint8_t>(roundf(val.get_brightness() * val.get_state() * 255.0f));
this->correction_.set_local_brightness(max_brightness);
if (this->is_effect_active())
return;
// don't use LightState helper, gamma correction+brightness is handled by ESPColorView
ESPColor color = ESPColor(uint8_t(roundf(val.get_red() * 255.0f)), uint8_t(roundf(val.get_green() * 255.0f)),
uint8_t(roundf(val.get_blue() * 255.0f)),
// white is not affected by brightness; so manually scale by state
uint8_t(roundf(val.get_white() * val.get_state() * 255.0f)));
this->all() = color;
this->schedule_show();
}
void write_state(LightState *state) override;
void set_correction(float red, float green, float blue, float white = 1.0f) {
this->correction_.set_max_brightness(ESPColor(uint8_t(roundf(red * 255.0f)), uint8_t(roundf(green * 255.0f)),
uint8_t(roundf(blue * 255.0f)), uint8_t(roundf(white * 255.0f))));
@ -524,6 +492,8 @@ class AddressableLight : public LightOutput, public Component {
power_supply::PowerSupplyRequester power_;
#endif
LightState *state_parent_{nullptr};
float last_transition_progress_{0.0f};
float accumulated_alpha_{0.0f};
};
} // namespace light

View file

@ -318,11 +318,16 @@ class AddressableFlickerEffect : public AddressableLightEffect {
const uint8_t inv_intensity = 255 - intensity;
if (now - this->last_update_ < this->update_interval_)
return;
this->last_update_ = now;
fast_random_set_seed(random_uint32());
for (auto var : it) {
const uint8_t flicker = fast_random_8() % intensity;
var = (var.get() * inv_intensity) + (current_color * flicker);
// scale down by random factor
var = var.get() * (255 - flicker);
// slowly fade back to "real" value
var = (var.get() * inv_intensity) + (current_color * intensity);
}
}
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }

View file

@ -463,7 +463,7 @@ LightColorValues LightCall::validate_() {
this->transition_length_.reset();
}
if (!this->has_transition_() && !this->has_flash_() && !this->has_effect_() && supports_transition) {
if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) && supports_transition) {
// nothing specified and light supports transitions, set default transition length
this->transition_length_ = this->parent_->default_transition_length_;
}

View file

@ -277,6 +277,7 @@ class LightState : public Nameable, public Component {
protected:
friend LightOutput;
friend LightCall;
friend class AddressableLight;
uint32_t hash_base() override;

View file

@ -17,7 +17,7 @@ class LightTransformer {
LightTransformer() = delete;
/// Whether this transformation is finished
virtual bool is_finished() { return this->get_progress_() >= 1.0f; }
virtual bool is_finished() { return this->get_progress() >= 1.0f; }
/// This will be called to get the current values for output.
virtual LightColorValues get_values() = 0;
@ -29,11 +29,11 @@ class LightTransformer {
virtual LightColorValues get_end_values() { return this->get_target_values_(); }
virtual bool publish_at_end() = 0;
virtual bool is_transition() = 0;
float get_progress() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); }
protected:
/// Get the completion of this transformer, 0 to 1.
float get_progress_() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); }
const LightColorValues &get_start_values_() const { return this->start_values_; }
const LightColorValues &get_target_values_() const { return this->target_values_; }
@ -61,12 +61,14 @@ class LightTransitionTransformer : public LightTransformer {
}
LightColorValues get_values() override {
float x = this->get_progress_();
float v = x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f);
float v = LightTransitionTransformer::smoothed_progress(this->get_progress());
return LightColorValues::lerp(this->get_start_values_(), this->get_target_values_(), v);
}
bool publish_at_end() override { return false; }
bool is_transition() override { return true; }
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
};
class LightFlashTransformer : public LightTransformer {
@ -80,6 +82,7 @@ class LightFlashTransformer : public LightTransformer {
LightColorValues get_end_values() override { return this->get_start_values_(); }
bool publish_at_end() override { return true; }
bool is_transition() override { return false; }
};
} // namespace light