Add Warp flow, a video effect that sets Warp's noise field drifting
Warp's noise field moves to effects/noise_field.hpp so both share it; Warp renders exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "core/rng.hpp"
|
||||
#include "effects/sampling.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
// One layer of value noise: random heights on a lattice `spacing` pixels
|
||||
// apart, blended smoothly between the lattice points. Positions are read from
|
||||
// (0, 0) to the width and height it was built for.
|
||||
class NoiseLayer {
|
||||
public:
|
||||
NoiseLayer(Rng& rng, int width, int height, double spacing)
|
||||
: spacing_(spacing),
|
||||
cols_(static_cast<int>(width / spacing) + 2),
|
||||
rows_(static_cast<int>(height / spacing) + 2),
|
||||
lattice_(static_cast<std::size_t>(cols_) * rows_) {
|
||||
for (auto& value : lattice_) value = unitDouble(rng.next()) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
// The height at (x, y) in [-1, 1], and its slope per pixel along x and y.
|
||||
void sample(double x, double y, double& value, double& slopeX, double& slopeY) const {
|
||||
const double gx = x / spacing_;
|
||||
const double gy = y / spacing_;
|
||||
const int ix = std::min(static_cast<int>(gx), cols_ - 2);
|
||||
const int iy = std::min(static_cast<int>(gy), rows_ - 2);
|
||||
const double fx = gx - ix;
|
||||
const double fy = gy - iy;
|
||||
const double sx = fx * fx * (3.0 - 2.0 * fx);
|
||||
const double sy = fy * fy * (3.0 - 2.0 * fy);
|
||||
|
||||
const double a = at(ix, iy);
|
||||
const double b = at(ix + 1, iy);
|
||||
const double c = at(ix, iy + 1);
|
||||
const double d = at(ix + 1, iy + 1);
|
||||
const double cross = a - b - c + d;
|
||||
value = a + (b - a) * sx + (c - a) * sy + cross * sx * sy;
|
||||
slopeX = 6.0 * fx * (1.0 - fx) * ((b - a) + cross * sy) / spacing_;
|
||||
slopeY = 6.0 * fy * (1.0 - fy) * ((c - a) + cross * sx) / spacing_;
|
||||
}
|
||||
|
||||
private:
|
||||
double at(int x, int y) const { return lattice_[static_cast<std::size_t>(y) * cols_ + x]; }
|
||||
|
||||
double spacing_;
|
||||
int cols_;
|
||||
int rows_;
|
||||
std::vector<double> lattice_;
|
||||
};
|
||||
|
||||
// Where one layer of a field is read from, against where it is asked for.
|
||||
struct NoiseShift {
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
};
|
||||
|
||||
// Octaves of noise summed, each twice as fine and half as strong as the one
|
||||
// before, and scaled back so the total stays in [-1, 1].
|
||||
class NoiseField {
|
||||
public:
|
||||
NoiseField(Rng& rng, int width, int height, double scale, int octaves) {
|
||||
double spacing = scale;
|
||||
double weight = 1.0;
|
||||
for (int i = 0; i < octaves; ++i) {
|
||||
layers_.emplace_back(rng, width, height, std::max(1.0, spacing));
|
||||
weights_.push_back(weight);
|
||||
total_ += weight;
|
||||
spacing *= 0.5;
|
||||
weight *= 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// As above, with each layer read that far off from (x, y), one shift per
|
||||
// octave -- which is how a field is set moving: its layers slide apart.
|
||||
void sample(double x, double y, double& value, double& slopeX, double& slopeY,
|
||||
const std::vector<NoiseShift>* shifts = nullptr) const {
|
||||
value = slopeX = slopeY = 0.0;
|
||||
for (std::size_t i = 0; i < layers_.size(); ++i) {
|
||||
double v = 0.0, sx = 0.0, sy = 0.0;
|
||||
if (shifts != nullptr) {
|
||||
layers_[i].sample(x + (*shifts)[i].x, y + (*shifts)[i].y, v, sx, sy);
|
||||
} else {
|
||||
layers_[i].sample(x, y, v, sx, sy);
|
||||
}
|
||||
value += v * weights_[i];
|
||||
slopeX += sx * weights_[i];
|
||||
slopeY += sy * weights_[i];
|
||||
}
|
||||
value /= total_;
|
||||
slopeX /= total_;
|
||||
slopeY /= total_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<NoiseLayer> layers_;
|
||||
std::vector<double> weights_;
|
||||
double total_ = 0.0;
|
||||
};
|
||||
|
||||
// The push the warp effects give a pixel: one field for a swirl, which flows
|
||||
// along its contours, two for a push along x and y. The fields are drawn in
|
||||
// full, in that order, when this is built.
|
||||
class WarpField {
|
||||
public:
|
||||
WarpField(Rng& rng, int width, int height, double scale, int octaves, bool swirl)
|
||||
: first_(rng, width, height, scale, octaves), scale_(scale), swirl_(swirl) {
|
||||
if (!swirl) second_.emplace_back(rng, width, height, scale, octaves);
|
||||
}
|
||||
|
||||
// How far the pixel at (x, y) is pushed, at `strength` pixels at most;
|
||||
// `shifts` (one list per field) slides the fields' layers as NoiseField does.
|
||||
void push(double x, double y, double strength, double& dx, double& dy,
|
||||
const std::vector<NoiseShift>* firstShifts = nullptr,
|
||||
const std::vector<NoiseShift>* secondShifts = nullptr) const {
|
||||
double value = 0.0, slopeX = 0.0, slopeY = 0.0;
|
||||
first_.sample(x, y, value, slopeX, slopeY, firstShifts);
|
||||
if (swirl_) {
|
||||
// Along the contour: the slope turned a quarter, which never piles
|
||||
// pixels up or tears them apart. The slope of a field `scale` wide
|
||||
// is about 1/scale, hence the factor.
|
||||
dx = slopeY * scale_ * strength * 0.5;
|
||||
dy = -slopeX * scale_ * strength * 0.5;
|
||||
} else {
|
||||
double other = 0.0;
|
||||
second_.front().sample(x, y, other, slopeX, slopeY, secondShifts);
|
||||
dx = value * strength;
|
||||
dy = other * strength;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
NoiseField first_;
|
||||
std::vector<NoiseField> second_;
|
||||
double scale_;
|
||||
bool swirl_;
|
||||
};
|
||||
|
||||
} // namespace effector
|
||||
Reference in New Issue
Block a user