# Fields Implementation Notes _Edited transcript of a conversation between Jacques and Hans_ We want to build functions and we want to evaluate functions that we have built before. We can have various systems to build functions and various systems to evaluate functions. It now happens to be the case that sometimes one system is capable of building and evaluating functions. However, these should still be considered to be separate tasks. When evaluating a function we typically have to look at the function and the context to determine the best way to evaluate it. The system used for evaluation might be the same system that built the function, but it might be a separate system on the CPU, or it might run the function on the GPU. Theoretically, it could even make sense that an individual node, in order to evaluate a geometry field, creates a new geometry-nodes-evaluator during its execution. Then we would have nested geometry-nodes-evaluators. The geometry nodes evaluator and multi-function procedure evaluator are both essentially just moving generic data around and call functions with that data. The evaluator is responsible for the overall geometry nodes evaluation, building up the callbacks that can then be used by individual nodes. We also want to be able to evaluate fields separate from the evaluator (e.g. for the viewer maybe). When the geometry nodes evaluator executes the function nodes, it should just create a new field that also references the input fields. It does not actually evaluate the fields, that is done by the node that uses the field. In the prototype the evaluator also does not evaluate the fields (unless it is constant, which is not important right now). Basically, you build up some kind of tree data structure when the nodes are evaluated. Then, when the field is evaluated, this tree is converted into a multi-function procedure, and then you evaluate this procedure. The multi-function field just references a bunch of other fields. One benefit of building up the separate field tree structure and evaluating it as a procedure is that the procedure system can look at all of the nodes at once and make optimizations that the evaluator, which works at the node level, wouldn't be able to do. Also the evaluator can't really evaluate the fields because it has no idea about the context the fields are evaluated in. We could keep track of that context, but it would add a lot of complexity and it wouldn't really fit. # Mockup of a Node Implementation ```lanc=c++ static void point_translate_node_execute(...) { /* First evaluate the selection separately, since maybe it allows us to compute fewer values for the translation. */ Field<bool> selection_field = params.get_input_field<bool>("Selection"); IndexMask mask = evaluate_geometry_selection_field(params, component, ATTR_DOMAIN_POINT, selection_field, scope); /* evaluate_geometry_selection_field could also take a mask input, but it would be optional. */ Field<float3> translation = params.get_input_field<float3>("Translation"); OutputAttribute_Typed<float3> position_attribute = component.attribute_get_for_output<float3>("position", ATTR_DOMAIN_POINT); MutableSpan<float3> positions = position_attribute.as_span(); /* We could also retrieve the position field input with a get_position_field_input(); function, but since we're calling "as_span" * here anyway we can use the position span here as an optimization, but "hard-coding" the field input context. */ std::shared_ptr<FieldInputSpan<float3>> position_input = std::make_shared<FieldInputSpan<float3>>(positions); std::unique_ptr<MultiFunction> add_fn = std::make_unique<CustomMF_SI_SI_SO<flaot3, flaot3, flaot3>>( "Add", [](flaot3 a, flaot3 b) { return a + b; }); FieldFunctionPtr add_field = std::make_shared<FieldFunction>(std::move(add_fn), {translation, Field<float3>(position_input)}); /* The point translate node (if it ends up existing) just adds another field on top of its input field network * and evaluates directly into the position span. That is so cool! */ evaluate_geometry_field(params, component, ATTR_DOMAIN_POINT, mask, {add_field}, {positions}); /* But now it becomes very tempting to pack the extra parameters into a "FieldContext" * FieldContextGeometry only uses an "ErrorReporter" interface from params. * The mask could also be part of the context, but maybe if it's more visible then developers would use it more, which would be good. */ FieldContextGeometry context{params, component, ATTR_DOMAIN_POINT}; /* Then we just have a generic "evaluate_field" function, which changes the behavior of input nodes based on the context. */ evaluate_field(context, mask, {add_field, another_field, etc}, {positions, ...}) } /* There could also be a separate context for curve splines! Maybe the idea is over the top, but it's really cool! * And it's a very generic solution to the idea of doing computations in contiguous data chunks of splines, so it might work. */ Array<int> offsets = curve.control_point_offsets(); for (const int i : curve.splines().index_range()) { FieldContextCurveSpline context{params, curve, *curve.splines()[i], offsets[i]}; /* I guess we would have to make sure *all* supported input nodes supported the spline context, or the evaluation wouldn't work. */ evaluate_field(context, mask, {spline_positions_add_field, another_field}, {positions}); } ```