Using new, bidirectional animation algorithm

Also, some rule tweaks
This commit is contained in:
Daniel Wolf 2016-12-03 20:13:00 +01:00
parent 1e5a21dbfb
commit 3cdc78e889
6 changed files with 155 additions and 247 deletions

View File

@ -223,8 +223,6 @@ add_library(rhubarb-animation
src/animation/mouthAnimation.cpp
src/animation/mouthAnimation.h
src/animation/shapeShorthands.h
src/animation/Viseme.cpp
src/animation/Viseme.h
)
target_include_directories(rhubarb-animation PUBLIC "src/animation")
target_link_libraries(rhubarb-animation

View File

@ -1,52 +0,0 @@
#include "Viseme.h"
VisemeOption::VisemeOption(Shape shape) :
VisemeOption(shape, shape, shape, shape, shape)
{}
VisemeOption::VisemeOption(Shape nearB, Shape nearC, Shape nearD, Shape nearE, Shape nearF) :
VisemeOption(nearB, nearB, nearC, nearD, nearE, nearF, nearB, nearC, nearB)
{}
VisemeOption::VisemeOption(Shape nearA, Shape nearB, Shape nearC, Shape nearD, Shape nearE, Shape nearF, Shape nearG, Shape nearH, Shape nearX) :
shapes{ nearA, nearB, nearC, nearD, nearE, nearF, nearG, nearH, nearX }
{
static_assert(static_cast<int>(Shape::EndSentinel) == 9, "Shape definition has changed.");
}
Shape VisemeOption::getShape(Shape context) const {
return shapes.at(static_cast<int>(context));
}
bool VisemeOption::operator==(const VisemeOption& rhs) const {
return shapes == rhs.shapes;
}
bool VisemeOption::operator!=(const VisemeOption& rhs) const {
return !operator==(rhs);
}
Viseme::Viseme(const VisemeOption& option) :
options{ { 0_cs, option } }
{}
Viseme::Viseme(const VisemeOption& option1, centiseconds threshold, const VisemeOption& option2) :
options{ { 0_cs, option1 }, { threshold, option2 } }
{}
Viseme::Viseme(const VisemeOption& option1, centiseconds threshold1, const VisemeOption& option2, centiseconds threshold2, const VisemeOption& option3) :
options{ { 0_cs, option1 },{ threshold1, option2 }, { threshold2, option3 } }
{}
Shape Viseme::getShape(centiseconds duration, Shape context) const {
VisemeOption option = std::prev(options.upper_bound(duration))->second;
return option.getShape(context);
}
bool Viseme::operator==(const Viseme& rhs) const {
return options == rhs.options;
}
bool Viseme::operator!=(const Viseme& rhs) const {
return !operator==(rhs);
}

View File

@ -1,31 +0,0 @@
#pragma once
#include "Shape.h"
#include "centiseconds.h"
#include <map>
#include <array>
class VisemeOption {
public:
VisemeOption(Shape shape);
VisemeOption(Shape nearB, Shape nearC, Shape nearD, Shape nearE, Shape nearF);
VisemeOption(Shape nearA, Shape nearB, Shape nearC, Shape nearD, Shape nearE, Shape nearF, Shape nearG, Shape nearH, Shape nearX);
Shape getShape(Shape context) const;
bool operator==(const VisemeOption& rhs) const;
bool operator!=(const VisemeOption& rhs) const;
private:
std::array<Shape, 9> shapes;
};
class Viseme {
public:
Viseme(const VisemeOption& option);
Viseme(const VisemeOption& option1, centiseconds threshold, const VisemeOption& option2);
Viseme(const VisemeOption& option1, centiseconds threshold1, const VisemeOption& option2, centiseconds threshold2, const VisemeOption& option3);
Shape getShape(centiseconds duration, Shape context) const;
bool operator==(const Viseme& rhs) const;
bool operator!=(const Viseme& rhs) const;
private:
std::map<centiseconds, VisemeOption> options;
};

View File

@ -7,6 +7,8 @@ using std::chrono::duration_cast;
using boost::algorithm::clamp;
using boost::optional;
using std::array;
using std::pair;
using std::map;
constexpr size_t shapeValueCount = static_cast<size_t>(Shape::EndSentinel);
@ -16,10 +18,14 @@ Shape getBasicShape(Shape shape) {
}
Shape relax(Shape shape) {
static constexpr array<Shape, shapeValueCount> relaxedShapes = make_array(A, B, C, C, C, B, B, C, X);
static constexpr array<Shape, shapeValueCount> relaxedShapes = make_array(A, B, B, C, C, B, X, B, X);
return relaxedShapes[static_cast<size_t>(shape)];
}
Shape getRelaxedBridge(Shape lhs, Shape rhs) {
return lhs == rhs ? lhs : relax(lhs);
}
Shape getClosestShape(Shape reference, ShapeSet shapes) {
if (shapes.empty()) {
throw std::invalid_argument("Cannot select from empty set of shapes.");
@ -48,12 +54,26 @@ Shape getClosestShape(Shape reference, ShapeSet shapes) {
throw std::invalid_argument("Unable to find closest shape.");
}
optional<pair<Shape, TweenTiming>> getTween(Shape first, Shape second) {
static const map<pair<Shape, Shape>, pair<Shape, TweenTiming>> lookup{
{{A, D}, {C, TweenTiming::Late}}, {{D, A}, {C, TweenTiming::Early}},
{{B, D}, {C, TweenTiming::Centered}}, {{D, B}, {C, TweenTiming::Centered}},
{{G, D}, {C, TweenTiming::Late}}, {{D, G}, {C, TweenTiming::Early}},
{{X, D}, {C, TweenTiming::Early}}, {{D, X}, {C, TweenTiming::Late}},
{{C, F}, {E, TweenTiming::Centered}}, {{F, C}, {E, TweenTiming::Centered}},
{{D, F}, {E, TweenTiming::Centered}}, {{F, D}, {E, TweenTiming::Centered}},
{{H, F}, {E, TweenTiming::Late}}, {{F, H}, {E, TweenTiming::Early}},
};
auto it = lookup.find({first, second});
return it != lookup.end() ? it->second : optional<pair<Shape, TweenTiming>>();
}
ShapeRule::ShapeRule(const ShapeSet& regularShapes, const ShapeSet& alternativeShapes) :
regularShapes(regularShapes),
alternativeShapes(alternativeShapes)
{}
Timeline<ShapeRule> getShapeRule(Phone phone, centiseconds duration, centiseconds previousDuration) {
Timeline<ShapeRule> getShapeRules(Phone phone, centiseconds duration, centiseconds previousDuration) {
// Returns a timeline with a single shape set
auto single = [duration](ShapeRule value) {
return Timeline<ShapeRule> {{0_cs, duration, value}};
@ -79,14 +99,19 @@ Timeline<ShapeRule> getShapeRule(Phone phone, centiseconds duration, centisecond
};
};
// Returns the result of `getShapeRule` when called with identical arguments
// Returns the result of `getShapeRules` when called with identical arguments
// except for a different phone.
auto like = [duration, previousDuration](Phone referencePhone) {
return getShapeRule(referencePhone, duration, previousDuration);
return getShapeRules(referencePhone, duration, previousDuration);
};
static const ShapeRule any{{A, B, C, D, E, F, G, H, X}};
// Note:
// The shapes {A, B, G, X} are very similar. You should avoid regular shape sets containing more than one of these shapes.
// Otherwise, the resulting shape may be more or less random and might not be a good fit.
// As an exception, a very flexible rule may contain *all* these shapes.
switch (phone) {
case Phone::AO: return single({{E}});
case Phone::AA: return single({{D}});
@ -108,9 +133,9 @@ Timeline<ShapeRule> getShapeRule(Phone phone, centiseconds duration, centisecond
case Phone::P:
case Phone::B: return plosive({{A}}, any);
case Phone::T:
case Phone::D: return plosive({{B, C, F, G, H}}, any);
case Phone::D: return plosive({{B, C, F, H}}, {{B, C, D, E, F, G, H}});
case Phone::K:
case Phone::G: return plosive({{B, C, E, F, G, H}}, any);
case Phone::G: return plosive({{B, C, E, F, H}}, any);
case Phone::CH:
case Phone::JH: return single({{B, F}});
case Phone::F:
@ -123,9 +148,9 @@ Timeline<ShapeRule> getShapeRule(Phone phone, centiseconds duration, centisecond
case Phone::ZH: return single({{B, F}});
case Phone::HH: return single(any);
case Phone::M: return single({{A}});
case Phone::N: return single({{B, C, F, G, H}});
case Phone::NG: return single({{B, C, E, F, G}});
case Phone::L: return duration < 20_cs ? single({{B, C, D, E, F, G, H}}) : single({{H}});
case Phone::N: return single({{B, C, F, H}});
case Phone::NG: return single({{B, C, E, F}});
case Phone::L: return duration < 20_cs ? single({{B, C, D, E, F, H}}) : single({{H}});
case Phone::R: return single({{B, C, E, F}});
case Phone::Y: return single({{B, C, F}});
case Phone::W: return single({{F}});

View File

@ -11,12 +11,30 @@ Shape getBasicShape(Shape shape);
// Returns the mouth shape that results from relaxing the specified shape.
Shape relax(Shape shape);
// Returns the mouth shape to use for *short* pauses between words.
Shape getRelaxedBridge(Shape lhs, Shape rhs);
// A set of mouth shapes that can be used to represent a certain sound
using ShapeSet = std::set<Shape>;
// Gets the shape from a non-empty set of shapes that most closely resembles a reference shape.
Shape getClosestShape(Shape reference, ShapeSet shapes);
// Indicates how to time a tween between two mouth shapes
enum class TweenTiming {
// Tween should end at the original transition
Early,
// Tween should overlap both original mouth shapes equally
Centered,
// Tween should begin at the original transition
Late
};
// Returns the tween shape and timing to use to transition between the specified two mouth shapes.
boost::optional<std::pair<Shape, TweenTiming>> getTween(Shape first, Shape second);
// A struct describing the possible shapes to use during a given time range.
struct ShapeRule {
ShapeRule(const ShapeSet& regularShapes, const ShapeSet& alternativeShapes = {});
@ -29,9 +47,18 @@ struct ShapeRule {
// This is a set of zero or more shapes that may be used in these cases.
// In contrast to the regular shapes, this set should only contain shapes that can be used regardless of the surrounding shapes.
ShapeSet alternativeShapes;
bool operator==(const ShapeRule& rhs) const {
return regularShapes == rhs.regularShapes && alternativeShapes == rhs.alternativeShapes;
}
bool operator!=(const ShapeRule& rhs) const {
return !operator==(rhs);
}
};
// Returns the shape rule(s) to use for a given phone.
// The resulting timeline will always cover the entire duration of the phone (starting at 0 cs).
// It may extend into the negative time range if animation is required prior to the sound being heard.
Timeline<ShapeRule> getShapeRule(Phone phone, centiseconds duration, centiseconds previousDuration);
Timeline<ShapeRule> getShapeRules(Phone phone, centiseconds duration, centiseconds previousDuration);

View File

@ -3,143 +3,23 @@
#include <unordered_set>
#include <unordered_map>
#include <boost/algorithm/clamp.hpp>
#include "Viseme.h"
#include <boost/range/adaptor/transformed.hpp>
#include "timedLogging.h"
#include "shapeShorthands.h"
#include "animationRules.h"
using std::map;
using std::unordered_set;
using std::unordered_map;
using std::vector;
using boost::optional;
using boost::make_optional;
using std::chrono::duration_cast;
using boost::algorithm::clamp;
using boost::adaptors::transformed;
using std::pair;
using std::tuple;
Timeline<Viseme> animate(optional<Phone> phone, centiseconds duration, centiseconds previousPhoneDuration) {
auto single = [&](Viseme viseme) {
return Timeline<Viseme>{
{ 0_cs, duration, viseme }
};
};
auto diphtong = [&](Viseme first, Viseme second) {
centiseconds firstDuration = duration_cast<centiseconds>(duration * 0.6);
return Timeline<Viseme>{
{ 0_cs, firstDuration, first },
{ firstDuration, duration, second }
};
};
auto bilabialStop = [&]() {
centiseconds maxDuration = 12_cs;
centiseconds leftOverlap = clamp(previousPhoneDuration / 2, 4_cs, maxDuration);
centiseconds rightOverlap = min(duration, maxDuration - leftOverlap);
return Timeline<Viseme>{
{ -leftOverlap, rightOverlap, { A } },
{ rightOverlap, duration, {{ B, C, D, E, F }} }
};
};
if (!phone) return single({ X });
switch (*phone) {
case Phone::AO: return single({ E });
case Phone::AA: return single({ D });
case Phone::IY: return single({ B });
case Phone::UW: return single({ F });
case Phone::EH: return single({ { C }, 20_cs, { D } });
case Phone::IH: return single({ B });
case Phone::UH: return single({ E });
case Phone::AH: return single({ C });
case Phone::Schwa: return single({ { B, C, D, E, F } });
case Phone::AE: return single({ D });
case Phone::EY: return diphtong({ { C }, 20_cs, { D } }, { B });
case Phone::AY: return diphtong({ D }, { B });
case Phone::OW: return diphtong({ E }, { F });
case Phone::AW: return diphtong({ D }, { F });
case Phone::OY: return diphtong({ F }, { B });
case Phone::ER: return single({ { B }, 7_cs, { E } });
case Phone::P:
case Phone::B: return bilabialStop();
case Phone::T:
case Phone::D:
case Phone::K: return single({ { B, B, B, B, F } });
case Phone::G: return single({ { B, C, C, E, F } });
case Phone::CH:
case Phone::JH: return single({ { B, B, B, B, F } });
case Phone::F:
case Phone::V: return single({ G });
case Phone::TH:
case Phone::DH:
case Phone::S:
case Phone::Z:
case Phone::SH:
case Phone::ZH: return single({ { B, B, B, B, F } });
case Phone::HH: return single({ { B, C, D, E, F } });
case Phone::M: return single({ A });
case Phone::N: return single({ { B, C, C, C, F } });
case Phone::NG: return single({ { B, C, D, E, F } });
case Phone::L: return single({ { H, H, H, E, F } });
case Phone::R: return single({ { B, B, B, B, F } });
case Phone::Y: return single({ B });
case Phone::W: return single({ F });
case Phone::Breath:
case Phone::Cough:
case Phone::Smack: return single({ C });
case Phone::Noise: return single({ B });
default:
throw std::invalid_argument("Unexpected phone.");
}
}
enum class TweenTiming {
Early,
Centered,
Late
};
optional<pair<Shape, TweenTiming>> getTween(Shape first, Shape second) {
static const map<pair<Shape, Shape>, pair<Shape, TweenTiming>> lookup {
{ { A, D }, { C, TweenTiming::Late } }, { { D, A },{ C, TweenTiming::Early } },
{ { B, D }, { C, TweenTiming::Centered } }, { { D, B },{ C, TweenTiming::Centered } },
{ { G, D }, { C, TweenTiming::Late } }, { { D, G },{ C, TweenTiming::Early } },
{ { X, D }, { C, TweenTiming::Early } }, { { D, X },{ C, TweenTiming::Late } },
{ { C, F }, { E, TweenTiming::Centered } }, { { F, C },{ E, TweenTiming::Centered } },
{ { D, F }, { E, TweenTiming::Centered } }, { { F, D },{ E, TweenTiming::Centered } },
{ { H, F }, { E, TweenTiming::Late } }, { { F, H },{ E, TweenTiming::Early } },
};
auto it = lookup.find({ first, second });
return it != lookup.end() ? it->second : optional<pair<Shape, TweenTiming>>();
}
// Returns the mouth shape to use for *short* pauses between words.
Shape getRelaxedShape(Shape lhs, Shape rhs) {
if (lhs == rhs) {
return lhs;
}
// Lookup that contains relaxed versions of mouth shapes
static const map<Shape, Shape> relaxed {
{ A, A },
{ B, B },
{ C, C },
{ D, C },
{ E, C },
{ F, B },
{ G, X },
{ H, C },
{ X, X }
};
assert(relaxed.size() == static_cast<size_t>(Shape::EndSentinel));
return relaxed.at(lhs);
}
Timeline<Shape> createTweens(ContinuousTimeline<Shape> shapes) {
centiseconds minTweenDuration = 4_cs;
centiseconds maxTweenDuration = 10_cs;
@ -196,7 +76,7 @@ Timeline<Shape> animatePauses(const ContinuousTimeline<Shape>& shapes) {
const centiseconds maxPausedOpenMouthDuration = 35_cs;
const TimeRange timeRange = pause.getTimeRange();
if (timeRange.getDuration() <= maxPausedOpenMouthDuration) {
result.set(timeRange, getRelaxedShape(lhs.getValue(), rhs.getValue()));
result.set(timeRange, getRelaxedBridge(lhs.getValue(), rhs.getValue()));
}
});
@ -209,53 +89,114 @@ Timeline<Shape> animatePauses(const ContinuousTimeline<Shape>& shapes) {
if (isClosed(secondLast.getValue()) && !isClosed(last.getValue()) && lastDuration < minOpenDuration) {
const centiseconds minSpillDuration = 20_cs;
centiseconds spillDuration = std::min(minSpillDuration, pause.getDuration());
result.set(pause.getStart(), pause.getStart() + spillDuration, getRelaxedShape(last.getValue(), X));
result.set(pause.getStart(), pause.getStart() + spillDuration, getRelaxedBridge(last.getValue(), X));
}
});
return result;
}
ContinuousTimeline<Shape> animate(const BoundedTimeline<Phone> &phones) {
// Convert phones to continuous timeline so that silences aren't skipped when iterating
ContinuousTimeline<optional<Phone>> continuousPhones(phones.getRange(), boost::none);
for (const auto& timedPhone : phones) {
continuousPhones.set(timedPhone.getTimeRange(), timedPhone.getValue());
template<typename T>
ContinuousTimeline<optional<T>> boundedTimelinetoContinuousOptional(const BoundedTimeline<T>& timeline) {
return {
timeline.getRange(), boost::none,
timeline | transformed([](const Timed<T>& timedValue) { return Timed<optional<T>>(timedValue.getTimeRange(), timedValue.getValue()); })
};
}
// Create timeline of visemes
ContinuousTimeline<Viseme> visemes(phones.getRange(), { X });
centiseconds previousPhoneDuration = 0_cs;
ContinuousTimeline<ShapeRule> getShapeRules(const BoundedTimeline<Phone>& phones) {
// Convert to continuous timeline so that silences aren't skipped when iterating
auto continuousPhones = boundedTimelinetoContinuousOptional(phones);
// Create timeline of shape rules
ContinuousTimeline<ShapeRule> shapeRules(phones.getRange(), {{X}});
centiseconds previousDuration = 0_cs;
for (const auto& timedPhone : continuousPhones) {
// Animate one phone
optional<Phone> phone = timedPhone.getValue();
centiseconds duration = timedPhone.getDuration();
Timeline<Viseme> phoneVisemes = animate(phone, duration, previousPhoneDuration);
if (phone) {
// Animate one phone
Timeline<ShapeRule> phoneShapeRules = getShapeRules(*phone, duration, previousDuration);
// Result timing is relative to phone. Make absolute.
phoneVisemes.shift(timedPhone.getStart());
phoneShapeRules.shift(timedPhone.getStart());
// Copy to viseme timeline
for (const auto& timedViseme : phoneVisemes) {
visemes.set(timedViseme);
// Copy to timeline.
// Later shape rules may overwrite earlier ones if overlapping.
for (const auto& timedShapeRule : phoneShapeRules) {
shapeRules.set(timedShapeRule);
}
}
previousPhoneDuration = duration;
previousDuration = duration;
}
// Create timeline of shapes.
// Iterate visemes in *reverse* order so we always know what shape will follow.
ContinuousTimeline<Shape> shapes(phones.getRange(), X);
Shape lastShape = X;
for (auto it = visemes.rbegin(); it != visemes.rend(); ++it) {
Viseme viseme = it->getValue();
return shapeRules;
}
// Convert viseme to phone
Shape shape = viseme.getShape(it->getDuration(), lastShape);
// Create timeline of shape rules using a bidirectional algorithm.
// Here's a rough sketch:
//
// * Most consonants result in shape sets with multiple options; most vowels have only one shape option.
// * When speaking, we tend to slur mouth shapes into each other. So we animate from start to end,
// always choosing a shape from the current set that resembles the last shape and is somewhat relaxed.
// * When speaking, we anticipate vowels, trying to form their shape before the actual vowel.
// So whenever we come across a one-shape set, we backtrack a little, spreating that shape to the left.
ContinuousTimeline<Shape> animate(const ContinuousTimeline<ShapeSet>& shapeSets) {
ContinuousTimeline<Shape> shapes(shapeSets.getRange(), X);
Shape referenceShape = X;
// Animate forwards
centiseconds lastAnticipatedShapeStart = -1_cs;
for (auto it = shapeSets.begin(); it != shapeSets.end(); ++it) {
const ShapeSet shapeSet = it->getValue();
const Shape shape = getClosestShape(referenceShape, shapeSet);
shapes.set(it->getTimeRange(), shape);
const bool anticipateShape = shapeSet.size() == 1 && *shapeSet.begin() != X;
if (anticipateShape) {
// Animate backwards a little
const Shape anticipatedShape = shape;
const centiseconds anticipatedShapeStart = it->getStart();
referenceShape = anticipatedShape;
for (auto reverseIt = it; reverseIt != shapeSets.begin(); ) {
--reverseIt;
lastShape = shape;
// Make sure we haven't animated too far back
centiseconds anticipatingShapeStart = reverseIt->getStart();
if (anticipatingShapeStart == lastAnticipatedShapeStart) break;
const centiseconds maxAnticipationDuration = 20_cs;
const centiseconds anticipationDuration = anticipatedShapeStart - anticipatingShapeStart;
if (anticipationDuration > maxAnticipationDuration) break;
// Make sure the new, backwards-animated shape still resembles the anticipated shape
const Shape anticipatingShape = getClosestShape(referenceShape, reverseIt->getValue());
if (getBasicShape(anticipatingShape) != getBasicShape(anticipatedShape)) break;
// Overwrite forward-animated shape with backwards-animated, anticipating shape
shapes.set(reverseIt->getTimeRange(), anticipatingShape);
referenceShape = anticipatingShape;
}
lastAnticipatedShapeStart = anticipatedShapeStart;
}
referenceShape = anticipateShape ? shape : relax(shape);
}
return shapes;
}
ContinuousTimeline<Shape> animate(const BoundedTimeline<Phone> &phones) {
// Create timeline of shape rules
ContinuousTimeline<ShapeRule> shapeRules = getShapeRules(phones);
// Take only the regular shapes from each shape rule. Alternative shapes will be implemented later.
ContinuousTimeline<ShapeSet> shapeSets(
shapeRules.getRange(), {{X}},
shapeRules | transformed([](const Timed<ShapeRule>& timedRule) { return Timed<ShapeSet>(timedRule.getTimeRange(), timedRule.getValue().regularShapes); }));
// Animate
ContinuousTimeline<Shape> shapes = animate(shapeSets);
// Animate pauses
Timeline<Shape> pauses = animatePauses(shapes);