Files
test2/source/blender/blenlib/BLI_compute_context.hh
Jacques Lucke 24dc9a21b1 Geometry Nodes: support attaching gizmos to input values
This adds support for attaching gizmos for input values. The goal is to make it
easier for users to set input values intuitively in the 3D viewport.

We went through multiple different possible designs until we settled on the one
implemented here. We picked it for it's flexibility and ease of use when using
geometry node assets. The core principle in the design is that **gizmos are
attached to existing input values instead of being the input value themselves**.
This actually fits the existing concept of gizmos in Blender well, but may be a
bit unintutitive in a node setup at first. The attachment is done using links in
the node editor.

The most basic usage of the node is to link a Value node to the new Linear Gizmo
node. This attaches the gizmo to the input value and allows you to change it
from the 3D view. The attachment is indicated by the gizmo icon in the sockets
which are controlled by a gizmo as well as the back-link (notice the double
link) when the gizmo is active.

The core principle makes it straight forward to control the same node setup from
the 3D view with gizmos, or by manually changing input values, or by driving the
input values procedurally.

If the input value is controlled indirectly by other inputs, it's often possible
to **automatically propagate** the gizmo to the actual input.

Backpropagation does not work for all nodes, although more nodes can be
supported over time.

This patch adds the first three gizmo nodes which cover common use cases:
* **Linear Gizmo**: Creates a gizmo that controls a float or integer value using
  a linear movement of e.g. an arrow in the 3D viewport.
* **Dial Gizmo**: Creates a circular gizmo in the 3D viewport that can be
  rotated to change the attached angle input.
* **Transform Gizmo**: Creates a simple gizmo for location, rotation and scale.

In the future, more built-in gizmos and potentially the ability for custom
gizmos could be added.

All gizmo nodes have a **Transform** geometry output. Using it is optional but
it is recommended when the gizmo is used to control inputs that affect a
geometry. When it is used, Blender will automatically transform the gizmos
together with the geometry that they control. To achieve this, the output should
be merged with the generated geometry using the *Join Geometry* node. The data
contained in *Transform* output is not visible geometry, but just internal
information that helps Blender to give a better user experience when using
gizmos.

The gizmo nodes have a multi-input socket. This allows **controlling multiple
values** with the same gizmo.

Only a small set of **gizmo shapes** is supported initially. It might be
extended in the future but one goal is to give the gizmos used by different node
group assets a familiar look and feel. A similar constraint exists for
**colors**. Currently, one can choose from a fixed set of colors which can be
modified in the theme settings.

The set of **visible gizmos** is determined by a multiple factors because it's
not really feasible to show all possible gizmos at all times. To see any of the
geometry nodes gizmos, the "Active Modifier" option has to be enabled in the
"Viewport Gizmos" popover. Then all gizmos are drawn for which at least one of
the following is true:
* The gizmo controls an input of the active modifier of the active object.
* The gizmo controls a value in a selected node in an open node editor.
* The gizmo controls a pinned value in an open node editor. Pinning works by
  clicking the gizmo icon next to the value.

Pull Request: https://projects.blender.org/blender/blender/pulls/112677
2024-07-10 16:18:47 +02:00

201 lines
5.9 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup bli
*
* When logging computed values, we generally want to know where the value was computed. For
* example, geometry nodes logs socket values so that they can be displayed in the ui. For that we
* can combine the logged value with a `ComputeContext`, which identifies the place where the value
* was computed.
*
* This is not a trivial problem because e.g. just storing a pointer to the socket a value
* belongs to is not enough. That's because the same socket may correspond to many different values
* when the socket is used in a node group that is used multiple times. In this case, not only does
* the socket have to be stored but also the entire nested node group path that led to the
* evaluation of the socket.
*
* Storing the entire "context path" for every logged value is not feasible, because that path can
* become quite long. So that would need much more memory, more compute overhead and makes it
* complicated to compare if two contexts are the same. If the identifier for a compute context
* would have a variable size, it would also be much harder to create a map from context to values.
*
* The solution implemented below uses the following key ideas:
* - Every compute context can be hashed to a unique fixed size value (`ComputeContextHash`). While
* technically there could be hash collisions, the hashing algorithm has to be chosen to make
* that practically impossible. This way an entire context path, possibly consisting of many
* nested contexts, is represented by a single value that can be stored easily.
* - A nested compute context is build as singly linked list, where every compute context has a
* pointer to the parent compute context. Note that a link in the other direction is not possible
* because the same parent compute context may be used by many different children which possibly
* run on different threads.
*/
#include <optional>
#include "BLI_array.hh"
#include "BLI_linear_allocator.hh"
#include "BLI_stack.hh"
#include "BLI_string_ref.hh"
#include "BLI_struct_equality_utils.hh"
namespace blender {
/**
* A hash that uniquely identifies a specific (non-fixed-size) compute context. The hash has to
* have enough bits to make collisions practically impossible.
*/
struct ComputeContextHash {
uint64_t v1 = 0;
uint64_t v2 = 0;
uint64_t hash() const
{
return v1;
}
BLI_STRUCT_EQUALITY_OPERATORS_2(ComputeContextHash, v1, v2)
void mix_in(const void *data, int64_t len);
friend std::ostream &operator<<(std::ostream &stream, const ComputeContextHash &hash);
};
/**
* Identifies the context in which a computation happens. This context can be used to identify
* values logged during the computation. For more details, see the comment at the top of the file.
*
* This class should be subclassed to implement specific contexts.
*/
class ComputeContext {
private:
/**
* Only used for debugging currently.
*/
const char *static_type_;
/**
* Pointer to the context that this context is child of. That allows nesting compute contexts.
*/
const ComputeContext *parent_ = nullptr;
protected:
/**
* The hash that uniquely identifies this context. It's a combined hash of this context as well
* as all the parent contexts.
*/
ComputeContextHash hash_;
public:
ComputeContext(const char *static_type, const ComputeContext *parent)
: static_type_(static_type), parent_(parent)
{
if (parent != nullptr) {
hash_ = parent_->hash_;
}
}
virtual ~ComputeContext() = default;
const ComputeContextHash &hash() const
{
return hash_;
}
const char *static_type() const
{
return static_type_;
}
const ComputeContext *parent() const
{
return parent_;
}
/**
* Print the entire nested context stack.
*/
void print_stack(std::ostream &stream, StringRef name) const;
/**
* Print information about this specific context. This has to be implemented by each subclass.
*/
virtual void print_current_in_line(std::ostream &stream) const = 0;
friend std::ostream &operator<<(std::ostream &stream, const ComputeContext &compute_context);
};
/**
* Utility class to build a context stack in one place. This is typically used to get the hash that
* corresponds to a specific nested compute context, in order to look up corresponding logged
* values.
*/
class ComputeContextBuilder {
private:
LinearAllocator<> allocator_;
Stack<destruct_ptr<ComputeContext>> contexts_;
std::optional<Vector<destruct_ptr<ComputeContext>>> old_contexts_;
public:
/**
* If called, compute contexts are not destructed when they are popped. Instead their lifetime
* will be the lifetime of this builder.
*/
void keep_old_contexts()
{
if (!old_contexts_.has_value()) {
old_contexts_.emplace();
}
}
bool is_empty() const
{
return contexts_.is_empty();
}
const ComputeContext *current() const
{
if (contexts_.is_empty()) {
return nullptr;
}
return contexts_.peek().get();
}
const ComputeContextHash hash() const
{
BLI_assert(!contexts_.is_empty());
return this->current()->hash();
}
template<typename T, typename... Args> void push(Args &&...args)
{
const ComputeContext *current = this->current();
destruct_ptr<T> context = allocator_.construct<T>(current, std::forward<Args>(args)...);
contexts_.push(std::move(context));
}
void pop()
{
auto context = contexts_.pop();
if (old_contexts_) {
old_contexts_->append(std::move(context));
}
}
/** Pops all compute contexts until the given one is at the top. */
void pop_until(const ComputeContext *context)
{
while (!contexts_.is_empty()) {
if (contexts_.peek().get() == context) {
return;
}
this->pop();
}
/* Should have found the context above if it's not null. */
BLI_assert(context == nullptr);
}
};
} // namespace blender