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:
@@ -17,6 +17,10 @@ commit that made it. The interface shows this file under Changelog in its menu.
|
|||||||
new echo fading in at the far end as the nearest melts into the picture.
|
new echo fading in at the far end as the nearest melts into the picture.
|
||||||
On the command line it is `-e tunnel` with `--tunnel-speed`,
|
On the command line it is `-e tunnel` with `--tunnel-speed`,
|
||||||
`--tunnel-scale`, `--tunnel-direction` and the rest.
|
`--tunnel-scale`, `--tunnel-direction` and the rest.
|
||||||
|
- Warp flow, a video effect: the picture pushed or swirled through a field
|
||||||
|
of noise as Warp does, with the field drifting so the picture ripples like
|
||||||
|
heat haze or water. On the command line it is `-e flow` with
|
||||||
|
`--flow-strength`, `--flow-travel`, `--flow-speed` and the rest.
|
||||||
- A Quality choice under Video on Setup: Small file, Standard or High, for
|
- A Quality choice under Video on Setup: Small file, Standard or High, for
|
||||||
how the saved video is encoded. It is kept in the project, and on the
|
how the saved video is encoded. It is kept in the project, and on the
|
||||||
command line it is `--video-quality small|standard|high`. Standard writes
|
command line it is `--video-quality small|standard|high`. Standard writes
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ add_library(effector_core STATIC
|
|||||||
src/effects/channelsplit.cpp
|
src/effects/channelsplit.cpp
|
||||||
src/effects/corrupt.cpp
|
src/effects/corrupt.cpp
|
||||||
src/effects/feedback.cpp
|
src/effects/feedback.cpp
|
||||||
|
src/effects/flow.cpp
|
||||||
src/effects/grain.cpp
|
src/effects/grain.cpp
|
||||||
src/effects/monochrome.cpp
|
src/effects/monochrome.cpp
|
||||||
src/effects/overlay.cpp
|
src/effects/overlay.cpp
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "effects/channelsplit.hpp"
|
#include "effects/channelsplit.hpp"
|
||||||
#include "effects/corrupt.hpp"
|
#include "effects/corrupt.hpp"
|
||||||
#include "effects/feedback.hpp"
|
#include "effects/feedback.hpp"
|
||||||
|
#include "effects/flow.hpp"
|
||||||
#include "effects/grain.hpp"
|
#include "effects/grain.hpp"
|
||||||
#include "effects/monochrome.hpp"
|
#include "effects/monochrome.hpp"
|
||||||
#include "effects/overlay.hpp"
|
#include "effects/overlay.hpp"
|
||||||
@@ -43,6 +44,7 @@ const std::vector<EffectPtr>& effects() {
|
|||||||
std::make_shared<TrackingEffect>(),
|
std::make_shared<TrackingEffect>(),
|
||||||
std::make_shared<TileSwapEffect>(),
|
std::make_shared<TileSwapEffect>(),
|
||||||
std::make_shared<TunnelEffect>(),
|
std::make_shared<TunnelEffect>(),
|
||||||
|
std::make_shared<FlowEffect>(),
|
||||||
};
|
};
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#include "effects/flow.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/parallel.hpp"
|
||||||
|
#include "effects/noise_field.hpp"
|
||||||
|
#include "effects/sampling.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
const std::vector<ParamSpec>& FlowEffect::params() const {
|
||||||
|
static const std::vector<ParamSpec> specs = {
|
||||||
|
ParamSpec::makeInt("flow_strength", "Strength", 30, 0, 4096,
|
||||||
|
"How far in pixels the field pushes the image at its strongest."),
|
||||||
|
ParamSpec::makeInt("flow_scale", "Scale", 160, 4, 8192,
|
||||||
|
"Size in pixels of the largest features in the field."),
|
||||||
|
ParamSpec::makeInt("flow_octaves", "Octaves", 3, 1, 6,
|
||||||
|
"Finer layers of noise laid over the first; more give a turbulent "
|
||||||
|
"edge to the flow."),
|
||||||
|
ParamSpec::makeChoice("flow_mode", "Mode", "displace", {"displace", "swirl"},
|
||||||
|
"displace pushes along the field; swirl flows along its contour "
|
||||||
|
"lines, which curls the image into eddies."),
|
||||||
|
ParamSpec::makeInt("flow_travel", "Travel", 80, 0, 4096,
|
||||||
|
"Across how many pixels the field drifts in each cycle; the finer "
|
||||||
|
"layers drift half as far and turn faster."),
|
||||||
|
ParamSpec::makeDouble("flow_speed", "Speed", 0.25, 0.0, 10.0,
|
||||||
|
"Cycles per second, rounded so that the clip loops without a jump; "
|
||||||
|
"0 holds still.",
|
||||||
|
0.05, 2),
|
||||||
|
};
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Settings {
|
||||||
|
double strength;
|
||||||
|
double scale;
|
||||||
|
int octaves;
|
||||||
|
bool swirl;
|
||||||
|
int travel;
|
||||||
|
double speed;
|
||||||
|
|
||||||
|
explicit Settings(const ParamValues& values)
|
||||||
|
: strength(static_cast<double>(std::max(0L, paramInt(values, "flow_strength", 30)))),
|
||||||
|
scale(static_cast<double>(std::max(4L, paramInt(values, "flow_scale", 160)))),
|
||||||
|
octaves(static_cast<int>(std::clamp(paramInt(values, "flow_octaves", 3), 1L, 6L))),
|
||||||
|
swirl(paramChoice(values, "flow_mode", "displace") == "swirl"),
|
||||||
|
travel(static_cast<int>(std::max(0L, paramInt(values, "flow_travel", 80)))),
|
||||||
|
speed(std::max(0.0, paramDouble(values, "flow_speed", 0.25))) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Which way round, and from where, one layer of a field drifts.
|
||||||
|
struct Orbit {
|
||||||
|
double start; // degrees
|
||||||
|
double turn; // +1 or -1
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Image FlowEffect::animate(Image input, const ParamValues& values, const Moment& at) const {
|
||||||
|
const Settings settings(values);
|
||||||
|
const int width = input.width();
|
||||||
|
const int height = input.height();
|
||||||
|
if (settings.strength <= 0.0) return input;
|
||||||
|
|
||||||
|
// What stays put for the whole clip: the field, drawn a circle's radius
|
||||||
|
// larger on every side than the picture, so it can drift without running
|
||||||
|
// out, and where each of its layers starts its circle, and which way.
|
||||||
|
const double reach = settings.travel * 0.5;
|
||||||
|
const int margin = static_cast<int>(reach) + 2;
|
||||||
|
Rng steady = at.steady();
|
||||||
|
const WarpField field(steady, width + 2 * margin, height + 2 * margin, settings.scale,
|
||||||
|
settings.octaves, settings.swirl);
|
||||||
|
std::vector<Orbit> orbits(static_cast<std::size_t>(settings.octaves) * 2);
|
||||||
|
for (Orbit& orbit : orbits) {
|
||||||
|
orbit.start = unitDouble(steady.next()) * 360.0;
|
||||||
|
orbit.turn = (steady.next() >> 63) != 0 ? 1.0 : -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where this frame is in time: layer i has gone i + 1 times round a
|
||||||
|
// circle half as wide as the one before in a whole cycle.
|
||||||
|
const double t = settings.speed > 0.0 ? at.cycle(settings.speed) : 0.0;
|
||||||
|
std::vector<NoiseShift> firstShifts(static_cast<std::size_t>(settings.octaves));
|
||||||
|
std::vector<NoiseShift> secondShifts(static_cast<std::size_t>(settings.octaves));
|
||||||
|
double radius = reach;
|
||||||
|
for (int i = 0; i < settings.octaves; ++i) {
|
||||||
|
for (int f = 0; f < 2; ++f) {
|
||||||
|
const Orbit& orbit = orbits[static_cast<std::size_t>(i * 2 + f)];
|
||||||
|
double sine = 0.0;
|
||||||
|
double cosine = 1.0;
|
||||||
|
sinCosDegrees(orbit.start + orbit.turn * (i + 1) * 360.0 * t, sine, cosine);
|
||||||
|
NoiseShift& shift = (f == 0 ? firstShifts : secondShifts)[static_cast<std::size_t>(i)];
|
||||||
|
shift.x = margin + radius * cosine;
|
||||||
|
shift.y = margin + radius * sine;
|
||||||
|
}
|
||||||
|
radius *= 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image frame(width, height);
|
||||||
|
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
||||||
|
for (std::size_t row = begin; row < end; ++row) {
|
||||||
|
std::uint8_t* out = frame.row(static_cast<int>(row));
|
||||||
|
const double y = static_cast<double>(row);
|
||||||
|
for (int col = 0; col < width; ++col) {
|
||||||
|
const double x = col;
|
||||||
|
double dx = 0.0;
|
||||||
|
double dy = 0.0;
|
||||||
|
field.push(x, y, settings.strength, dx, dy, &firstShifts, &secondShifts);
|
||||||
|
sampleBilinear(input, x + dx, y + dy, out + static_cast<std::size_t>(col) * 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "effects/video_effect.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// The warp effect in motion: the same field of noise pushes or swirls the
|
||||||
|
// picture, and its layers drift, each round a circle of its own, so the
|
||||||
|
// picture ripples like heat haze or water under wind.
|
||||||
|
//
|
||||||
|
// Every layer of the field goes round its circle a whole number of times in a
|
||||||
|
// cycle -- the finer layers smaller and faster -- so the field, and with it
|
||||||
|
// the clip, comes back to where it started without a jump.
|
||||||
|
class FlowEffect : public VideoEffect {
|
||||||
|
public:
|
||||||
|
std::string name() const override { return "flow"; }
|
||||||
|
std::string title() const override { return "Warp flow"; }
|
||||||
|
std::string description() const override {
|
||||||
|
return "Pushes the picture through a field of noise, as Warp does, and sets the field "
|
||||||
|
"drifting so the picture ripples.";
|
||||||
|
}
|
||||||
|
const std::vector<ParamSpec>& params() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Image animate(Image input, const ParamValues& values, const Moment& at) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -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
|
||||||
+4
-101
@@ -5,6 +5,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "core/parallel.hpp"
|
#include "core/parallel.hpp"
|
||||||
|
#include "effects/noise_field.hpp"
|
||||||
#include "effects/sampling.hpp"
|
#include "effects/sampling.hpp"
|
||||||
#include "effects/source_fill.hpp"
|
#include "effects/source_fill.hpp"
|
||||||
|
|
||||||
@@ -26,88 +27,6 @@ const std::vector<ParamSpec>& WarpEffect::params() const {
|
|||||||
return specs;
|
return specs;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// One layer of value noise: random heights on a lattice `spacing` pixels
|
|
||||||
// apart, blended smoothly between the lattice points.
|
|
||||||
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_;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sample(double x, double y, double& value, double& slopeX, double& slopeY) 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;
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Image WarpEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
Image WarpEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||||
const ParamValues& values) const {
|
const ParamValues& values) const {
|
||||||
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
|
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
|
||||||
@@ -119,11 +38,8 @@ Image WarpEffect::render(const std::vector<Image>& sources, int width, int heigh
|
|||||||
|
|
||||||
const Image input = canvasFromSources(sources, width, height, rng);
|
const Image input = canvasFromSources(sources, width, height, rng);
|
||||||
|
|
||||||
// Lattices are drawn in full before any pixel moves: one field for the
|
// The lattices are drawn in full before any pixel moves.
|
||||||
// swirl, which flows along its contours, two for a push along x and y.
|
const WarpField field(rng, width, height, scale, octaves, swirl);
|
||||||
const NoiseField first(rng, width, height, scale, octaves);
|
|
||||||
std::vector<NoiseField> extra;
|
|
||||||
if (!swirl) extra.emplace_back(rng, width, height, scale, octaves);
|
|
||||||
|
|
||||||
Image canvas(width, height);
|
Image canvas(width, height);
|
||||||
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
||||||
@@ -132,22 +48,9 @@ Image WarpEffect::render(const std::vector<Image>& sources, int width, int heigh
|
|||||||
const double y = static_cast<double>(row);
|
const double y = static_cast<double>(row);
|
||||||
for (int col = 0; col < width; ++col) {
|
for (int col = 0; col < width; ++col) {
|
||||||
const double x = col;
|
const double x = col;
|
||||||
double value = 0.0, slopeX = 0.0, slopeY = 0.0;
|
|
||||||
first.sample(x, y, value, slopeX, slopeY);
|
|
||||||
double dx = 0.0;
|
double dx = 0.0;
|
||||||
double dy = 0.0;
|
double dy = 0.0;
|
||||||
if (swirl) {
|
field.push(x, y, strength, dx, dy);
|
||||||
// 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;
|
|
||||||
extra.front().sample(x, y, other, slopeX, slopeY);
|
|
||||||
dx = value * strength;
|
|
||||||
dy = other * strength;
|
|
||||||
}
|
|
||||||
sampleBilinear(input, x + dx, y + dy, out + static_cast<std::size_t>(col) * 3);
|
sampleBilinear(input, x + dx, y + dy, out + static_cast<std::size_t>(col) * 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2107,6 +2107,7 @@ int main() {
|
|||||||
};
|
};
|
||||||
const std::vector<Moving> moving = {
|
const std::vector<Moving> moving = {
|
||||||
{"tunnel", {{"tunnel_drift", 6L}, {"tunnel_fade", 1.0}}, {}, {{"tunnel_fade", 0.0}}},
|
{"tunnel", {{"tunnel_drift", 6L}, {"tunnel_fade", 1.0}}, {}, {{"tunnel_fade", 0.0}}},
|
||||||
|
{"flow", {{"flow_mode", std::string("swirl")}}, {}, {{"flow_strength", 0L}}},
|
||||||
};
|
};
|
||||||
for (const auto& [name, lively, smooth, off] : moving) {
|
for (const auto& [name, lively, smooth, off] : moving) {
|
||||||
const auto effect = effectByName(name);
|
const auto effect = effectByName(name);
|
||||||
|
|||||||
Reference in New Issue
Block a user