Geometry Nodes: add Skip input to Simulation Output node
This adds a new Skip input to the Simulation Output node (design task: #112082). It is a convenience feature that makes it easy to conditionally forward the output of the Simulation Input node to the Simulation Output node, without the need for potentially multiple Switch nodes. When Skip is enabled, the other inputs of the Simulation Output node are not evaluated, i.e. the nodes in the simulation zone are ignored. The implementation adds this new functionality directly to the `LazyFunction` of the Simulation Output node. It has new inputs that are linked directly to the Simulation Input node, so that the simulation state can be forwarded. Pull Request: https://projects.blender.org/blender/blender/pulls/112140
This commit is contained in:
@@ -892,36 +892,32 @@ class NodesModifierSimulationParams : public nodes::GeoNodesSimulationParams {
|
||||
void output_store_frame_cache(bake::NodeCache &node_cache,
|
||||
nodes::SimulationZoneBehavior &zone_behavior) const
|
||||
{
|
||||
auto &store_and_pass_through_info =
|
||||
zone_behavior.output.emplace<sim_output::StoreAndPassThrough>();
|
||||
store_and_pass_through_info.store_fn =
|
||||
[simulation_cache = modifier_cache_,
|
||||
node_cache = &node_cache,
|
||||
current_frame = current_frame_](bke::bake::BakeState state) {
|
||||
std::lock_guard lock{simulation_cache->mutex};
|
||||
auto frame_cache = std::make_unique<bake::FrameCache>();
|
||||
frame_cache->frame = current_frame;
|
||||
frame_cache->state = std::move(state);
|
||||
node_cache->frame_caches.append(std::move(frame_cache));
|
||||
};
|
||||
auto &store_new_state_info = zone_behavior.output.emplace<sim_output::StoreNewState>();
|
||||
store_new_state_info.store_fn = [simulation_cache = modifier_cache_,
|
||||
node_cache = &node_cache,
|
||||
current_frame = current_frame_](bke::bake::BakeState state) {
|
||||
std::lock_guard lock{simulation_cache->mutex};
|
||||
auto frame_cache = std::make_unique<bake::FrameCache>();
|
||||
frame_cache->frame = current_frame;
|
||||
frame_cache->state = std::move(state);
|
||||
node_cache->frame_caches.append(std::move(frame_cache));
|
||||
};
|
||||
}
|
||||
|
||||
void store_as_prev_items(bake::NodeCache &node_cache,
|
||||
nodes::SimulationZoneBehavior &zone_behavior) const
|
||||
{
|
||||
auto &store_and_pass_through_info =
|
||||
zone_behavior.output.emplace<sim_output::StoreAndPassThrough>();
|
||||
store_and_pass_through_info.store_fn =
|
||||
[simulation_cache = modifier_cache_,
|
||||
node_cache = &node_cache,
|
||||
current_frame = current_frame_](bke::bake::BakeState state) {
|
||||
std::lock_guard lock{simulation_cache->mutex};
|
||||
if (!node_cache->prev_cache) {
|
||||
node_cache->prev_cache.emplace();
|
||||
}
|
||||
node_cache->prev_cache->state = std::move(state);
|
||||
node_cache->prev_cache->frame = current_frame;
|
||||
};
|
||||
auto &store_new_state_info = zone_behavior.output.emplace<sim_output::StoreNewState>();
|
||||
store_new_state_info.store_fn = [simulation_cache = modifier_cache_,
|
||||
node_cache = &node_cache,
|
||||
current_frame = current_frame_](bke::bake::BakeState state) {
|
||||
std::lock_guard lock{simulation_cache->mutex};
|
||||
if (!node_cache->prev_cache) {
|
||||
node_cache->prev_cache.emplace();
|
||||
}
|
||||
node_cache->prev_cache->state = std::move(state);
|
||||
node_cache->prev_cache->frame = current_frame;
|
||||
};
|
||||
}
|
||||
|
||||
void read_from_cache(const FrameIndices &frame_indices,
|
||||
|
||||
@@ -77,17 +77,17 @@ using Behavior = std::variant<PassThrough, OutputCopy, OutputMove>;
|
||||
namespace sim_output {
|
||||
|
||||
/**
|
||||
* The data is just passed through the node. Data that is incompatible with simulations (like
|
||||
* anonymous attributes), is removed though.
|
||||
* Output the data that comes from the corresponding simulation input node, ignoring the nodes in
|
||||
* the zone.
|
||||
*/
|
||||
struct PassThrough {
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as above, but also calls the given function with the data that is passed through the node.
|
||||
* This allows the caller of geometry nodes (e.g. the modifier), to cache the new simulation state.
|
||||
* Computes the simulation step and calls the given function to cache the new simulation state.
|
||||
* The new simulation state is the output of the node.
|
||||
*/
|
||||
struct StoreAndPassThrough {
|
||||
struct StoreNewState {
|
||||
std::function<void(bke::bake::BakeState state)> store_fn;
|
||||
};
|
||||
|
||||
@@ -108,7 +108,7 @@ struct ReadInterpolated {
|
||||
bke::bake::BakeStateRef next_state;
|
||||
};
|
||||
|
||||
using Behavior = std::variant<PassThrough, StoreAndPassThrough, ReadSingle, ReadInterpolated>;
|
||||
using Behavior = std::variant<PassThrough, StoreNewState, ReadSingle, ReadInterpolated>;
|
||||
|
||||
} // namespace sim_output
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
#include "NOD_add_node_search.hh"
|
||||
|
||||
#include "BLT_translation.h"
|
||||
|
||||
#include "node_geometry_util.hh"
|
||||
|
||||
namespace blender::nodes {
|
||||
@@ -40,7 +42,9 @@ std::string socket_identifier_for_simulation_item(const NodeSimulationItem &item
|
||||
}
|
||||
|
||||
static std::unique_ptr<SocketDeclaration> socket_declaration_for_simulation_item(
|
||||
const NodeSimulationItem &item, const eNodeSocketInOut in_out, const int index)
|
||||
const NodeSimulationItem &item,
|
||||
const eNodeSocketInOut in_out,
|
||||
const int corresponding_input = -1)
|
||||
{
|
||||
const eNodeSocketDatatype socket_type = eNodeSocketDatatype(item.socket_type);
|
||||
BLI_assert(NOD_geometry_simulation_output_item_socket_type_supported(socket_type));
|
||||
@@ -50,32 +54,38 @@ static std::unique_ptr<SocketDeclaration> socket_declaration_for_simulation_item
|
||||
case SOCK_FLOAT:
|
||||
decl = std::make_unique<decl::Float>();
|
||||
decl->input_field_type = InputSocketFieldType::IsSupported;
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField({index});
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField(
|
||||
{corresponding_input});
|
||||
break;
|
||||
case SOCK_VECTOR:
|
||||
decl = std::make_unique<decl::Vector>();
|
||||
decl->input_field_type = InputSocketFieldType::IsSupported;
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField({index});
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField(
|
||||
{corresponding_input});
|
||||
break;
|
||||
case SOCK_RGBA:
|
||||
decl = std::make_unique<decl::Color>();
|
||||
decl->input_field_type = InputSocketFieldType::IsSupported;
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField({index});
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField(
|
||||
{corresponding_input});
|
||||
break;
|
||||
case SOCK_BOOLEAN:
|
||||
decl = std::make_unique<decl::Bool>();
|
||||
decl->input_field_type = InputSocketFieldType::IsSupported;
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField({index});
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField(
|
||||
{corresponding_input});
|
||||
break;
|
||||
case SOCK_ROTATION:
|
||||
decl = std::make_unique<decl::Rotation>();
|
||||
decl->input_field_type = InputSocketFieldType::IsSupported;
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField({index});
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField(
|
||||
{corresponding_input});
|
||||
break;
|
||||
case SOCK_INT:
|
||||
decl = std::make_unique<decl::Int>();
|
||||
decl->input_field_type = InputSocketFieldType::IsSupported;
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField({index});
|
||||
decl->output_field_dependency = OutputFieldDependency::ForPartiallyDependentField(
|
||||
{corresponding_input});
|
||||
break;
|
||||
case SOCK_STRING:
|
||||
decl = std::make_unique<decl::String>();
|
||||
@@ -96,10 +106,12 @@ static std::unique_ptr<SocketDeclaration> socket_declaration_for_simulation_item
|
||||
void socket_declarations_for_simulation_items(const Span<NodeSimulationItem> items,
|
||||
NodeDeclaration &r_declaration)
|
||||
{
|
||||
const int inputs_offset = r_declaration.inputs.size();
|
||||
for (const int i : items.index_range()) {
|
||||
const NodeSimulationItem &item = items[i];
|
||||
SocketDeclarationPtr input_decl = socket_declaration_for_simulation_item(item, SOCK_IN, i);
|
||||
SocketDeclarationPtr output_decl = socket_declaration_for_simulation_item(item, SOCK_OUT, i);
|
||||
SocketDeclarationPtr input_decl = socket_declaration_for_simulation_item(item, SOCK_IN);
|
||||
SocketDeclarationPtr output_decl = socket_declaration_for_simulation_item(
|
||||
item, SOCK_OUT, inputs_offset + i);
|
||||
r_declaration.inputs.append(input_decl.get());
|
||||
r_declaration.items.append(std::move(input_decl));
|
||||
r_declaration.outputs.append(output_decl.get());
|
||||
@@ -516,6 +528,18 @@ static void mix_simulation_state(const NodeSimulationItem &item,
|
||||
class LazyFunctionForSimulationOutputNode final : public LazyFunction {
|
||||
const bNode &node_;
|
||||
Span<NodeSimulationItem> simulation_items_;
|
||||
int skip_input_index_;
|
||||
/**
|
||||
* Start index of the simulation state inputs that are used when the simulation is skipped. Those
|
||||
* inputs are linked directly to the simulation input node. Those inputs only exist internally,
|
||||
* but not in the UI.
|
||||
*/
|
||||
int skip_inputs_offset_;
|
||||
/**
|
||||
* Start index of the simulation state inputs that are used when the simulation is actually
|
||||
* computed. Those correspond to the sockets that are visible in the UI.
|
||||
*/
|
||||
int solve_inputs_offset_;
|
||||
|
||||
public:
|
||||
LazyFunctionForSimulationOutputNode(const bNode &node,
|
||||
@@ -528,9 +552,26 @@ class LazyFunctionForSimulationOutputNode final : public LazyFunction {
|
||||
|
||||
MutableSpan<int> lf_index_by_bsocket = own_lf_graph_info.mapping.lf_index_by_bsocket;
|
||||
|
||||
const bNodeSocket &skip_bsocket = node.input_socket(0);
|
||||
skip_input_index_ = inputs_.append_and_get_index_as(
|
||||
"Skip", *skip_bsocket.typeinfo->geometry_nodes_cpp_type, lf::ValueUsage::Maybe);
|
||||
lf_index_by_bsocket[skip_bsocket.index_in_tree()] = skip_input_index_;
|
||||
|
||||
skip_inputs_offset_ = inputs_.size();
|
||||
|
||||
/* Add the skip inputs that are linked to the simulation input node. */
|
||||
for (const int i : simulation_items_.index_range()) {
|
||||
const NodeSimulationItem &item = simulation_items_[i];
|
||||
const bNodeSocket &input_bsocket = node.input_socket(i);
|
||||
const CPPType &type = get_simulation_item_cpp_type(item);
|
||||
inputs_.append_as(item.name, type, lf::ValueUsage::Maybe);
|
||||
}
|
||||
|
||||
solve_inputs_offset_ = inputs_.size();
|
||||
|
||||
/* Add the solve inputs that correspond to the simulation state inputs in the UI. */
|
||||
for (const int i : simulation_items_.index_range()) {
|
||||
const NodeSimulationItem &item = simulation_items_[i];
|
||||
const bNodeSocket &input_bsocket = node.input_socket(i + 1);
|
||||
const bNodeSocket &output_bsocket = node.output_socket(i);
|
||||
|
||||
const CPPType &type = get_simulation_item_cpp_type(item);
|
||||
@@ -583,8 +624,8 @@ class LazyFunctionForSimulationOutputNode final : public LazyFunction {
|
||||
else if (std::get_if<sim_output::PassThrough>(&output_behavior)) {
|
||||
this->pass_through(params, user_data);
|
||||
}
|
||||
else if (auto *info = std::get_if<sim_output::StoreAndPassThrough>(&output_behavior)) {
|
||||
this->store_and_pass_through(params, user_data, *info);
|
||||
else if (auto *info = std::get_if<sim_output::StoreNewState>(&output_behavior)) {
|
||||
this->store_new_state(params, user_data, *info);
|
||||
}
|
||||
else {
|
||||
BLI_assert_unreachable();
|
||||
@@ -649,10 +690,8 @@ class LazyFunctionForSimulationOutputNode final : public LazyFunction {
|
||||
|
||||
void pass_through(lf::Params ¶ms, GeoNodesLFUserData &user_data) const
|
||||
{
|
||||
/* Instead of outputting the initial values directly, convert them to a simulation state and
|
||||
* then back. This ensures that some geometry processing happens on the data consistently (e.g.
|
||||
* removing anonymous attributes). */
|
||||
std::optional<bke::bake::BakeState> bake_state = this->get_bake_state_from_inputs(params);
|
||||
std::optional<bke::bake::BakeState> bake_state = this->get_bake_state_from_inputs(params,
|
||||
true);
|
||||
if (!bake_state) {
|
||||
/* Wait for inputs to be computed. */
|
||||
return;
|
||||
@@ -673,11 +712,21 @@ class LazyFunctionForSimulationOutputNode final : public LazyFunction {
|
||||
}
|
||||
}
|
||||
|
||||
void store_and_pass_through(lf::Params ¶ms,
|
||||
GeoNodesLFUserData &user_data,
|
||||
const sim_output::StoreAndPassThrough &info) const
|
||||
void store_new_state(lf::Params ¶ms,
|
||||
GeoNodesLFUserData &user_data,
|
||||
const sim_output::StoreNewState &info) const
|
||||
{
|
||||
std::optional<bke::bake::BakeState> bake_state = this->get_bake_state_from_inputs(params);
|
||||
const bool *skip = params.try_get_input_data_ptr_or_request<bool>(skip_input_index_);
|
||||
if (skip == nullptr) {
|
||||
/* Wait for skip input to be computed. */
|
||||
return;
|
||||
}
|
||||
|
||||
/* Instead of outputting the values directly, convert them to a bake state and then back. This
|
||||
* ensures that some geometry processing happens on the data consistently (e.g. removing
|
||||
* anonymous attributes). */
|
||||
std::optional<bke::bake::BakeState> bake_state = this->get_bake_state_from_inputs(params,
|
||||
*skip);
|
||||
if (!bake_state) {
|
||||
/* Wait for inputs to be computed. */
|
||||
return;
|
||||
@@ -686,11 +735,14 @@ class LazyFunctionForSimulationOutputNode final : public LazyFunction {
|
||||
info.store_fn(std::move(*bake_state));
|
||||
}
|
||||
|
||||
std::optional<bke::bake::BakeState> get_bake_state_from_inputs(lf::Params ¶ms) const
|
||||
std::optional<bke::bake::BakeState> get_bake_state_from_inputs(lf::Params ¶ms,
|
||||
const bool skip) const
|
||||
{
|
||||
Array<void *> input_values(inputs_.size());
|
||||
for (const int i : inputs_.index_range()) {
|
||||
input_values[i] = params.try_get_input_data_ptr_or_request(i);
|
||||
/* Choose which set of input parameters to use. The others are ignored. */
|
||||
const int params_offset = skip ? skip_inputs_offset_ : solve_inputs_offset_;
|
||||
Array<void *> input_values(simulation_items_.size());
|
||||
for (const int i : simulation_items_.index_range()) {
|
||||
input_values[i] = params.try_get_input_data_ptr_or_request(i + params_offset);
|
||||
}
|
||||
if (input_values.as_span().contains(nullptr)) {
|
||||
/* Wait for inputs to be computed. */
|
||||
@@ -722,6 +774,17 @@ static void node_declare_dynamic(const bNodeTree & /*node_tree*/,
|
||||
NodeDeclaration &r_declaration)
|
||||
{
|
||||
const NodeGeometrySimulationOutput &storage = node_storage(node);
|
||||
{
|
||||
SocketDeclarationPtr skip_decl = std::make_unique<decl::Bool>();
|
||||
skip_decl->name = "Skip";
|
||||
skip_decl->identifier = skip_decl->name;
|
||||
skip_decl->description = N_(
|
||||
"Forward the output of the simulation input node directly to the output node and ignore "
|
||||
"the nodes in the simulation zone");
|
||||
skip_decl->in_out = SOCK_IN;
|
||||
r_declaration.inputs.append(skip_decl.get());
|
||||
r_declaration.items.append(std::move(skip_decl));
|
||||
}
|
||||
socket_declarations_for_simulation_items({storage.items, storage.items_num}, r_declaration);
|
||||
}
|
||||
|
||||
|
||||
@@ -916,8 +916,7 @@ class LazyFunctionForSimulationInputsUsage : public LazyFunction {
|
||||
params.set_output(
|
||||
1,
|
||||
solve_contains_side_effect ||
|
||||
std::holds_alternative<sim_output::PassThrough>(zone_behavior->output) ||
|
||||
std::holds_alternative<sim_output::StoreAndPassThrough>(zone_behavior->output));
|
||||
std::holds_alternative<sim_output::StoreNewState>(zone_behavior->output));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1882,6 +1881,8 @@ struct GeometryNodesLazyFunctionGraphBuilder {
|
||||
const int zone_i = zone.index;
|
||||
ZoneBuildInfo &zone_info = zone_build_infos_[zone_i];
|
||||
lf::Graph &lf_graph = scope_.construct<lf::Graph>();
|
||||
const auto &sim_output_storage = *static_cast<const NodeGeometrySimulationOutput *>(
|
||||
zone.output_node->storage);
|
||||
|
||||
lf::Node *lf_zone_input_node = nullptr;
|
||||
lf::Node *lf_main_input_usage_node = nullptr;
|
||||
@@ -1925,6 +1926,18 @@ struct GeometryNodesLazyFunctionGraphBuilder {
|
||||
graph_params.usage_by_bsocket.add(bsocket, &lf_simulation_usage_node.output(1));
|
||||
}
|
||||
|
||||
/* Link simulation input node directly to simulation output node for skip behavior. */
|
||||
for (const int i : IndexRange(sim_output_storage.items_num)) {
|
||||
lf::InputSocket &lf_to = lf_simulation_output.input(i + 1);
|
||||
if (lf_simulation_input) {
|
||||
lf::OutputSocket &lf_from = lf_simulation_input->output(i + 1);
|
||||
lf_graph.add_link(lf_from, lf_to);
|
||||
}
|
||||
else {
|
||||
lf_to.set_default_value(lf_to.type().default_value());
|
||||
}
|
||||
}
|
||||
|
||||
this->insert_nodes_and_zones(zone.child_nodes, zone.child_zones, graph_params);
|
||||
|
||||
if (zone.input_node) {
|
||||
|
||||
Reference in New Issue
Block a user