#include "timingOptimization.h" #include "time/timedLogging.h" #include #include #include #include "ShapeRule.h" using std::string; using std::map; string getShapesString(const JoiningContinuousTimeline& shapes) { string result; for (const auto& timedShape : shapes) { if (result.size()) { result.append(" "); } result.append(boost::lexical_cast(timedShape.getValue())); } return result; } Shape getRepresentativeShape(const JoiningTimeline& timeline) { if (timeline.empty()) { throw std::invalid_argument("Cannot determine representative shape from empty timeline."); } // Collect candidate shapes with weights map candidateShapeWeights; for (const auto& timedShape : timeline) { candidateShapeWeights[timedShape.getValue()] += timedShape.getDuration(); } // Select shape with highest total duration within the candidate range const Shape bestShape = std::max_element( candidateShapeWeights.begin(), candidateShapeWeights.end(), [](auto a, auto b) { return a.second < b.second; } )->first; // Shapes C and D are similar, but D is more interesting. const bool substituteD = bestShape == Shape::C && candidateShapeWeights[Shape::D] > 0_cs; return substituteD ? Shape::D : bestShape; } struct ShapeReduction { ShapeReduction(const JoiningTimeline& sourceShapes) : sourceShapes(sourceShapes), shape(getRepresentativeShape(sourceShapes)) {} ShapeReduction(const JoiningTimeline& sourceShapes, TimeRange candidateRange) : ShapeReduction(JoiningBoundedTimeline(candidateRange, sourceShapes)) {} JoiningTimeline sourceShapes; Shape shape; }; // Returns a time range of candidate shapes for the next shape to draw. // Guaranteed to be non-empty. TimeRange getNextMinimalCandidateRange(const JoiningContinuousTimeline& sourceShapes, const TimeRange targetRange, const centiseconds writePosition) { if (sourceShapes.empty()) { throw std::invalid_argument("Cannot determine candidate range for empty source timeline."); } // Too short, and and we get flickering. Too long, and too many shapes are lost. // Good values turn out to be 5 to 7 cs, with 7 cs sometimes looking just marginally better. const centiseconds minShapeDuration = 7_cs; // If the remaining time can hold more than one shape, but not two: split it evenly const centiseconds remainingTargetDuration = writePosition - targetRange.getStart(); const bool canFitOneOrLess = remainingTargetDuration <= minShapeDuration; const bool canFitTwo = remainingTargetDuration >= 2 * minShapeDuration; const centiseconds duration = canFitOneOrLess || canFitTwo ? minShapeDuration : remainingTargetDuration / 2; TimeRange candidateRange(writePosition - duration, writePosition); if (writePosition == targetRange.getEnd()) { // This is the first iteration. // Extend the candidate range to the right in order to consider all source shapes after the target range. candidateRange.setEndIfLater(sourceShapes.getRange().getEnd()); } if (candidateRange.getStart() >= sourceShapes.getRange().getEnd()) { // We haven't reached the source range yet. // Extend the candidate range to the left in order to encompass the right-most source shape. candidateRange.setStart(sourceShapes.rbegin()->getStart()); } if (candidateRange.getEnd() <= sourceShapes.getRange().getStart()) { // We're past the source range. This can happen in corner cases. // Extend the candidate range to the right in order to encompass the left-most source shape candidateRange.setEnd(sourceShapes.begin()->getEnd()); } return candidateRange; } ShapeReduction getNextShapeReduction(const JoiningContinuousTimeline& sourceShapes, const TimeRange targetRange, centiseconds writePosition) { // Determine the next time range of candidate shapes. Consider two scenarios: // ... the shortest-possible candidate range const ShapeReduction minReduction(sourceShapes, getNextMinimalCandidateRange(sourceShapes, targetRange, writePosition)); // ... a candidate range extended to the left to fully encompass its left-most shape const ShapeReduction extendedReduction(sourceShapes, {minReduction.sourceShapes.begin()->getStart(), minReduction.sourceShapes.getRange().getEnd()}); // Determine the shape that might be picked *next* if we choose the shortest-possible candidate range now const ShapeReduction nextReduction(sourceShapes, getNextMinimalCandidateRange(sourceShapes, targetRange, minReduction.sourceShapes.getRange().getStart())); const bool minEqualsExtended = minReduction.shape == extendedReduction.shape; const bool extendedIsSpecial = extendedReduction.shape != minReduction.shape && extendedReduction.shape != nextReduction.shape; return minEqualsExtended || extendedIsSpecial ? extendedReduction : minReduction; } // Modifies the timing of the given animation to fit into the specified target time range without jitter. JoiningContinuousTimeline retime(const JoiningContinuousTimeline& sourceShapes, const TimeRange targetRange) { logTimedEvent("segment", targetRange, getShapesString(sourceShapes)); JoiningContinuousTimeline result(targetRange, Shape::X); if (sourceShapes.empty()) return result; // Animate backwards centiseconds writePosition = targetRange.getEnd(); while (writePosition > targetRange.getStart()) { // Decide which shape to show next, possibly discarding short shapes const ShapeReduction shapeReduction = getNextShapeReduction(sourceShapes, targetRange, writePosition); // Determine how long to display the shape TimeRange targetShapeRange(shapeReduction.sourceShapes.getRange()); if (targetShapeRange.getStart() <= sourceShapes.getRange().getStart()) { // We've used up the left-most source shape. Fill the entire remaining target range. targetShapeRange.setStartIfEarlier(targetRange.getStart()); } targetShapeRange.trimRight(writePosition); // Draw shape result.set(targetShapeRange, shapeReduction.shape); writePosition = targetShapeRange.getStart(); } return result; } JoiningContinuousTimeline retime(const JoiningContinuousTimeline& animation, TimeRange sourceRange, TimeRange targetRange) { const auto sourceShapes = JoiningContinuousTimeline(sourceRange, Shape::X, animation); return retime(sourceShapes, targetRange); } enum class MouthState { Idle, Closed, Open }; JoiningContinuousTimeline optimizeTiming(const JoiningContinuousTimeline& animation) { // Identify segments with idle, closed, and open mouth shapes JoiningContinuousTimeline segments(animation.getRange(), MouthState::Idle); for (const auto& timedShape : animation) { const Shape shape = timedShape.getValue(); const MouthState mouthState = shape == Shape::X ? MouthState::Idle : shape == Shape::A ? MouthState::Closed : MouthState::Open; segments.set(timedShape.getTimeRange(), mouthState); } // The minimum duration a segment of open or closed mouth shapes must have to visually register const centiseconds minSegmentDuration = 8_cs; // The maximum amount by which the start of a shape can be brought forward const centiseconds maxExtensionDuration = 6_cs; // Make sure all open and closed segments are long enough to register visually. JoiningContinuousTimeline result(animation.getRange(), Shape::X); // ... we're filling the result timeline from right to left, so `resultStart` points to the earliest shape already written centiseconds resultStart = result.getRange().getEnd(); for (auto segmentIt = segments.rbegin(); segmentIt != segments.rend(); ++segmentIt) { // We don't care about idle shapes at this point. if (segmentIt->getValue() == MouthState::Idle) continue; resultStart = std::min(segmentIt->getEnd(), resultStart); if (resultStart - segmentIt->getStart() >= minSegmentDuration) { // The segment is long enough; we don't have to extend it to the left. const TimeRange targetRange(segmentIt->getStart(), resultStart); const auto retimedSegment = retime(animation, segmentIt->getTimeRange(), targetRange); for (const auto& timedShape : retimedSegment) { result.set(timedShape); } resultStart = targetRange.getStart(); } else { // The segment is too short; we have to extend it to the left. // Find all adjacent segments to our left that are also too short, then distribute them evenly. const auto begin = segmentIt; auto end = std::next(begin); while (end != segments.rend() && end->getValue() != MouthState::Idle && end->getDuration() < minSegmentDuration) ++end; // Determine how much we should extend the entire set of short segments to the left const size_t shortSegmentCount = std::distance(begin, end); const centiseconds desiredDuration = minSegmentDuration * shortSegmentCount; const centiseconds currentDuration = begin->getEnd() - std::prev(end)->getStart(); const centiseconds desiredExtensionDuration = desiredDuration - currentDuration; const centiseconds availableExtensionDuration = end != segments.rend() ? end->getDuration() - 1_cs : 0_cs; const centiseconds extensionDuration = std::min({desiredExtensionDuration, availableExtensionDuration, maxExtensionDuration}); // Distribute available time range evenly among all short segments const centiseconds shortSegmentsTargetStart = std::prev(end)->getStart() - extensionDuration; for (auto shortSegmentIt = begin; shortSegmentIt != end; ++shortSegmentIt) { size_t remainingShortSegmentCount = std::distance(shortSegmentIt, end); const centiseconds segmentDuration = (resultStart - shortSegmentsTargetStart) / remainingShortSegmentCount; const TimeRange segmentTargetRange(resultStart - segmentDuration, resultStart); const auto retimedSegment = retime(animation, shortSegmentIt->getTimeRange(), segmentTargetRange); for (const auto& timedShape : retimedSegment) { result.set(timedShape); } resultStart = segmentTargetRange.getStart(); } segmentIt = std::prev(end); } } return result; }