Skip to content

Commit

Permalink
Add new NoiseNode brush behavior node
Browse files Browse the repository at this point in the history
This new node type is like `SourceNode` or `ConstantNode`, but instead emits a seed-stable random noise function.  It can be used for generating random-looking brush dynamics (such as randomized particle position offsets).

Right now, the noise function is seeded only from the node specification, so it will generate identical noise functions across strokes.  A future CL will change this so that different seeds are used for different strokes.

PiperOrigin-RevId: 715481744
  • Loading branch information
Ink Open Source authored and copybara-github committed Jan 14, 2025
1 parent 7f0fa32 commit ccdbcc0
Show file tree
Hide file tree
Showing 13 changed files with 350 additions and 9 deletions.
1 change: 1 addition & 0 deletions ink/brush/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ cc_test(
":fuzz_domains",
"//ink/types:duration",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:status_matchers",
"@com_google_absl//absl/strings",
"@com_google_fuzztest//fuzztest",
"@com_google_googletest//:gtest_main",
Expand Down
32 changes: 32 additions & 0 deletions ink/brush/brush_behavior.cc
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ bool operator!=(const BrushBehavior::ConstantNode& lhs,
return !(lhs == rhs);
}

bool operator==(const BrushBehavior::NoiseNode& lhs,
const BrushBehavior::NoiseNode& rhs) {
return lhs.seed == rhs.seed && lhs.vary_over == rhs.vary_over &&
lhs.base_period == rhs.base_period;
}

bool operator!=(const BrushBehavior::NoiseNode& lhs,
const BrushBehavior::NoiseNode& rhs) {
return !(lhs == rhs);
}

bool operator==(const BrushBehavior::FallbackFilterNode& lhs,
const BrushBehavior::FallbackFilterNode& rhs) {
return lhs.is_fallback_for == rhs.is_fallback_for;
Expand Down Expand Up @@ -350,6 +361,7 @@ bool IsValidBehaviorInterpolation(BrushBehavior::Interpolation interpolation) {
// Returns the number of input values that a given `Node` consumes.
int NodeInputCount(const BrushBehavior::SourceNode& node) { return 0; }
int NodeInputCount(const BrushBehavior::ConstantNode& node) { return 0; }
int NodeInputCount(const BrushBehavior::NoiseNode& node) { return 0; }
int NodeInputCount(const BrushBehavior::FallbackFilterNode& node) { return 1; }
int NodeInputCount(const BrushBehavior::ToolTypeFilterNode& node) { return 1; }
int NodeInputCount(const BrushBehavior::DampingNode& node) { return 1; }
Expand Down Expand Up @@ -405,6 +417,20 @@ absl::Status ValidateNode(const BrushBehavior::ConstantNode& node) {
return absl::OkStatus();
}

absl::Status ValidateNode(const BrushBehavior::NoiseNode& node) {
if (!IsValidBehaviorDampingSource(node.vary_over)) {
return absl::InvalidArgumentError(
absl::StrFormat("`NoiseNode::vary_over` holds non-enumerator value %d",
static_cast<int>(node.vary_over)));
}
if (!std::isfinite(node.base_period) || node.base_period <= 0.f) {
return absl::InvalidArgumentError(absl::StrCat(
"`NoiseNode::base_period` must be finite and positive. Got ",
node.base_period));
}
return absl::OkStatus();
}

absl::Status ValidateNode(const BrushBehavior::FallbackFilterNode& node) {
if (!IsValidOptionalInputProperty(node.is_fallback_for)) {
return absl::InvalidArgumentError(absl::StrFormat(
Expand Down Expand Up @@ -734,6 +760,12 @@ std::string ToFormattedString(const BrushBehavior::ConstantNode& node) {
return absl::StrCat("ConstantNode{", node.value, "}");
}

std::string ToFormattedString(const BrushBehavior::NoiseNode& node) {
return absl::StrCat(
"NoiseNode{seed=0x", absl::Hex(node.seed, absl::kZeroPad8),
", vary_over=", node.vary_over, ", base_period=", node.base_period, "}");
}

std::string ToFormattedString(const BrushBehavior::FallbackFilterNode& node) {
return absl::StrCat("FallbackFilterNode{", node.is_fallback_for, "}");
}
Expand Down
25 changes: 22 additions & 3 deletions ink/brush/brush_behavior.h
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,19 @@ struct BrushBehavior {
float value;
};

// Value node for producing a continuous random noise function with values
// between 0 to 1.
// Inputs: 0
// Output: The current random value.
// To be valid:
// - `vary_over` must be a valid `DampingSource` enumerator.
// - `base_period` must be finite and strictly positive.
struct NoiseNode {
uint32_t seed;
DampingSource vary_over;
float base_period;
};

//////////////////////////
/// FILTER VALUE NODES ///
//////////////////////////
Expand Down Expand Up @@ -530,9 +543,10 @@ struct BrushBehavior {
// value, or a "target node" which consumes one or more input values and
// applies some effect to the brush tip (but does not produce any output
// value).
using Node = std::variant<SourceNode, ConstantNode, FallbackFilterNode,
ToolTypeFilterNode, DampingNode, ResponseNode,
BinaryOpNode, InterpolationNode, TargetNode>;
using Node =
std::variant<SourceNode, ConstantNode, NoiseNode, FallbackFilterNode,
ToolTypeFilterNode, DampingNode, ResponseNode, BinaryOpNode,
InterpolationNode, TargetNode>;

std::vector<Node> nodes;
};
Expand All @@ -552,6 +566,11 @@ bool operator==(const BrushBehavior::ConstantNode& lhs,
bool operator!=(const BrushBehavior::ConstantNode& lhs,
const BrushBehavior::ConstantNode& rhs);

bool operator==(const BrushBehavior::NoiseNode& lhs,
const BrushBehavior::NoiseNode& rhs);
bool operator!=(const BrushBehavior::NoiseNode& lhs,
const BrushBehavior::NoiseNode& rhs);

bool operator==(const BrushBehavior::FallbackFilterNode& lhs,
const BrushBehavior::FallbackFilterNode& rhs);
bool operator!=(const BrushBehavior::FallbackFilterNode& lhs,
Expand Down
74 changes: 74 additions & 0 deletions ink/brush/brush_behavior_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "gtest/gtest.h"
#include "fuzztest/fuzztest.h"
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/strings/str_cat.h"
#include "ink/brush/easing_function.h"
#include "ink/brush/fuzz_domains.h"
Expand All @@ -28,6 +29,8 @@
namespace ink {
namespace {

using ::absl_testing::IsOk;
using ::absl_testing::StatusIs;
using ::testing::HasSubstr;

constexpr float kInfinity = std::numeric_limits<float>::infinity();
Expand Down Expand Up @@ -240,6 +243,15 @@ TEST(BrushBehaviorTest, StringifyConstantNode) {
"ConstantNode{0.25}");
}

TEST(BrushBehaviorTest, StringifyNoiseNode) {
EXPECT_EQ(absl::StrCat(BrushBehavior::NoiseNode{
.seed = 0xEffaced,
.vary_over = BrushBehavior::DampingSource::kTimeInSeconds,
.base_period = 0.25f}),
"NoiseNode{seed=0x0effaced, vary_over=kTimeInSeconds, "
"base_period=0.25}");
}

TEST(BrushBehaviorTest, StringifyFallbackFilterNode) {
EXPECT_EQ(absl::StrCat(BrushBehavior::FallbackFilterNode{
BrushBehavior::OptionalInputProperty::kPressure}),
Expand Down Expand Up @@ -397,6 +409,34 @@ TEST(BrushBehaviorTest, ConstantNodeEqualAndNotEqual) {
EXPECT_NE((BrushBehavior::ConstantNode{.value = 37}), node);
}

TEST(BrushBehaviorTest, NoiseNodeEqualAndNotEqual) {
BrushBehavior::NoiseNode node = {
.seed = 12345,
.vary_over = BrushBehavior::DampingSource::kTimeInSeconds,
.base_period = 0.25f};
EXPECT_EQ((BrushBehavior::NoiseNode{
.seed = 12345,
.vary_over = BrushBehavior::DampingSource::kTimeInSeconds,
.base_period = 0.25f}),
node);
EXPECT_NE((BrushBehavior::NoiseNode{
.seed = 54321, // different
.vary_over = BrushBehavior::DampingSource::kTimeInSeconds,
.base_period = 0.25f}),
node);
EXPECT_NE(
(BrushBehavior::NoiseNode{.seed = 12345,
.vary_over = BrushBehavior::DampingSource::
kDistanceInCentimeters, // different
.base_period = 0.25f}),
node);
EXPECT_NE((BrushBehavior::NoiseNode{
.seed = 12345,
.vary_over = BrushBehavior::DampingSource::kTimeInSeconds,
.base_period = 0.75f}), // different
node);
}

TEST(BrushBehaviorTest, FallbackFilterNodeEqualAndNotEqual) {
BrushBehavior::FallbackFilterNode node = {
.is_fallback_for = BrushBehavior::OptionalInputProperty::kPressure};
Expand Down Expand Up @@ -616,6 +656,40 @@ TEST(BrushBehaviorTest, ValidateConstantNode) {
EXPECT_THAT(status.message(), HasSubstr("must be finite"));
}

TEST(BrushBehaviorTest, ValidateNoiseNode) {
EXPECT_THAT(
brush_internal::ValidateBrushBehaviorNode(BrushBehavior::NoiseNode{
.seed = 12345,
.vary_over = BrushBehavior::DampingSource::kTimeInSeconds,
.base_period = 0.25,
}),
IsOk());
EXPECT_THAT(
brush_internal::ValidateBrushBehaviorNode(BrushBehavior::NoiseNode{
.seed = 12345,
.vary_over = static_cast<BrushBehavior::DampingSource>(123),
.base_period = 0.25,
}),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("non-enumerator value 123")));
EXPECT_THAT(
brush_internal::ValidateBrushBehaviorNode(BrushBehavior::NoiseNode{
.seed = 12345,
.vary_over = BrushBehavior::DampingSource::kTimeInSeconds,
.base_period = 0,
}),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("base_period` must be finite and positive")));
EXPECT_THAT(
brush_internal::ValidateBrushBehaviorNode(BrushBehavior::NoiseNode{
.seed = 12345,
.vary_over = BrushBehavior::DampingSource::kTimeInSeconds,
.base_period = kInfinity,
}),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("base_period` must be finite and positive")));
}

TEST(BrushBehaviorTest, ValidateFallbackFilterNode) {
EXPECT_EQ(brush_internal::ValidateBrushBehaviorNode(
BrushBehavior::FallbackFilterNode{
Expand Down
11 changes: 9 additions & 2 deletions ink/brush/fuzz_domains.cc
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,12 @@ Domain<BrushBehavior::ConstantNode> ValidBrushBehaviorConstantNode() {
return StructOf<BrushBehavior::ConstantNode>(Finite<float>());
}

Domain<BrushBehavior::NoiseNode> ValidBrushBehaviorNoiseNode() {
return StructOf<BrushBehavior::NoiseNode>(
Arbitrary<uint32_t>(), ArbitraryBrushBehaviorDampingSource(),
FinitePositiveFloat());
}

Domain<BrushBehavior::FallbackFilterNode>
ValidBrushBehaviorFallbackFilterNode() {
return StructOf<BrushBehavior::FallbackFilterNode>(
Expand Down Expand Up @@ -353,7 +359,8 @@ Domain<std::vector<BrushBehavior::Node>> ValidBrushBehaviorNodeLeaf() {
return std::vector<BrushBehavior::Node>{node};
},
OneOf(BrushBehaviorNodeOf(ValidBrushBehaviorSourceNode()),
BrushBehaviorNodeOf(ValidBrushBehaviorConstantNode())));
BrushBehaviorNodeOf(ValidBrushBehaviorConstantNode()),
BrushBehaviorNodeOf(ValidBrushBehaviorNoiseNode())));
}

// A domain over all valid behavior node subtrees (i.e. with a value node at the
Expand Down Expand Up @@ -460,7 +467,7 @@ Domain<BrushBehavior> ValidBrushBehavior() {
Domain<BrushBehavior::Node> ValidBrushBehaviorNode() {
return VariantOf(
ValidBrushBehaviorSourceNode(), ValidBrushBehaviorConstantNode(),
ValidBrushBehaviorFallbackFilterNode(),
ValidBrushBehaviorNoiseNode(), ValidBrushBehaviorFallbackFilterNode(),
ValidBrushBehaviorToolTypeFilterNode(), ValidBrushBehaviorDampingNode(),
ValidBrushBehaviorResponseNode(), ValidBrushBehaviorBinaryOpNode(),
ValidBrushBehaviorInterpolationNode(), ValidBrushBehaviorTargetNode());
Expand Down
13 changes: 13 additions & 0 deletions ink/brush/internal/jni/brush_behavior_jni.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#include <jni.h>

#include <cstdint>
#include <optional>
#include <utility>
#include <vector>
Expand Down Expand Up @@ -77,6 +78,18 @@ JNI_METHOD_INNER(brush, BrushBehavior, ConstantNode, void,
});
}

JNI_METHOD_INNER(brush, BrushBehavior, NoiseNode, void, nativeAppendNoiseNode)
(JNIEnv* env, jobject thiz, jlong native_behavior_pointer, jint seed,
jint vary_over, jfloat base_period) {
auto* brush_behavior =
reinterpret_cast<ink::BrushBehavior*>(native_behavior_pointer);
brush_behavior->nodes.push_back(ink::BrushBehavior::NoiseNode{
.seed = static_cast<uint32_t>(seed),
.vary_over = static_cast<ink::BrushBehavior::DampingSource>(vary_over),
.base_period = base_period,
});
}

JNI_METHOD_INNER(brush, BrushBehavior, FallbackFilterNode, void,
nativeAppendFallbackFilterNode)
(JNIEnv* env, jobject thiz, jlong native_behavior_pointer,
Expand Down
10 changes: 10 additions & 0 deletions ink/brush/type_matchers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ Matcher<BrushBehavior::Node> BrushBehaviorNodeEqMatcher(
"value", &BrushBehavior::ConstantNode::value, FloatEq(expected.value)));
}

Matcher<BrushBehavior::Node> BrushBehaviorNodeEqMatcher(
const BrushBehavior::NoiseNode& expected) {
return VariantWith<BrushBehavior::NoiseNode>(
AllOf(Field("seed", &BrushBehavior::NoiseNode::seed, Eq(expected.seed)),
Field("vary_over", &BrushBehavior::NoiseNode::vary_over,
Eq(expected.vary_over)),
Field("base_period", &BrushBehavior::NoiseNode::base_period,
FloatEq(expected.base_period))));
}

Matcher<BrushBehavior::Node> BrushBehaviorNodeEqMatcher(
const BrushBehavior::FallbackFilterNode& expected) {
return VariantWith<BrushBehavior::FallbackFilterNode>(Field(
Expand Down
2 changes: 2 additions & 0 deletions ink/strokes/internal/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ cc_library(
deps = [
":brush_tip_state",
":easing_implementation",
":noise_generator",
":stroke_input_modeler",
"//ink/brush:brush_behavior",
"//ink/brush:brush_tip",
Expand All @@ -131,6 +132,7 @@ cc_test(
":brush_tip_modeler_helpers",
":brush_tip_state",
":easing_implementation",
":noise_generator",
":stroke_input_modeler",
":type_matchers",
"//ink/brush:brush_behavior",
Expand Down
23 changes: 23 additions & 0 deletions ink/strokes/internal/brush_tip_modeler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <optional>
#include <variant>
Expand Down Expand Up @@ -295,6 +296,8 @@ void BrushTipModeler::StartStroke(absl::Nonnull<const BrushTip*> brush_tip,
behaviors_depend_on_next_input_ = false;

behavior_nodes_.clear();
current_noise_generators_.clear();
fixed_noise_generators_.clear();
current_damped_values_.clear();
fixed_damped_values_.clear();
behavior_targets_.clear();
Expand Down Expand Up @@ -327,6 +330,19 @@ void BrushTipModeler::AppendBehaviorNode(
behavior_nodes_.push_back(node);
}

void BrushTipModeler::AppendBehaviorNode(const BrushBehavior::NoiseNode& node) {
behavior_nodes_.push_back(NoiseNodeImplementation{
.generator_index = current_noise_generators_.size(),
.vary_over = node.vary_over,
.base_period = node.base_period,
});
// TODO: b/373649344 - Concatenate the 32-bit per-stroke seed (once that
// exists) with the 32-bit node seed to form the 64-bit noise generator seed.
uint64_t noise_seed = node.seed;
current_noise_generators_.emplace_back(noise_seed);
fixed_noise_generators_.push_back(current_noise_generators_.back());
}

void BrushTipModeler::AppendBehaviorNode(
const BrushBehavior::FallbackFilterNode& node) {
behavior_nodes_.push_back(node);
Expand Down Expand Up @@ -398,6 +414,9 @@ void BrushTipModeler::UpdateStroke(

InputMetrics max_fixed_metrics =
CalculateMaxFixedInputMetrics(input_modeler_state, inputs);
ABSL_DCHECK_EQ(fixed_noise_generators_.size(),
current_noise_generators_.size());
absl::c_copy(fixed_noise_generators_, current_noise_generators_.begin());
ABSL_DCHECK_EQ(fixed_damped_values_.size(), current_damped_values_.size());
absl::c_copy(fixed_damped_values_, current_damped_values_.begin());
ABSL_DCHECK_EQ(fixed_target_modifiers_.size(),
Expand Down Expand Up @@ -440,6 +459,9 @@ void BrushTipModeler::UpdateStroke(
// Save the necessary fixed properties:
last_fixed_modeled_tip_state_metrics_ = last_modeled_tip_state_metrics;
new_fixed_tip_state_count_ = saved_tip_states_.size();
ABSL_DCHECK_EQ(current_noise_generators_.size(),
fixed_noise_generators_.size());
absl::c_copy(current_noise_generators_, fixed_noise_generators_.begin());
ABSL_DCHECK_EQ(current_damped_values_.size(), fixed_damped_values_.size());
absl::c_copy(current_damped_values_, fixed_damped_values_.begin());
ABSL_DCHECK_EQ(current_target_modifiers_.size(),
Expand Down Expand Up @@ -579,6 +601,7 @@ void BrushTipModeler::AddNewTipState(
.brush_size = brush_size_,
.previous_input_metrics = previous_input_metrics,
.stack = behavior_stack_,
.noise_generators = absl::MakeSpan(current_noise_generators_),
.damped_values = absl::MakeSpan(current_damped_values_),
.target_modifiers = absl::MakeSpan(current_target_modifiers_),
};
Expand Down
4 changes: 4 additions & 0 deletions ink/strokes/internal/brush_tip_modeler.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class BrushTipModeler {
// Helper methods for the `std::visit` call in `StartStroke`.
void AppendBehaviorNode(const BrushBehavior::SourceNode& node);
void AppendBehaviorNode(const BrushBehavior::ConstantNode& node);
void AppendBehaviorNode(const BrushBehavior::NoiseNode& node);
void AppendBehaviorNode(const BrushBehavior::FallbackFilterNode& node);
void AppendBehaviorNode(const BrushBehavior::ToolTypeFilterNode& node);
void AppendBehaviorNode(const BrushBehavior::DampingNode& node);
Expand Down Expand Up @@ -187,6 +188,9 @@ class BrushTipModeler {
std::vector<BehaviorNodeImplementation> behavior_nodes_;
std::vector<float> behavior_stack_;
// These next two vectors must always be the same size:
std::vector<NoiseGenerator> current_noise_generators_;
std::vector<NoiseGenerator> fixed_noise_generators_;
// These next two vectors must always be the same size:
std::vector<float> current_damped_values_;
std::vector<float> fixed_damped_values_;
// These next three vectors must always be the same size:
Expand Down
Loading

0 comments on commit ccdbcc0

Please sign in to comment.