Compositor: Refactor File Output node

This patches refactors the compositor File Output mechanism and
implements the file output node for the Realtime Compositor. The
refactor was done for the following reasons:

1. The existing file output mechanism relied on a global EXR image
   resource where the result of each compositor execution for each
   view was accumulated and stored in the global resource, until the
   last view is executed, when the EXR is finally saved. Aside from
   relying on global resources, this can cause effective memory leaks
   since the compositor can be interrupted before the EXR is written and
   closed.
2. We need common code to share between all compositors since we now
   have multiple compositor implementations.
3. We needed to take the opportunity to fix some of the issues with the
   existing implementation, like lossy compression of data passes,
   and inability to save single values passes.

The refactor first introduced a new structure called the Compositor
Render Context. This context stores compositor information related to
the render pipeline and is persistent across all compositor executions
of all views. Its extended lifetime relative to a single compositor
execution lends itself well to store data that is accumulated across
views. The context currently has a map of File Output objects. Those
objects wrap a Render Result structure and can be used to construct
multi-view images which can then be saved after all views are executed
using the existing BKE_image_render_write function.

Minor adjustments were made to the BKE and RE modules to allow saving
using the BKE_image_render_write function. Namely, the function now
allows the use of a source image format for saving as well as the
ability to not save the render result as a render by introducing two new
default arguments. Further, for multi-layer EXR saving, the existent of
a single unnamed render layer will omit the layer name from the EXR
channel full name, and only the pass, view, and channel ID will remain.
Finally, the Render Result to Image Buffer conversion now take he number
of channels into account, instead of always assuming color channels.

The patch implements the File Output node in the Realtime Compositor
using the aforementioned mechanisms, replaces the implementation of the
CPU compositor using the same Realtime Compositor implementation, and
setup the necessary logic in the render pipeline code.

Pull Request: https://projects.blender.org/blender/blender/pulls/113982
This commit is contained in:
Omar Emara 2023-12-13 11:08:03 +01:00 committed by Omar Emara
parent 105db1a06e
commit 931c188ce5
36 changed files with 1294 additions and 1271 deletions

View File

@ -74,12 +74,15 @@ bool BKE_image_render_write_exr(struct ReportList *reports,
/**
* \param filepath_basis: May be used as-is, or used as a basis for multi-view images.
* \param format: The image format to use for saving, if null, the scene format will be used.
*/
bool BKE_image_render_write(struct ReportList *reports,
struct RenderResult *rr,
const struct Scene *scene,
const bool stamp,
const char *filepath_basis);
const char *filepath_basis,
const struct ImageFormatData *format = nullptr,
bool save_as_render = true);
#ifdef __cplusplus
}

View File

@ -785,6 +785,7 @@ bool BKE_image_render_write_exr(ReportList *reports,
/* Other render layers. */
int nr = (rr->have_combined) ? 1 : 0;
const bool has_multiple_layers = BLI_listbase_count_at_most(&rr->layers, 2) > 1;
LISTBASE_FOREACH (RenderLayer *, rl, &rr->layers) {
/* Skip other render layers if requested. */
if (!multi_layer && nr != layer) {
@ -830,8 +831,18 @@ bool BKE_image_render_write_exr(ReportList *reports,
char layname[EXR_PASS_MAXNAME];
if (multi_layer) {
RE_render_result_full_channel_name(passname, nullptr, rp->name, nullptr, rp->chan_id, a);
STRNCPY(layname, rl->name);
/* A single unnamed layer indicates that the pass name should be used as the layer name,
* while the pass name should be the channel ID. */
if (!has_multiple_layers && rl->name[0] == '\0') {
passname[0] = rp->chan_id[a];
passname[1] = '\0';
STRNCPY(layname, rp->name);
}
else {
RE_render_result_full_channel_name(
passname, nullptr, rp->name, nullptr, rp->chan_id, a);
STRNCPY(layname, rl->name);
}
}
else {
passname[0] = rp->chan_id[a];
@ -920,7 +931,9 @@ bool BKE_image_render_write(ReportList *reports,
RenderResult *rr,
const Scene *scene,
const bool stamp,
const char *filepath_basis)
const char *filepath_basis,
const ImageFormatData *format,
bool save_as_render)
{
bool ok = true;
@ -929,7 +942,7 @@ bool BKE_image_render_write(ReportList *reports,
}
ImageFormatData image_format;
BKE_image_format_init_for_write(&image_format, scene, nullptr);
BKE_image_format_init_for_write(&image_format, scene, format);
const bool is_mono = BLI_listbase_count_at_most(&rr->views, 2) < 2;
const bool is_exr_rr = ELEM(
@ -938,7 +951,8 @@ bool BKE_image_render_write(ReportList *reports,
const float dither = scene->r.dither_intensity;
if (image_format.views_format == R_IMF_VIEWS_MULTIVIEW && is_exr_rr) {
ok = BKE_image_render_write_exr(reports, rr, filepath_basis, &image_format, true, nullptr, -1);
ok = BKE_image_render_write_exr(
reports, rr, filepath_basis, &image_format, save_as_render, nullptr, -1);
image_render_print_save_message(reports, filepath_basis, ok, errno);
}
@ -956,7 +970,8 @@ bool BKE_image_render_write(ReportList *reports,
}
if (is_exr_rr) {
ok = BKE_image_render_write_exr(reports, rr, filepath, &image_format, true, rv->name, -1);
ok = BKE_image_render_write_exr(
reports, rr, filepath, &image_format, save_as_render, rv->name, -1);
image_render_print_save_message(reports, filepath, ok, errno);
/* optional preview images for exr */
@ -971,7 +986,7 @@ bool BKE_image_render_write(ReportList *reports,
ImBuf *ibuf = RE_render_result_rect_to_ibuf(rr, &image_format, dither, view_id);
ibuf->planes = 24;
IMB_colormanagement_imbuf_for_write(ibuf, true, false, &image_format);
IMB_colormanagement_imbuf_for_write(ibuf, save_as_render, false, &image_format);
ok = image_render_write_stamp_test(
reports, scene, rr, ibuf, filepath, &image_format, stamp);
@ -982,7 +997,7 @@ bool BKE_image_render_write(ReportList *reports,
else {
ImBuf *ibuf = RE_render_result_rect_to_ibuf(rr, &image_format, dither, view_id);
IMB_colormanagement_imbuf_for_write(ibuf, true, false, &image_format);
IMB_colormanagement_imbuf_for_write(ibuf, save_as_render, false, &image_format);
ok = image_render_write_stamp_test(
reports, scene, rr, ibuf, filepath, &image_format, stamp);
@ -1009,7 +1024,7 @@ bool BKE_image_render_write(ReportList *reports,
for (i = 0; i < 2; i++) {
int view_id = BLI_findstringindex(&rr->views, names[i], offsetof(RenderView, name));
ibuf_arr[i] = RE_render_result_rect_to_ibuf(rr, &image_format, dither, view_id);
IMB_colormanagement_imbuf_for_write(ibuf_arr[i], true, false, &image_format);
IMB_colormanagement_imbuf_for_write(ibuf_arr[i], save_as_render, false, &image_format);
}
ibuf_arr[2] = IMB_stereo3d_ImBuf(&image_format, ibuf_arr[0], ibuf_arr[1]);

View File

@ -10,6 +10,7 @@ if(WITH_COMPOSITOR_CPU)
intern
nodes
operations
realtime_compositor
../blenkernel
../blentranslation
../imbuf
@ -117,8 +118,6 @@ if(WITH_COMPOSITOR_CPU)
nodes/COM_MaskNode.h
nodes/COM_MovieClipNode.cc
nodes/COM_MovieClipNode.h
nodes/COM_OutputFileNode.cc
nodes/COM_OutputFileNode.h
nodes/COM_RenderLayersNode.cc
nodes/COM_RenderLayersNode.h
nodes/COM_SceneTimeNode.cc
@ -139,6 +138,8 @@ if(WITH_COMPOSITOR_CPU)
# output nodes
nodes/COM_CompositorNode.cc
nodes/COM_CompositorNode.h
nodes/COM_FileOutputNode.cc
nodes/COM_FileOutputNode.h
nodes/COM_ViewLevelsNode.cc
nodes/COM_ViewLevelsNode.h
nodes/COM_ViewerNode.cc
@ -419,10 +420,8 @@ if(WITH_COMPOSITOR_CPU)
operations/COM_CompositorOperation.h
operations/COM_ConvertDepthToRadiusOperation.cc
operations/COM_ConvertDepthToRadiusOperation.h
operations/COM_OutputFileMultiViewOperation.cc
operations/COM_OutputFileMultiViewOperation.h
operations/COM_OutputFileOperation.cc
operations/COM_OutputFileOperation.h
operations/COM_FileOutputOperation.cc
operations/COM_FileOutputOperation.h
operations/COM_PreviewOperation.cc
operations/COM_PreviewOperation.h
operations/COM_SplitOperation.cc
@ -602,6 +601,7 @@ if(WITH_COMPOSITOR_CPU)
PRIVATE bf::dna
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
bf_realtime_compositor
extern_clew
PRIVATE bf::intern::atomic
)

View File

@ -7,6 +7,10 @@
#include "DNA_color_types.h"
#include "DNA_node_types.h"
namespace blender::realtime_compositor {
class RenderContext;
}
struct Render;
/* Keep ascii art. */
@ -332,7 +336,8 @@ void COM_execute(Render *render,
Scene *scene,
bNodeTree *node_tree,
bool rendering,
const char *view_name);
const char *view_name,
blender::realtime_compositor::RenderContext *render_context);
/**
* \brief Deinitialize the compositor caches and allocated memory.

View File

@ -12,6 +12,10 @@
struct bNodeInstanceHash;
namespace blender::realtime_compositor {
class RenderContext;
}
namespace blender::compositor {
/**
@ -70,6 +74,12 @@ class CompositorContext {
*/
const char *view_name_;
/**
* \brief Render context that contains information about active render. Can be null if the
* compositor is not executing as part of the render pipeline.
*/
realtime_compositor::RenderContext *render_context_;
public:
/**
* \brief constructor initializes the context with default values.
@ -192,6 +202,22 @@ class CompositorContext {
return view_name_ && view_name_[0] != '\0';
}
/**
* \brief get the render context
*/
realtime_compositor::RenderContext *get_render_context() const
{
return render_context_;
}
/**
* \brief set the render context
*/
void set_render_context(realtime_compositor::RenderContext *render_context)
{
render_context_ = render_context;
}
/**
* \brief get the active rendering view
*/

View File

@ -50,6 +50,7 @@
#include "COM_DistanceMatteNode.h"
#include "COM_DoubleEdgeMaskNode.h"
#include "COM_EllipseMaskNode.h"
#include "COM_FileOutputNode.h"
#include "COM_FilterNode.h"
#include "COM_FlipNode.h"
#include "COM_GammaNode.h"
@ -75,7 +76,6 @@
#include "COM_MovieDistortionNode.h"
#include "COM_NormalNode.h"
#include "COM_NormalizeNode.h"
#include "COM_OutputFileNode.h"
#include "COM_PixelateNode.h"
#include "COM_PlaneTrackDeformNode.h"
#include "COM_PosterizeNode.h"
@ -347,7 +347,7 @@ Node *COM_convert_bnode(bNode *b_node)
node = new ColorSpillNode(b_node);
break;
case CMP_NODE_OUTPUT_FILE:
node = new OutputFileNode(b_node);
node = new FileOutputNode(b_node);
break;
case CMP_NODE_MAP_VALUE:
node = new MapValueNode(b_node);

View File

@ -24,9 +24,11 @@ ExecutionSystem::ExecutionSystem(RenderData *rd,
bNodeTree *editingtree,
bool rendering,
bool fastcalculation,
const char *view_name)
const char *view_name,
realtime_compositor::RenderContext *render_context)
{
num_work_threads_ = WorkScheduler::get_num_cpu_threads();
context_.set_render_context(render_context);
context_.set_view_name(view_name);
context_.set_scene(scene);
context_.set_bnodetree(editingtree);

View File

@ -20,6 +20,10 @@
#include "DNA_scene_types.h"
#include "DNA_vec_types.h"
namespace blender::realtime_compositor {
class RenderContext;
}
namespace blender::compositor {
/**
@ -161,7 +165,8 @@ class ExecutionSystem {
bNodeTree *editingtree,
bool rendering,
bool fastcalculation,
const char *view_name);
const char *view_name,
realtime_compositor::RenderContext *render_context);
/**
* Destructor

View File

@ -51,6 +51,14 @@ void MetaData::add_to_render_result(RenderResult *render_result) const
}
}
void MetaData::for_each_entry(
FunctionRef<void(const std::string &, const std::string &)> callback) const
{
for (MapItem<std::string, std::string> entry : entries_.items()) {
callback(entry.key, entry.value);
}
}
void MetaDataExtractCallbackData::add_meta_data(blender::StringRef key,
blender::StringRefNull value)
{

View File

@ -7,6 +7,7 @@
#include <string>
#include "BKE_cryptomatte.hh"
#include "BLI_function_ref.hh"
#include "BLI_map.hh"
#include "MEM_guardedalloc.h"
@ -44,6 +45,8 @@ class MetaData {
*/
void replace_hash_neutral_cryptomatte_keys(const blender::StringRef layer_name);
void add_to_render_result(RenderResult *render_result) const;
/* Invokes the given callback on each entry of the meta data. */
void for_each_entry(FunctionRef<void(const std::string &, const std::string &)> callback) const;
#ifdef WITH_CXX_GUARDEDALLOC
MEM_CXX_CLASS_ALLOC_FUNCS("COM:MetaData")
#endif

View File

@ -54,7 +54,8 @@ void COM_execute(Render *render,
Scene *scene,
bNodeTree *node_tree,
bool rendering,
const char *view_name)
const char *view_name,
blender::realtime_compositor::RenderContext *render_context)
{
/* Initialize mutex, TODO: this mutex init is actually not thread safe and
* should be done somewhere as part of blender startup, all the other
@ -80,7 +81,8 @@ void COM_execute(Render *render,
node_tree->execution_mode == NTREE_EXECUTION_MODE_REALTIME)
{
/* Realtime GPU compositor. */
RE_compositor_execute(*render, *scene, *render_data, *node_tree, rendering, view_name);
RE_compositor_execute(
*render, *scene, *render_data, *node_tree, rendering, view_name, render_context);
}
else {
/* Tiled and Full Frame compositors. */
@ -94,7 +96,7 @@ void COM_execute(Render *render,
const bool twopass = (node_tree->flag & NTREE_TWO_PASS) && !rendering;
if (twopass) {
blender::compositor::ExecutionSystem fast_pass(
render_data, scene, node_tree, rendering, true, view_name);
render_data, scene, node_tree, rendering, true, view_name, render_context);
fast_pass.execute();
if (node_tree->runtime->test_break(node_tree->runtime->tbh)) {
@ -104,7 +106,7 @@ void COM_execute(Render *render,
}
blender::compositor::ExecutionSystem system(
render_data, scene, node_tree, rendering, false, view_name);
render_data, scene, node_tree, rendering, false, view_name, render_context);
system.execute();
}

View File

@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_FileOutputNode.h"
#include "BLI_string.h"
namespace blender::compositor {
FileOutputNode::FileOutputNode(bNode *editor_node) : Node(editor_node)
{
/* pass */
}
void FileOutputNode::convert_to_operations(NodeConverter &converter,
const CompositorContext &context) const
{
for (NodeInput *input : inputs_) {
if (input->is_linked()) {
converter.add_node_input_preview(input);
break;
}
}
if (!context.is_rendering()) {
return;
}
Vector<FileOutputInput> inputs;
for (NodeInput *input : inputs_) {
auto *storage = static_cast<NodeImageMultiFileSocket *>(input->get_bnode_socket()->storage);
inputs.append(FileOutputInput(storage, input->get_data_type()));
}
auto *storage = static_cast<const NodeImageMultiFile *>(this->get_bnode()->storage);
auto *output_operation = new FileOutputOperation(&context, storage, inputs);
converter.add_operation(output_operation);
for (int i = 0; i < inputs_.size(); i++) {
converter.map_input_socket(inputs_[i], output_operation->get_input_socket(i));
}
}
} // namespace blender::compositor

View File

@ -0,0 +1,26 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_Node.h"
#include "COM_FileOutputOperation.h"
#include "DNA_node_types.h"
namespace blender::compositor {
/**
* \brief FileOutputNode
* \ingroup Node
*/
class FileOutputNode : public Node {
public:
FileOutputNode(bNode *editor_node);
void convert_to_operations(NodeConverter &converter,
const CompositorContext &context) const override;
};
} // namespace blender::compositor

View File

@ -1,160 +0,0 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_OutputFileNode.h"
#include "BLI_string.h"
namespace blender::compositor {
OutputFileNode::OutputFileNode(bNode *editor_node) : Node(editor_node)
{
/* pass */
}
void OutputFileNode::add_input_sockets(OutputOpenExrMultiLayerOperation &operation) const
{
for (NodeInput *input : inputs_) {
NodeImageMultiFileSocket *sockdata =
(NodeImageMultiFileSocket *)input->get_bnode_socket()->storage;
/* NOTE: layer becomes an empty placeholder if the input is not linked. */
operation.add_layer(sockdata->layer, input->get_data_type(), input->is_linked());
}
}
void OutputFileNode::map_input_sockets(NodeConverter &converter,
OutputOpenExrMultiLayerOperation &operation) const
{
bool preview_added = false;
int index = 0;
for (NodeInput *input : inputs_) {
converter.map_input_socket(input, operation.get_input_socket(index++));
if (!preview_added) {
converter.add_node_input_preview(input);
preview_added = true;
}
}
}
void OutputFileNode::add_preview_to_first_linked_input(NodeConverter &converter) const
{
if (get_input_sockets().is_empty()) {
return;
}
NodeInput *first_socket = this->get_input_socket(0);
if (first_socket->is_linked()) {
converter.add_node_input_preview(first_socket);
}
}
void OutputFileNode::convert_to_operations(NodeConverter &converter,
const CompositorContext &context) const
{
const NodeImageMultiFile *storage = (const NodeImageMultiFile *)this->get_bnode()->storage;
const bool is_multiview = (context.get_render_data()->scemode & R_MULTIVIEW) != 0;
add_preview_to_first_linked_input(converter);
if (!context.is_rendering()) {
/* only output files when rendering a sequence -
* otherwise, it overwrites the output files just
* scrubbing through the timeline when the compositor updates.
*/
return;
}
if (storage->format.imtype == R_IMF_IMTYPE_MULTILAYER) {
const bool use_half_float = (storage->format.depth == R_IMF_CHAN_DEPTH_16);
/* Single output operation for the multi-layer file. */
OutputOpenExrMultiLayerOperation *output_operation;
if (is_multiview && storage->format.views_format == R_IMF_VIEWS_MULTIVIEW) {
output_operation = new OutputOpenExrMultiLayerMultiViewOperation(context.get_scene(),
context.get_render_data(),
context.get_bnodetree(),
storage->base_path,
storage->format.exr_codec,
use_half_float,
context.get_view_name());
}
else {
output_operation = new OutputOpenExrMultiLayerOperation(context.get_scene(),
context.get_render_data(),
context.get_bnodetree(),
storage->base_path,
storage->format.exr_codec,
use_half_float,
context.get_view_name());
}
converter.add_operation(output_operation);
/* First add all inputs. Inputs are stored in a Vector and can be moved to a different
* memory address during this time. */
add_input_sockets(*output_operation);
/* After adding the sockets the memory addresses will stick. */
map_input_sockets(converter, *output_operation);
}
else { /* single layer format */
for (NodeInput *input : inputs_) {
if (input->is_linked()) {
NodeImageMultiFileSocket *sockdata =
(NodeImageMultiFileSocket *)input->get_bnode_socket()->storage;
const ImageFormatData *format = (sockdata->use_node_format ? &storage->format :
&sockdata->format);
char path[FILE_MAX];
/* combine file path for the input */
if (sockdata->path[0]) {
BLI_path_join(path, FILE_MAX, storage->base_path, sockdata->path);
}
else {
STRNCPY(path, storage->base_path);
BLI_path_slash_ensure(path, FILE_MAX);
}
NodeOperation *output_operation = nullptr;
if (is_multiview && format->views_format == R_IMF_VIEWS_MULTIVIEW) {
output_operation = new OutputOpenExrSingleLayerMultiViewOperation(
context.get_scene(),
context.get_render_data(),
context.get_bnodetree(),
input->get_data_type(),
format,
path,
context.get_view_name(),
sockdata->save_as_render);
}
else if ((!is_multiview) || (format->views_format == R_IMF_VIEWS_INDIVIDUAL)) {
output_operation = new OutputSingleLayerOperation(context.get_scene(),
context.get_render_data(),
context.get_bnodetree(),
input->get_data_type(),
format,
path,
context.get_view_name(),
sockdata->save_as_render);
}
else { /* R_IMF_VIEWS_STEREO_3D */
output_operation = new OutputStereoOperation(context.get_scene(),
context.get_render_data(),
context.get_bnodetree(),
input->get_data_type(),
format,
path,
sockdata->layer,
context.get_view_name(),
sockdata->save_as_render);
}
converter.add_operation(output_operation);
converter.map_input_socket(input, output_operation->get_input_socket(0));
}
}
}
}
} // namespace blender::compositor

View File

@ -1,32 +0,0 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_Node.h"
#include "COM_OutputFileMultiViewOperation.h"
#include "DNA_node_types.h"
namespace blender::compositor {
/**
* \brief OutputFileNode
* \ingroup Node
*/
class OutputFileNode : public Node {
public:
OutputFileNode(bNode *editor_node);
void convert_to_operations(NodeConverter &converter,
const CompositorContext &context) const override;
private:
void add_preview_to_first_linked_input(NodeConverter &converter) const;
void add_input_sockets(OutputOpenExrMultiLayerOperation &operation) const;
void map_input_sockets(NodeConverter &converter,
OutputOpenExrMultiLayerOperation &operation) const;
};
} // namespace blender::compositor

View File

@ -0,0 +1,382 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <memory>
#include "BLI_fileops.h"
#include "BLI_path_util.h"
#include "BLI_string.h"
#include "BLI_string_utils.hh"
#include "BLI_utildefines.h"
#include "DNA_node_types.h"
#include "DNA_scene_types.h"
#include "BKE_image.h"
#include "BKE_image_format.h"
#include "BKE_main.hh"
#include "BKE_scene.h"
#include "RE_pipeline.h"
#include "COM_FileOutputOperation.h"
#include "COM_render_context.hh"
namespace blender::compositor {
FileOutputInput::FileOutputInput(NodeImageMultiFileSocket *data, DataType data_type)
: data(data), data_type(data_type)
{
}
static int get_channels_count(DataType datatype)
{
switch (datatype) {
case DataType::Value:
return 1;
case DataType::Vector:
return 3;
case DataType::Color:
return 4;
default:
return 0;
}
}
static float *initialize_buffer(uint width, uint height, DataType datatype)
{
const int size = get_channels_count(datatype);
return static_cast<float *>(
MEM_malloc_arrayN(size_t(width) * height, sizeof(float) * size, "File Output Buffer."));
}
static void write_buffer_rect(
rcti *rect, SocketReader *reader, float *buffer, uint width, DataType datatype)
{
if (!buffer) {
return;
}
int x1 = rect->xmin;
int y1 = rect->ymin;
int x2 = rect->xmax;
int y2 = rect->ymax;
int size = get_channels_count(datatype);
int offset = (y1 * width + x1) * size;
for (int y = y1; y < y2; y++) {
for (int x = x1; x < x2; x++) {
float color[4];
reader->read_sampled(color, x, y, PixelSampler::Nearest);
for (int i = 0; i < size; i++) {
buffer[offset + i] = color[i];
}
offset += size;
}
offset += (width - (x2 - x1)) * size;
}
}
FileOutputOperation::FileOutputOperation(const CompositorContext *context,
const NodeImageMultiFile *node_data,
Vector<FileOutputInput> inputs)
: context_(context), node_data_(node_data), file_output_inputs_(inputs)
{
for (const FileOutputInput &input : inputs) {
add_input_socket(input.data_type);
}
this->set_canvas_input_index(RESOLUTION_INPUT_ANY);
}
void FileOutputOperation::init_execution()
{
for (int i = 0; i < file_output_inputs_.size(); i++) {
FileOutputInput &input = file_output_inputs_[i];
input.image_input = get_input_socket_reader(i);
if (!input.image_input) {
continue;
}
input.output_buffer = initialize_buffer(get_width(), get_height(), input.data_type);
}
}
void FileOutputOperation::execute_region(rcti *rect, uint /*tile_number*/)
{
for (int i = 0; i < file_output_inputs_.size(); i++) {
const FileOutputInput &input = file_output_inputs_[i];
if (!input.image_input || !input.output_buffer) {
continue;
}
write_buffer_rect(rect, input.image_input, input.output_buffer, get_width(), input.data_type);
}
}
void FileOutputOperation::update_memory_buffer_partial(MemoryBuffer * /*output*/,
const rcti &area,
Span<MemoryBuffer *> inputs)
{
for (int i = 0; i < file_output_inputs_.size(); i++) {
const FileOutputInput &input = file_output_inputs_[i];
if (!input.output_buffer) {
continue;
}
int channels_count = get_channels_count(input.data_type);
MemoryBuffer output_buf(input.output_buffer, channels_count, get_width(), get_height());
output_buf.copy_from(inputs[i], area, 0, inputs[i]->get_num_channels(), 0);
}
}
static void add_meta_data_for_input(realtime_compositor::FileOutput &file_output,
const FileOutputInput &input)
{
std::unique_ptr<MetaData> meta_data = input.image_input->get_meta_data();
if (!meta_data) {
return;
}
blender::StringRef layer_name = blender::bke::cryptomatte::BKE_cryptomatte_extract_layer_name(
blender::StringRef(input.data->layer,
BLI_strnlen(input.data->layer, sizeof(input.data->layer))));
meta_data->replace_hash_neutral_cryptomatte_keys(layer_name);
meta_data->for_each_entry([&](const std::string &key, const std::string &value) {
file_output.add_meta_data(key, value);
});
}
void FileOutputOperation::deinit_execution()
{
if (is_multi_layer()) {
execute_multi_layer();
}
else {
execute_single_layer();
}
}
/* --------------------
* Single Layer Images.
*/
void FileOutputOperation::execute_single_layer()
{
const int2 size = int2(get_width(), get_height());
for (const FileOutputInput &input : file_output_inputs_) {
/* Unlinked input. */
if (!input.image_input) {
continue;
}
char base_path[FILE_MAX];
get_single_layer_image_base_path(input.data->path, base_path);
/* The image saving code expects EXR images to have a different structure than standard
* images. In particular, in EXR images, the buffers need to be stored in passes that are, in
* turn, stored in a render layer. On the other hand, in non-EXR images, the buffers need to
* be stored in views. An exception to this is stereo images, which needs to have the same
* structure as non-EXR images. */
const auto &format = input.data->use_node_format ? node_data_->format : input.data->format;
const bool is_exr = format.imtype == R_IMF_IMTYPE_OPENEXR;
const int views_count = BKE_scene_multiview_num_views_get(context_->get_render_data());
if (is_exr && !(format.views_format == R_IMF_VIEWS_STEREO_3D && views_count == 2)) {
execute_single_layer_multi_view_exr(input, format, base_path);
continue;
}
char image_path[FILE_MAX];
get_single_layer_image_path(base_path, format, image_path);
realtime_compositor::FileOutput &file_output = context_->get_render_context()->get_file_output(
image_path, format, size, input.data->save_as_render);
add_view_for_input(file_output, input, context_->get_view_name());
add_meta_data_for_input(file_output, input);
}
}
/* -----------------------------------
* Single Layer Multi-View EXR Images.
*/
void FileOutputOperation::execute_single_layer_multi_view_exr(const FileOutputInput &input,
const ImageFormatData &format,
const char *base_path)
{
const bool has_views = format.views_format != R_IMF_VIEWS_INDIVIDUAL;
/* The EXR stores all views in the same file, so we supply an empty view to make sure the file
* name does not contain a view suffix. */
char image_path[FILE_MAX];
const char *path_view = has_views ? "" : context_->get_view_name();
get_multi_layer_exr_image_path(base_path, path_view, image_path);
const int2 size = int2(get_width(), get_height());
realtime_compositor::FileOutput &file_output = context_->get_render_context()->get_file_output(
image_path, format, size, false);
/* The EXR stores all views in the same file, so we add the actual render view. Otherwise, we
* add a default unnamed view. */
const char *view_name = has_views ? context_->get_view_name() : "";
file_output.add_view(view_name);
add_pass_for_input(file_output, input, "", view_name);
add_meta_data_for_input(file_output, input);
}
/* -----------------------
* Multi-Layer EXR Images.
*/
void FileOutputOperation::execute_multi_layer()
{
const bool store_views_in_single_file = is_multi_view_exr();
const char *view = context_->get_view_name();
/* If we are saving all views in a single multi-layer file, we supply an empty view to make
* sure the file name does not contain a view suffix. */
char image_path[FILE_MAX];
const char *write_view = store_views_in_single_file ? "" : view;
get_multi_layer_exr_image_path(get_base_path(), write_view, image_path);
const int2 size = int2(get_width(), get_height());
const ImageFormatData format = node_data_->format;
realtime_compositor::FileOutput &file_output = context_->get_render_context()->get_file_output(
image_path, format, size, false);
/* If we are saving views in separate files, we needn't store the view in the channel names, so
* we add an unnamed view. */
const char *pass_view = store_views_in_single_file ? view : "";
file_output.add_view(pass_view);
for (const FileOutputInput &input : file_output_inputs_) {
/* Unlinked input. */
if (!input.image_input) {
continue;
}
const char *pass_name = input.data->layer;
add_pass_for_input(file_output, input, pass_name, pass_view);
add_meta_data_for_input(file_output, input);
}
}
/* Add a pass of the given name, view, and input buffer. The pass channel identifiers follows the
* EXR conventions. */
void FileOutputOperation::add_pass_for_input(realtime_compositor::FileOutput &file_output,
const FileOutputInput &input,
const char *pass_name,
const char *view_name)
{
switch (input.data_type) {
case DataType::Color:
file_output.add_pass(pass_name, view_name, "RGBA", input.output_buffer);
break;
case DataType::Vector:
file_output.add_pass(pass_name, view_name, "XYZ", input.output_buffer);
break;
case DataType::Value:
file_output.add_pass(pass_name, view_name, "V", input.output_buffer);
break;
}
}
/* Add a view of the given name and input buffer. */
void FileOutputOperation::add_view_for_input(realtime_compositor::FileOutput &file_output,
const FileOutputInput &input,
const char *view_name)
{
switch (input.data_type) {
case DataType::Color:
file_output.add_view(view_name, 4, input.output_buffer);
break;
case DataType::Vector:
file_output.add_view(view_name, 3, input.output_buffer);
break;
case DataType::Value:
file_output.add_view(view_name, 1, input.output_buffer);
break;
}
}
/* Get the base path of the image to be saved, based on the base path of the node. The base name
* is an optional initial name of the image, which will later be concatenated with other
* information like the frame number, view, and extension. If the base name is empty, then the
* base path represents a directory, so a trailing slash is ensured. */
void FileOutputOperation::get_single_layer_image_base_path(const char *base_name, char *base_path)
{
if (base_name[0]) {
BLI_path_join(base_path, FILE_MAX, get_base_path(), base_name);
}
else {
BLI_strncpy(base_path, get_base_path(), FILE_MAX);
BLI_path_slash_ensure(base_path, FILE_MAX);
}
}
/* Get the path of the image to be saved based on the given format. */
void FileOutputOperation::get_single_layer_image_path(const char *base_path,
const ImageFormatData &format,
char *image_path)
{
BKE_image_path_from_imformat(image_path,
base_path,
BKE_main_blendfile_path_from_global(),
context_->get_framenumber(),
&format,
use_file_extension(),
true,
nullptr);
}
/* Get the path of the EXR image to be saved. If the given view is not empty, its corresponding
* file suffix will be appended to the name. */
void FileOutputOperation::get_multi_layer_exr_image_path(const char *base_path,
const char *view,
char *image_path)
{
const char *suffix = BKE_scene_multiview_view_suffix_get(context_->get_render_data(), view);
BKE_image_path_from_imtype(image_path,
base_path,
BKE_main_blendfile_path_from_global(),
context_->get_framenumber(),
R_IMF_IMTYPE_MULTILAYER,
use_file_extension(),
true,
suffix);
}
bool FileOutputOperation::is_multi_layer()
{
return node_data_->format.imtype == R_IMF_IMTYPE_MULTILAYER;
}
const char *FileOutputOperation::get_base_path()
{
return node_data_->base_path;
}
/* Add the file format extensions to the rendered file name. */
bool FileOutputOperation::use_file_extension()
{
return context_->get_render_data()->scemode & R_EXTENSION;
}
/* If true, save views in a multi-view EXR file, otherwise, save each view in its own file. */
bool FileOutputOperation::is_multi_view_exr()
{
if (!is_multi_view_scene()) {
return false;
}
return node_data_->format.views_format == R_IMF_VIEWS_MULTIVIEW;
}
bool FileOutputOperation::is_multi_view_scene()
{
return context_->get_render_data()->scemode & R_MULTIVIEW;
}
} // namespace blender::compositor

View File

@ -0,0 +1,106 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_vector.hh"
#include "DNA_node_types.h"
#include "COM_CompositorContext.h"
#include "COM_MultiThreadedOperation.h"
struct StampData;
namespace blender::realtime_compositor {
class FileOutput;
}
namespace blender::compositor {
struct FileOutputInput {
FileOutputInput(NodeImageMultiFileSocket *data, DataType data_type);
NodeImageMultiFileSocket *data;
DataType data_type;
float *output_buffer = nullptr;
SocketReader *image_input = nullptr;
};
class FileOutputOperation : public MultiThreadedOperation {
private:
const CompositorContext *context_;
const NodeImageMultiFile *node_data_;
Vector<FileOutputInput> file_output_inputs_;
public:
FileOutputOperation(const CompositorContext *context,
const NodeImageMultiFile *node_data,
Vector<FileOutputInput> inputs);
void execute_region(rcti *rect, unsigned int tile_number) override;
bool is_output_operation(bool /*rendering*/) const override
{
return true;
}
void init_execution() override;
void deinit_execution() override;
eCompositorPriority get_render_priority() const override
{
return eCompositorPriority::Low;
}
void update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs) override;
private:
void execute_single_layer();
void execute_single_layer_multi_view_exr(const FileOutputInput &input,
const ImageFormatData &format,
const char *base_path);
void execute_multi_layer();
/* Add a pass of the given name, view, and input buffer. The pass channel identifiers follows the
* EXR conventions. */
void add_pass_for_input(realtime_compositor::FileOutput &file_output,
const FileOutputInput &input,
const char *pass_name,
const char *view_name);
/* Add a view of the given name and input buffer. */
void add_view_for_input(realtime_compositor::FileOutput &file_output,
const FileOutputInput &input,
const char *view_name);
/* Get the base path of the image to be saved, based on the base path of the node. The base name
* is an optional initial name of the image, which will later be concatenated with other
* information like the frame number, view, and extension. If the base name is empty, then the
* base path represents a directory, so a trailing slash is ensured. */
void get_single_layer_image_base_path(const char *base_name, char *base_path);
/* Get the path of the image to be saved based on the given format. */
void get_single_layer_image_path(const char *base_path,
const ImageFormatData &format,
char *image_path);
/* Get the path of the EXR image to be saved. If the given view is not empty, its corresponding
* file suffix will be appended to the name. */
void get_multi_layer_exr_image_path(const char *base_path, const char *view, char *image_path);
bool is_multi_layer();
const char *get_base_path();
/* Add the file format extensions to the rendered file name. */
bool use_file_extension();
/* If true, save views in a multi-view EXR file, otherwise, save each view in its own file. */
bool is_multi_view_exr();
bool is_multi_view_scene();
};
} // namespace blender::compositor

View File

@ -1,353 +0,0 @@
/* SPDX-FileCopyrightText: 2015 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_OutputFileMultiViewOperation.h"
#include "BLI_fileops.h"
#include "BLI_string.h"
#include "BKE_image.h"
#include "BKE_image_format.h"
#include "BKE_main.hh"
#include "BKE_scene.h"
#include "IMB_colormanagement.h"
#include "IMB_imbuf.h"
#include "IMB_imbuf_types.h"
namespace blender::compositor {
/************************************ OpenEXR Singlelayer Multiview ******************************/
OutputOpenExrSingleLayerMultiViewOperation::OutputOpenExrSingleLayerMultiViewOperation(
const Scene *scene,
const RenderData *rd,
const bNodeTree *tree,
DataType datatype,
const ImageFormatData *format,
const char *path,
const char *view_name,
const bool save_as_render)
: OutputSingleLayerOperation(
scene, rd, tree, datatype, format, path, view_name, save_as_render)
{
}
void *OutputOpenExrSingleLayerMultiViewOperation::get_handle(const char *filepath)
{
size_t width = this->get_width();
size_t height = this->get_height();
if (width != 0 && height != 0) {
void *exrhandle;
exrhandle = IMB_exr_get_handle_name(filepath);
if (!BKE_scene_multiview_is_render_view_first(rd_, view_name_)) {
return exrhandle;
}
IMB_exr_clear_channels(exrhandle);
LISTBASE_FOREACH (SceneRenderView *, srv, &rd_->views) {
if (BKE_scene_multiview_is_render_view_active(rd_, srv) == false) {
continue;
}
IMB_exr_add_view(exrhandle, srv->name);
add_exr_channels(exrhandle, nullptr, datatype_, srv->name, width, false, nullptr);
}
BLI_file_ensure_parent_dir_exists(filepath);
/* prepare the file with all the channels */
if (!IMB_exr_begin_write(exrhandle, filepath, width, height, format_.exr_codec, nullptr)) {
printf("Error Writing Singlelayer Multiview Openexr\n");
IMB_exr_close(exrhandle);
}
else {
IMB_exr_clear_channels(exrhandle);
return exrhandle;
}
}
return nullptr;
}
void OutputOpenExrSingleLayerMultiViewOperation::deinit_execution()
{
uint width = this->get_width();
uint height = this->get_height();
if (width != 0 && height != 0) {
void *exrhandle;
char filepath[FILE_MAX];
BKE_image_path_from_imtype(filepath,
path_,
BKE_main_blendfile_path_from_global(),
rd_->cfra,
R_IMF_IMTYPE_OPENEXR,
(rd_->scemode & R_EXTENSION) != 0,
true,
nullptr);
exrhandle = this->get_handle(filepath);
add_exr_channels(exrhandle,
nullptr,
datatype_,
view_name_,
width,
format_.depth == R_IMF_CHAN_DEPTH_16,
output_buffer_);
/* memory can only be freed after we write all views to the file */
output_buffer_ = nullptr;
image_input_ = nullptr;
/* ready to close the file */
if (BKE_scene_multiview_is_render_view_last(rd_, view_name_)) {
IMB_exr_write_channels(exrhandle);
/* free buffer memory for all the views */
free_exr_channels(exrhandle, rd_, nullptr, datatype_);
/* remove exr handle and data */
IMB_exr_close(exrhandle);
}
}
}
/************************************ OpenEXR Multilayer Multiview *******************************/
OutputOpenExrMultiLayerMultiViewOperation::OutputOpenExrMultiLayerMultiViewOperation(
const Scene *scene,
const RenderData *rd,
const bNodeTree *tree,
const char *path,
char exr_codec,
bool exr_half_float,
const char *view_name)
: OutputOpenExrMultiLayerOperation(scene, rd, tree, path, exr_codec, exr_half_float, view_name)
{
}
void *OutputOpenExrMultiLayerMultiViewOperation::get_handle(const char *filepath)
{
uint width = this->get_width();
uint height = this->get_height();
if (width != 0 && height != 0) {
/* Get a new global handle. */
void *exrhandle = IMB_exr_get_handle_name(filepath);
if (!BKE_scene_multiview_is_render_view_first(rd_, view_name_)) {
return exrhandle;
}
IMB_exr_clear_channels(exrhandle);
/* check renderdata for amount of views */
LISTBASE_FOREACH (SceneRenderView *, srv, &rd_->views) {
if (BKE_scene_multiview_is_render_view_active(rd_, srv) == false) {
continue;
}
IMB_exr_add_view(exrhandle, srv->name);
for (uint i = 0; i < layers_.size(); i++) {
add_exr_channels(exrhandle,
layers_[i].name,
layers_[i].datatype,
srv->name,
width,
exr_half_float_,
nullptr);
}
}
BLI_file_ensure_parent_dir_exists(filepath);
/* prepare the file with all the channels for the header */
StampData *stamp_data = create_stamp_data();
if (!IMB_exr_begin_write(exrhandle, filepath, width, height, exr_codec_, stamp_data)) {
printf("Error Writing Multilayer Multiview Openexr\n");
IMB_exr_close(exrhandle);
BKE_stamp_data_free(stamp_data);
}
else {
IMB_exr_clear_channels(exrhandle);
BKE_stamp_data_free(stamp_data);
return exrhandle;
}
}
return nullptr;
}
void OutputOpenExrMultiLayerMultiViewOperation::deinit_execution()
{
uint width = this->get_width();
uint height = this->get_height();
if (width != 0 && height != 0) {
void *exrhandle;
char filepath[FILE_MAX];
BKE_image_path_from_imtype(filepath,
path_,
BKE_main_blendfile_path_from_global(),
rd_->cfra,
R_IMF_IMTYPE_MULTILAYER,
(rd_->scemode & R_EXTENSION) != 0,
true,
nullptr);
exrhandle = this->get_handle(filepath);
for (uint i = 0; i < layers_.size(); i++) {
add_exr_channels(exrhandle,
layers_[i].name,
layers_[i].datatype,
view_name_,
width,
exr_half_float_,
layers_[i].output_buffer);
}
for (uint i = 0; i < layers_.size(); i++) {
/* memory can only be freed after we write all views to the file */
layers_[i].output_buffer = nullptr;
layers_[i].image_input = nullptr;
}
/* ready to close the file */
if (BKE_scene_multiview_is_render_view_last(rd_, view_name_)) {
IMB_exr_write_channels(exrhandle);
/* free buffer memory for all the views */
for (uint i = 0; i < layers_.size(); i++) {
free_exr_channels(exrhandle, rd_, layers_[i].name, layers_[i].datatype);
}
IMB_exr_close(exrhandle);
}
}
}
/******************************** Stereo3D ******************************/
OutputStereoOperation::OutputStereoOperation(const Scene *scene,
const RenderData *rd,
const bNodeTree *tree,
DataType datatype,
const ImageFormatData *format,
const char *path,
const char *pass_name,
const char *view_name,
const bool save_as_render)
: OutputSingleLayerOperation(
scene, rd, tree, datatype, format, path, view_name, save_as_render)
{
STRNCPY(pass_name_, pass_name);
channels_ = get_datatype_size(datatype);
}
void *OutputStereoOperation::get_handle(const char *filepath)
{
size_t width = this->get_width();
size_t height = this->get_height();
const char *names[2] = {STEREO_LEFT_NAME, STEREO_RIGHT_NAME};
size_t i;
if (width != 0 && height != 0) {
void *exrhandle;
exrhandle = IMB_exr_get_handle_name(filepath);
if (!BKE_scene_multiview_is_render_view_first(rd_, view_name_)) {
return exrhandle;
}
IMB_exr_clear_channels(exrhandle);
for (i = 0; i < 2; i++) {
IMB_exr_add_view(exrhandle, names[i]);
}
return exrhandle;
}
return nullptr;
}
void OutputStereoOperation::deinit_execution()
{
uint width = this->get_width();
uint height = this->get_height();
if (width != 0 && height != 0) {
void *exrhandle;
exrhandle = this->get_handle(path_);
float *buf = output_buffer_;
/* populate single EXR channel with view data */
IMB_exr_add_channel(exrhandle,
nullptr,
pass_name_,
view_name_,
1,
channels_ * width * height,
buf,
format_.depth == R_IMF_CHAN_DEPTH_16);
image_input_ = nullptr;
output_buffer_ = nullptr;
/* create stereo ibuf */
if (BKE_scene_multiview_is_render_view_last(rd_, view_name_)) {
ImBuf *ibuf[3] = {nullptr};
const char *names[2] = {STEREO_LEFT_NAME, STEREO_RIGHT_NAME};
char filepath[FILE_MAX];
int i;
/* get rectf from EXR */
for (i = 0; i < 2; i++) {
float *rectf = IMB_exr_channel_rect(exrhandle, nullptr, pass_name_, names[i]);
ibuf[i] = IMB_allocImBuf(width, height, format_.planes, 0);
ibuf[i]->channels = channels_;
ibuf[i]->dither = rd_->dither_intensity;
IMB_assign_float_buffer(ibuf[i], rectf, IB_TAKE_OWNERSHIP);
/* do colormanagement in the individual views, so it doesn't need to do in the stereo */
IMB_colormanagement_imbuf_for_write(ibuf[i], true, false, &format_);
}
/* create stereo buffer */
ibuf[2] = IMB_stereo3d_ImBuf(&format_, ibuf[0], ibuf[1]);
BKE_image_path_from_imformat(filepath,
path_,
BKE_main_blendfile_path_from_global(),
rd_->cfra,
&format_,
(rd_->scemode & R_EXTENSION) != 0,
true,
nullptr);
BKE_imbuf_write(ibuf[2], filepath, &format_);
/* imbuf knows which rects are not part of ibuf */
for (i = 0; i < 3; i++) {
IMB_freeImBuf(ibuf[i]);
}
IMB_exr_close(exrhandle);
}
}
}
} // namespace blender::compositor

View File

@ -1,71 +0,0 @@
/* SPDX-FileCopyrightText: 2015 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_NodeOperation.h"
#include "COM_OutputFileOperation.h"
#include "BLI_path_util.h"
#include "BLI_rect.h"
#include "DNA_color_types.h"
#include "IMB_openexr.h"
namespace blender::compositor {
class OutputOpenExrSingleLayerMultiViewOperation : public OutputSingleLayerOperation {
private:
public:
OutputOpenExrSingleLayerMultiViewOperation(const Scene *scene,
const RenderData *rd,
const bNodeTree *tree,
DataType datatype,
const ImageFormatData *format,
const char *path,
const char *view_name,
bool save_as_render);
void *get_handle(const char *filepath);
void deinit_execution() override;
};
/** Writes inputs into OpenEXR multi-layer channels. */
class OutputOpenExrMultiLayerMultiViewOperation : public OutputOpenExrMultiLayerOperation {
private:
public:
OutputOpenExrMultiLayerMultiViewOperation(const Scene *scene,
const RenderData *rd,
const bNodeTree *tree,
const char *path,
char exr_codec,
bool exr_half_float,
const char *view_name);
void *get_handle(const char *filepath);
void deinit_execution() override;
};
class OutputStereoOperation : public OutputSingleLayerOperation {
private:
/* NOTE: Using FILE_MAX here is misleading, this is not a file path. */
char pass_name_[FILE_MAX];
size_t channels_;
public:
OutputStereoOperation(const Scene *scene,
const RenderData *rd,
const bNodeTree *tree,
DataType datatype,
const struct ImageFormatData *format,
const char *path,
const char *pass_name,
const char *view_name,
bool save_as_render);
void *get_handle(const char *filepath);
void deinit_execution() override;
};
} // namespace blender::compositor

View File

@ -1,467 +0,0 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_OutputFileOperation.h"
#include "BLI_fileops.h"
#include "BLI_listbase.h"
#include "BLI_string.h"
#include "BKE_image.h"
#include "BKE_image_format.h"
#include "BKE_main.hh"
#include "BKE_scene.h"
#include "DNA_color_types.h"
#include "IMB_colormanagement.h"
#include "IMB_imbuf.h"
#include "IMB_imbuf_types.h"
#include "RE_pipeline.h"
namespace blender::compositor {
void add_exr_channels(void *exrhandle,
const char *layer_name,
const DataType datatype,
const char *view_name,
const size_t width,
bool use_half_float,
float *buf)
{
/* create channels */
switch (datatype) {
case DataType::Value:
IMB_exr_add_channel(
exrhandle, layer_name, "V", view_name, 1, width, buf ? buf : nullptr, use_half_float);
break;
case DataType::Vector:
IMB_exr_add_channel(exrhandle,
layer_name,
"X",
view_name,
3,
3 * width,
buf ? buf : nullptr,
use_half_float);
IMB_exr_add_channel(exrhandle,
layer_name,
"Y",
view_name,
3,
3 * width,
buf ? buf + 1 : nullptr,
use_half_float);
IMB_exr_add_channel(exrhandle,
layer_name,
"Z",
view_name,
3,
3 * width,
buf ? buf + 2 : nullptr,
use_half_float);
break;
case DataType::Color:
IMB_exr_add_channel(exrhandle,
layer_name,
"R",
view_name,
4,
4 * width,
buf ? buf : nullptr,
use_half_float);
IMB_exr_add_channel(exrhandle,
layer_name,
"G",
view_name,
4,
4 * width,
buf ? buf + 1 : nullptr,
use_half_float);
IMB_exr_add_channel(exrhandle,
layer_name,
"B",
view_name,
4,
4 * width,
buf ? buf + 2 : nullptr,
use_half_float);
IMB_exr_add_channel(exrhandle,
layer_name,
"A",
view_name,
4,
4 * width,
buf ? buf + 3 : nullptr,
use_half_float);
break;
default:
break;
}
}
void free_exr_channels(void *exrhandle,
const RenderData *rd,
const char *layer_name,
const DataType datatype)
{
/* check renderdata for amount of views */
LISTBASE_FOREACH (SceneRenderView *, srv, &rd->views) {
float *rect = nullptr;
if (BKE_scene_multiview_is_render_view_active(rd, srv) == false) {
continue;
}
/* the pointer is stored in the first channel of each datatype */
switch (datatype) {
case DataType::Value:
rect = IMB_exr_channel_rect(exrhandle, layer_name, "V", srv->name);
break;
case DataType::Vector:
rect = IMB_exr_channel_rect(exrhandle, layer_name, "X", srv->name);
break;
case DataType::Color:
rect = IMB_exr_channel_rect(exrhandle, layer_name, "R", srv->name);
break;
default:
break;
}
if (rect) {
MEM_freeN(rect);
}
}
}
int get_datatype_size(DataType datatype)
{
switch (datatype) {
case DataType::Value:
return 1;
case DataType::Vector:
return 3;
case DataType::Color:
return 4;
default:
return 0;
}
}
static float *init_buffer(uint width, uint height, DataType datatype)
{
/* When initializing the tree during initial load the width and height can be zero. */
if (width != 0 && height != 0) {
int size = get_datatype_size(datatype);
return (float *)MEM_callocN(width * height * size * sizeof(float), "OutputFile buffer");
}
return nullptr;
}
static void write_buffer_rect(rcti *rect,
const bNodeTree *tree,
SocketReader *reader,
float *buffer,
uint width,
DataType datatype)
{
float color[4];
int i, size = get_datatype_size(datatype);
if (!buffer) {
return;
}
int x1 = rect->xmin;
int y1 = rect->ymin;
int x2 = rect->xmax;
int y2 = rect->ymax;
int offset = (y1 * width + x1) * size;
int x;
int y;
bool breaked = false;
for (y = y1; y < y2 && (!breaked); y++) {
for (x = x1; x < x2 && (!breaked); x++) {
reader->read_sampled(color, x, y, PixelSampler::Nearest);
for (i = 0; i < size; i++) {
buffer[offset + i] = color[i];
}
offset += size;
if (tree->runtime->test_break && tree->runtime->test_break(tree->runtime->tbh)) {
breaked = true;
}
}
offset += (width - (x2 - x1)) * size;
}
}
OutputSingleLayerOperation::OutputSingleLayerOperation(const Scene *scene,
const RenderData *rd,
const bNodeTree *tree,
DataType datatype,
const ImageFormatData *format,
const char *path,
const char *view_name,
const bool save_as_render)
{
rd_ = rd;
tree_ = tree;
this->add_input_socket(datatype);
output_buffer_ = nullptr;
datatype_ = datatype;
image_input_ = nullptr;
BKE_image_format_init_for_write(&format_, scene, format);
if (!save_as_render) {
/* If not saving as render, stop IMB_colormanagement_imbuf_for_write using this
* colorspace for conversion. */
format_.linear_colorspace_settings.name[0] = '\0';
}
STRNCPY(path_, path);
view_name_ = view_name;
save_as_render_ = save_as_render;
}
OutputSingleLayerOperation::~OutputSingleLayerOperation()
{
BKE_image_format_free(&format_);
}
void OutputSingleLayerOperation::init_execution()
{
image_input_ = get_input_socket_reader(0);
output_buffer_ = init_buffer(this->get_width(), this->get_height(), datatype_);
}
void OutputSingleLayerOperation::execute_region(rcti *rect, uint /*tile_number*/)
{
write_buffer_rect(rect, tree_, image_input_, output_buffer_, this->get_width(), datatype_);
}
void OutputSingleLayerOperation::deinit_execution()
{
if (this->get_width() * this->get_height() != 0) {
int size = get_datatype_size(datatype_);
ImBuf *ibuf = IMB_allocImBuf(this->get_width(), this->get_height(), format_.planes, 0);
char filepath[FILE_MAX];
const char *suffix;
ibuf->channels = size;
ibuf->dither = rd_->dither_intensity;
IMB_assign_float_buffer(ibuf, output_buffer_, IB_TAKE_OWNERSHIP);
IMB_colormanagement_imbuf_for_write(ibuf, save_as_render_, false, &format_);
suffix = BKE_scene_multiview_view_suffix_get(rd_, view_name_);
BKE_image_path_from_imformat(filepath,
path_,
BKE_main_blendfile_path_from_global(),
rd_->cfra,
&format_,
(rd_->scemode & R_EXTENSION) != 0,
true,
suffix);
if (0 == BKE_imbuf_write(ibuf, filepath, &format_)) {
printf("Cannot save Node File Output to %s\n", filepath);
}
else {
printf("Saved: %s\n", filepath);
}
IMB_freeImBuf(ibuf);
}
output_buffer_ = nullptr;
image_input_ = nullptr;
}
void OutputSingleLayerOperation::update_memory_buffer_partial(MemoryBuffer * /*output*/,
const rcti &area,
Span<MemoryBuffer *> inputs)
{
if (!output_buffer_) {
return;
}
MemoryBuffer output_buf(output_buffer_,
COM_data_type_num_channels(datatype_),
this->get_width(),
this->get_height());
const MemoryBuffer *input_image = inputs[0];
output_buf.copy_from(input_image, area);
}
/******************************* MultiLayer *******************************/
OutputOpenExrLayer::OutputOpenExrLayer(const char *name_, DataType datatype_, bool use_layer_)
{
STRNCPY(this->name, name_);
this->datatype = datatype_;
this->use_layer = use_layer_;
/* these are created in init_execution */
this->output_buffer = nullptr;
this->image_input = nullptr;
}
OutputOpenExrMultiLayerOperation::OutputOpenExrMultiLayerOperation(const Scene *scene,
const RenderData *rd,
const bNodeTree *tree,
const char *path,
char exr_codec,
bool exr_half_float,
const char *view_name)
{
scene_ = scene;
rd_ = rd;
tree_ = tree;
STRNCPY(path_, path);
exr_codec_ = exr_codec;
exr_half_float_ = exr_half_float;
view_name_ = view_name;
this->set_canvas_input_index(RESOLUTION_INPUT_ANY);
}
void OutputOpenExrMultiLayerOperation::add_layer(const char *name,
DataType datatype,
bool use_layer)
{
this->add_input_socket(datatype);
layers_.append(OutputOpenExrLayer(name, datatype, use_layer));
}
StampData *OutputOpenExrMultiLayerOperation::create_stamp_data() const
{
/* StampData API doesn't provide functions to modify an instance without having a RenderResult.
*/
RenderResult render_result;
StampData *stamp_data = BKE_stamp_info_from_scene_static(scene_);
render_result.stamp_data = stamp_data;
for (const OutputOpenExrLayer &layer : layers_) {
/* Skip unconnected sockets. */
if (layer.image_input == nullptr) {
continue;
}
std::unique_ptr<MetaData> meta_data = layer.image_input->get_meta_data();
if (meta_data) {
blender::StringRef layer_name =
blender::bke::cryptomatte::BKE_cryptomatte_extract_layer_name(
blender::StringRef(layer.name, BLI_strnlen(layer.name, sizeof(layer.name))));
meta_data->replace_hash_neutral_cryptomatte_keys(layer_name);
meta_data->add_to_render_result(&render_result);
}
}
return stamp_data;
}
void OutputOpenExrMultiLayerOperation::init_execution()
{
for (uint i = 0; i < layers_.size(); i++) {
if (layers_[i].use_layer) {
SocketReader *reader = get_input_socket_reader(i);
layers_[i].image_input = reader;
layers_[i].output_buffer = init_buffer(
this->get_width(), this->get_height(), layers_[i].datatype);
}
}
}
void OutputOpenExrMultiLayerOperation::execute_region(rcti *rect, uint /*tile_number*/)
{
for (uint i = 0; i < layers_.size(); i++) {
OutputOpenExrLayer &layer = layers_[i];
if (layer.image_input) {
write_buffer_rect(
rect, tree_, layer.image_input, layer.output_buffer, this->get_width(), layer.datatype);
}
}
}
void OutputOpenExrMultiLayerOperation::deinit_execution()
{
uint width = this->get_width();
uint height = this->get_height();
if (width != 0 && height != 0) {
char filepath[FILE_MAX];
const char *suffix;
void *exrhandle = IMB_exr_get_handle();
suffix = BKE_scene_multiview_view_suffix_get(rd_, view_name_);
BKE_image_path_from_imtype(filepath,
path_,
BKE_main_blendfile_path_from_global(),
rd_->cfra,
R_IMF_IMTYPE_MULTILAYER,
(rd_->scemode & R_EXTENSION) != 0,
true,
suffix);
BLI_file_ensure_parent_dir_exists(filepath);
for (uint i = 0; i < layers_.size(); i++) {
OutputOpenExrLayer &layer = layers_[i];
if (!layer.image_input) {
continue; /* skip unconnected sockets */
}
add_exr_channels(exrhandle,
layers_[i].name,
layers_[i].datatype,
"",
width,
exr_half_float_,
layers_[i].output_buffer);
}
/* When the filepath has no permissions, this can fail. */
StampData *stamp_data = create_stamp_data();
if (IMB_exr_begin_write(exrhandle, filepath, width, height, exr_codec_, stamp_data)) {
IMB_exr_write_channels(exrhandle);
}
else {
/* TODO: get the error from openexr's exception. */
/* XXX: nice way to do report? */
printf("Error Writing Render Result, see console\n");
}
IMB_exr_close(exrhandle);
for (uint i = 0; i < layers_.size(); i++) {
if (layers_[i].output_buffer) {
MEM_freeN(layers_[i].output_buffer);
layers_[i].output_buffer = nullptr;
}
layers_[i].image_input = nullptr;
}
BKE_stamp_data_free(stamp_data);
}
}
void OutputOpenExrMultiLayerOperation::update_memory_buffer_partial(MemoryBuffer * /*output*/,
const rcti &area,
Span<MemoryBuffer *> inputs)
{
for (int i = 0; i < layers_.size(); i++) {
OutputOpenExrLayer &layer = layers_[i];
int layer_num_channels = COM_data_type_num_channels(layer.datatype);
if (layer.output_buffer) {
MemoryBuffer output_buf(
layer.output_buffer, layer_num_channels, this->get_width(), this->get_height());
/* Input node always has 4 channels. Not all are needed depending on datatype. */
output_buf.copy_from(inputs[i], area, 0, layer_num_channels, 0);
}
}
}
} // namespace blender::compositor

View File

@ -1,131 +0,0 @@
/* SPDX-FileCopyrightText: 2011 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_MultiThreadedOperation.h"
#include "BLI_path_util.h"
#include "BLI_rect.h"
#include "DNA_color_types.h"
#include "IMB_openexr.h"
namespace blender::compositor {
/* Writes the image to a single-layer file. */
class OutputSingleLayerOperation : public MultiThreadedOperation {
protected:
const RenderData *rd_;
const bNodeTree *tree_;
ImageFormatData format_;
char path_[FILE_MAX];
float *output_buffer_;
DataType datatype_;
SocketReader *image_input_;
const char *view_name_;
bool save_as_render_;
public:
OutputSingleLayerOperation(const Scene *scene,
const RenderData *rd,
const bNodeTree *tree,
DataType datatype,
const ImageFormatData *format,
const char *path,
const char *view_name,
bool save_as_render);
~OutputSingleLayerOperation();
void execute_region(rcti *rect, unsigned int tile_number) override;
bool is_output_operation(bool /*rendering*/) const override
{
return true;
}
void init_execution() override;
void deinit_execution() override;
eCompositorPriority get_render_priority() const override
{
return eCompositorPriority::Low;
}
void update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs) override;
};
/* extra info for OpenEXR layers */
struct OutputOpenExrLayer {
OutputOpenExrLayer(const char *name, DataType datatype, bool use_layer);
char name[EXR_TOT_MAXNAME - 2];
DataType datatype;
bool use_layer;
/* internals */
float *output_buffer;
SocketReader *image_input;
};
/* Writes inputs into OpenEXR multi-layer channels. */
class OutputOpenExrMultiLayerOperation : public MultiThreadedOperation {
protected:
const Scene *scene_;
const RenderData *rd_;
const bNodeTree *tree_;
char path_[FILE_MAX];
char exr_codec_;
bool exr_half_float_;
Vector<OutputOpenExrLayer> layers_;
const char *view_name_;
StampData *create_stamp_data() const;
public:
OutputOpenExrMultiLayerOperation(const Scene *scene,
const RenderData *rd,
const bNodeTree *tree,
const char *path,
char exr_codec,
bool exr_half_float,
const char *view_name);
void add_layer(const char *name, DataType datatype, bool use_layer);
void execute_region(rcti *rect, unsigned int tile_number) override;
bool is_output_operation(bool /*rendering*/) const override
{
return true;
}
void init_execution() override;
void deinit_execution() override;
eCompositorPriority get_render_priority() const override
{
return eCompositorPriority::Low;
}
void update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs) override;
};
void add_exr_channels(void *exrhandle,
const char *layer_name,
const DataType datatype,
const char *view_name,
size_t width,
bool use_half_float,
float *buf);
void free_exr_channels(void *exrhandle,
const RenderData *rd,
const char *layer_name,
const DataType datatype);
int get_datatype_size(DataType datatype);
} // namespace blender::compositor

View File

@ -32,6 +32,7 @@ set(SRC
intern/operation.cc
intern/realize_on_domain_operation.cc
intern/reduce_to_single_value_operation.cc
intern/render_context.cc
intern/result.cc
intern/scheduler.cc
intern/shader_node.cc
@ -52,6 +53,7 @@ set(SRC
COM_operation.hh
COM_realize_on_domain_operation.hh
COM_reduce_to_single_value_operation.hh
COM_render_context.hh
COM_result.hh
COM_scheduler.hh
COM_shader_node.hh

View File

@ -14,6 +14,7 @@
#include "GPU_shader.h"
#include "GPU_texture.h"
#include "COM_render_context.hh"
#include "COM_result.hh"
#include "COM_static_cache_manager.hh"
#include "COM_texture_pool.hh"
@ -104,6 +105,11 @@ class Context {
* that is, to ready it to track the next change. */
virtual IDRecalcFlag query_id_recalc_flag(ID *id) const = 0;
/* Get a pointer to the render context of this context. A render context stores information about
* the current render. It might be null if the compositor is not being evaluated as part of a
* render pipeline. */
virtual RenderContext *render_context() const;
/* Get the size of the compositing region. See get_compositing_region(). */
int2 get_compositing_region_size() const;

View File

@ -0,0 +1,119 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <memory>
#include <string>
#include "BLI_map.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_scene_types.h"
struct RenderResult;
namespace blender::realtime_compositor {
/* ------------------------------------------------------------------------------------------------
* File Output
*
* A FileOutput represents an image that will be saved to a file output. The image is internally
* stored as a RenderResult and saved at the path according to the image format. The image can
* either be saved as an EXR image or a non-EXR image, specified by the format. This is important
* because EXR images needs to constructed differently from other image types as will be explained
* in the following sections.
*
* For EXR images, the render result needs to be composed of passes for each layer, so the add_pass
* method should be called to add each of the passes. Additionally, an empty view should be added
* for each of the views referenced by the passes, using the single-argument overload of the
* add_view method. Those views are merely empty structure and does not hold any data aside from
* the view name. An exception to this rule is stereo EXR images, which needs to have the same
* structure as non-EXR images as explained in the following section.
*
* For non-EXR images, the render result needs to composed of views, so the multi-argument overload
* of the method add_view should be used to add each view.
*
* Color management will be applied on the images if save_as_render_ is true.
*
* Meta data can be added using the add_meta_data function. */
class FileOutput {
private:
std::string path_;
ImageFormatData format_;
RenderResult *render_result_;
bool save_as_render_;
Map<std::string, std::string> meta_data_;
public:
/* Allocate and initialize the internal render result of the file output using the give
* parameters. See the implementation for more information. */
FileOutput(std::string path, ImageFormatData format, int2 size, bool save_as_render);
/* Free the internal render result. */
~FileOutput();
/* Add an empty view with the given name. An empty view is just structure and does not hold any
* data aside from the view name. This should be called for each view referenced by passes. This
* should only be called for EXR images. */
void add_view(const char *view_name);
/* Add a view of the given name that stores the given pixel buffer composed of the given number
* of channels. */
void add_view(const char *view_name, int channels, float *buffer);
/* Add a pass of the given name in the given view that stores the given pixel buffer composed of
* each of the channels given by the channels string. The channels string should contain a
* character for each channel in the pixel buffer representing the channel ID. This should only
* be called for EXR images. The given view name should be the name of an added view using the
* add_view method. */
void add_pass(const char *pass_name, const char *view_name, const char *channels, float *buffer);
/* Add meta data that will eventually be saved to the file if the format supports it. */
void add_meta_data(std::string key, std::string value);
/* Save the file to the path along with its meta data, reporting any reports to the standard
* output. */
void save(Scene *scene);
};
/* ------------------------------------------------------------------------------------------------
* Render Context
*
* A render context is created by the render pipeline and passed to the compositor to stores data
* that is specifically related to the rendering process. In particular, since the compositor is
* executed for each view separately and consecutively, it can be used to store and accumulate
* data from each of the evaluations of each view, for instance, to save all views in a single file
* for the File Output node, see the file_outputs_ member for more information. */
class RenderContext {
private:
/* A mapping between file outputs and their image file paths. Those are constructed in the
* get_file_output method and saved in the save_file_outputs method. See those methods for more
* information. */
Map<std::string, std::unique_ptr<FileOutput>> file_outputs_;
public:
/* Check if there is an available file output with the given path in the context, if one exists,
* return it, otherwise, return a newly created one from the given parameters and add it to the
* context. The arguments are ignored if the file output already exist. This method is typically
* called in the File Output nodes in the compositor.
*
* Since the compositor gets executed multiple times for each view, for single view renders, the
* file output will be constructed and fully initialized in the same compositor evaluation. For
* multi-view renders, the file output will be constructed in the evaluation of the first view,
* and each view will subsequently add its data until the file output is fully initialized in the
* last view. The render pipeline code will then call the save_file_outputs method after all
* views were evaluated to write the file outputs. */
FileOutput &get_file_output(std::string path,
ImageFormatData format,
int2 size,
bool save_as_render);
/* Write the file outputs that were added to the context. The render pipeline code should call
* this method after all views were evaluated to write the file outputs. See the get_file_output
* method for more information. */
void save_file_outputs(Scene *scene);
};
} // namespace blender::realtime_compositor

View File

@ -9,6 +9,7 @@
#include "GPU_shader.h"
#include "COM_context.hh"
#include "COM_render_context.hh"
#include "COM_static_cache_manager.hh"
#include "COM_texture_pool.hh"
@ -16,6 +17,11 @@ namespace blender::realtime_compositor {
Context::Context(TexturePool &texture_pool) : texture_pool_(texture_pool) {}
RenderContext *Context::render_context() const
{
return nullptr;
}
int2 Context::get_compositing_region_size() const
{
const rcti compositing_region = get_compositing_region();

View File

@ -0,0 +1,151 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <memory>
#include <string>
#include "BLI_assert.h"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_string.h"
#include "BLI_utildefines.h"
#include "MEM_guardedalloc.h"
#include "IMB_imbuf.h"
#include "IMB_imbuf_types.h"
#include "DNA_scene_types.h"
#include "DNA_windowmanager_types.h"
#include "BKE_image.h"
#include "BKE_image_save.h"
#include "BKE_report.h"
#include "RE_pipeline.h"
#include "COM_render_context.hh"
namespace blender::realtime_compositor {
/* ------------------------------------------------------------------------------------------------
* File Output
*/
FileOutput::FileOutput(std::string path, ImageFormatData format, int2 size, bool save_as_render)
: path_(path), format_(format), save_as_render_(save_as_render)
{
render_result_ = MEM_cnew<RenderResult>("Temporary Render Result For File Output");
render_result_->rectx = size.x;
render_result_->recty = size.y;
/* File outputs are always single layer, as images are actually stored in passes on that single
* layer. Create a single unnamed layer to add the passes to. A single unnamed layer is treated
* by the EXR writer as a special case where the channel names take the form:
* <pass-name>.<view-name>.<channel-id>
* Otherwise, the layer name would have preceded in the pass name in yet another section. */
RenderLayer *render_layer = MEM_cnew<RenderLayer>("Render Layer For File Output.");
BLI_addtail(&render_result_->layers, render_layer);
render_layer->name[0] = '\0';
}
FileOutput::~FileOutput()
{
RE_FreeRenderResult(render_result_);
}
void FileOutput::add_view(const char *view_name)
{
/* Empty views can only be added for EXR images. */
BLI_assert(ELEM(format_.imtype, R_IMF_IMTYPE_OPENEXR, R_IMF_IMTYPE_MULTILAYER));
RenderView *render_view = MEM_cnew<RenderView>("Render View For File Output.");
BLI_addtail(&render_result_->views, render_view);
STRNCPY(render_view->name, view_name);
}
void FileOutput::add_view(const char *view_name, int channels, float *buffer)
{
RenderView *render_view = MEM_cnew<RenderView>("Render View For File Output.");
BLI_addtail(&render_result_->views, render_view);
STRNCPY(render_view->name, view_name);
render_view->ibuf = IMB_allocImBuf(
render_result_->rectx, render_result_->recty, channels * 8, 0);
render_view->ibuf->channels = channels;
IMB_assign_float_buffer(render_view->ibuf, buffer, IB_TAKE_OWNERSHIP);
}
void FileOutput::add_pass(const char *pass_name,
const char *view_name,
const char *channels,
float *buffer)
{
/* Passes can only be added for EXR images. */
BLI_assert(ELEM(format_.imtype, R_IMF_IMTYPE_OPENEXR, R_IMF_IMTYPE_MULTILAYER));
RenderLayer *render_layer = static_cast<RenderLayer *>(render_result_->layers.first);
RenderPass *render_pass = MEM_cnew<RenderPass>("Render Pass For File Output.");
BLI_addtail(&render_layer->passes, render_pass);
STRNCPY(render_pass->name, pass_name);
STRNCPY(render_pass->view, view_name);
STRNCPY(render_pass->chan_id, channels);
const int channels_count = BLI_strnlen(channels, 4);
render_pass->rectx = render_result_->rectx;
render_pass->recty = render_result_->recty;
render_pass->channels = channels_count;
render_pass->ibuf = IMB_allocImBuf(
render_result_->rectx, render_result_->recty, channels_count * 8, 0);
render_pass->ibuf->channels = channels_count;
IMB_assign_float_buffer(render_pass->ibuf, buffer, IB_TAKE_OWNERSHIP);
}
void FileOutput::add_meta_data(std::string key, std::string value)
{
meta_data_.add(key, value);
}
void FileOutput::save(Scene *scene)
{
ReportList reports;
BKE_reports_init(&reports, RPT_STORE);
/* Add scene stamp data as meta data as well as the custom meta data. */
BKE_render_result_stamp_info(scene, nullptr, render_result_, false);
for (const auto &field : meta_data_.items()) {
BKE_render_result_stamp_data(render_result_, field.key.c_str(), field.value.c_str());
}
BKE_image_render_write(
&reports, render_result_, scene, true, path_.c_str(), &format_, save_as_render_);
BKE_reports_free(&reports);
}
/* ------------------------------------------------------------------------------------------------
* Render Context
*/
FileOutput &RenderContext::get_file_output(std::string path,
ImageFormatData format,
int2 size,
bool save_as_render)
{
return *file_outputs_.lookup_or_add_cb(
path, [&]() { return std::make_unique<FileOutput>(path, format, size, save_as_render); });
}
void RenderContext::save_file_outputs(Scene *scene)
{
for (std::unique_ptr<FileOutput> &file_output : file_outputs_.values()) {
file_output->save(scene);
}
}
} // namespace blender::realtime_compositor

View File

@ -296,14 +296,15 @@ static void compo_startjob(void *cjv, wmJobWorkerStatus *worker_status)
BKE_callback_exec_id(cj->bmain, &scene->id, BKE_CB_EVT_COMPOSITE_PRE);
if ((cj->scene->r.scemode & R_MULTIVIEW) == 0) {
ntreeCompositExecTree(cj->re, cj->scene, ntree, &cj->scene->r, false, true, "");
ntreeCompositExecTree(cj->re, cj->scene, ntree, &cj->scene->r, false, true, "", nullptr);
}
else {
LISTBASE_FOREACH (SceneRenderView *, srv, &scene->r.views) {
if (BKE_scene_multiview_is_render_view_active(&scene->r, srv) == false) {
continue;
}
ntreeCompositExecTree(cj->re, cj->scene, ntree, &cj->scene->r, false, true, srv->name);
ntreeCompositExecTree(
cj->re, cj->scene, ntree, &cj->scene->r, false, true, srv->name, nullptr);
}
}

View File

@ -10,6 +10,10 @@
#include "BKE_node.h"
namespace blender::realtime_compositor {
class RenderContext;
}
struct bNodeTreeType;
struct CryptomatteSession;
struct Scene;
@ -36,7 +40,8 @@ void ntreeCompositExecTree(Render *render,
RenderData *rd,
bool rendering,
int do_previews,
const char *view_name);
const char *view_name,
blender::realtime_compositor::RenderContext *render_context);
/**
* Called from render pipeline, to tag render input and output.

View File

@ -59,6 +59,7 @@ set(SRC
nodes/node_composite_double_edge_mask.cc
nodes/node_composite_ellipsemask.cc
nodes/node_composite_exposure.cc
nodes/node_composite_file_output.cc
nodes/node_composite_filter.cc
nodes/node_composite_flip.cc
nodes/node_composite_gamma.cc
@ -85,7 +86,6 @@ set(SRC
nodes/node_composite_moviedistortion.cc
nodes/node_composite_normal.cc
nodes/node_composite_normalize.cc
nodes/node_composite_output_file.cc
nodes/node_composite_pixelate.cc
nodes/node_composite_planetrackdeform.cc
nodes/node_composite_posterize.cc

View File

@ -179,12 +179,13 @@ void ntreeCompositExecTree(Render *render,
RenderData *rd,
bool rendering,
int do_preview,
const char *view_name)
const char *view_name,
blender::realtime_compositor::RenderContext *render_context)
{
#ifdef WITH_COMPOSITOR_CPU
COM_execute(render, rd, scene, ntree, rendering, view_name);
COM_execute(render, rd, scene, ntree, rendering, view_name, render_context);
#else
UNUSED_VARS(render, scene, ntree, rd, rendering, view_name);
UNUSED_VARS(render, scene, ntree, rd, rendering, view_name, render_context);
#endif
UNUSED_VARS(do_preview);

View File

@ -8,13 +8,26 @@
#include <cstring>
#include "BLI_assert.h"
#include "BLI_fileops.h"
#include "BLI_index_range.hh"
#include "BLI_path_util.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BLI_string_utils.hh"
#include "BLI_task.hh"
#include "BLI_utildefines.h"
#include "MEM_guardedalloc.h"
#include "DNA_node_types.h"
#include "DNA_scene_types.h"
#include "BKE_context.hh"
#include "BKE_image.h"
#include "BKE_image_format.h"
#include "BKE_main.hh"
#include "BKE_scene.h"
#include "RNA_access.hh"
#include "RNA_prototypes.h"
@ -24,8 +37,14 @@
#include "WM_api.hh"
#include "IMB_colormanagement.h"
#include "IMB_imbuf.h"
#include "IMB_imbuf_types.h"
#include "IMB_openexr.h"
#include "GPU_state.h"
#include "GPU_texture.h"
#include "COM_node_operation.hh"
#include "node_composite_util.hh"
@ -187,7 +206,9 @@ void ntreeCompositOutputFileSetLayer(bNode *node, bNodeSocket *sock, const char
ntreeCompositOutputFileUniqueLayer(&node->inputs, sock, name, '_');
}
namespace blender::nodes::node_composite_output_file_cc {
namespace blender::nodes::node_composite_file_output_cc {
NODE_STORAGE_FUNCS(NodeImageMultiFile)
/* XXX uses initfunc_api callback, regular initfunc does not support context yet */
static void init_output_file(const bContext *C, PointerRNA *ptr)
@ -443,28 +464,291 @@ static void node_composit_buts_file_output_ex(uiLayout *layout, bContext *C, Poi
using namespace blender::realtime_compositor;
class OutputFileOperation : public NodeOperation {
class FileOutputOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
if (context().use_file_output()) {
context().set_info_message("Viewport compositor setup not fully supported");
if (is_multi_layer()) {
execute_multi_layer();
}
else {
execute_single_layer();
}
}
/* --------------------
* Single Layer Images.
*/
void execute_single_layer()
{
const int2 size = compute_domain().size;
for (const bNodeSocket *input : this->node()->input_sockets()) {
const Result &result = get_input(input->identifier);
/* We only write images, not single values. */
if (result.is_single_value()) {
continue;
}
char base_path[FILE_MAX];
const auto &socket = *static_cast<NodeImageMultiFileSocket *>(input->storage);
get_single_layer_image_base_path(socket.path, base_path);
/* The image saving code expects EXR images to have a different structure than standard
* images. In particular, in EXR images, the buffers need to be stored in passes that are, in
* turn, stored in a render layer. On the other hand, in non-EXR images, the buffers need to
* be stored in views. An exception to this is stereo images, which needs to have the same
* structure as non-EXR images. */
const auto &format = socket.use_node_format ? node_storage(bnode()).format : socket.format;
const bool is_exr = format.imtype == R_IMF_IMTYPE_OPENEXR;
const int views_count = BKE_scene_multiview_num_views_get(&context().get_render_data());
if (is_exr && !(format.views_format == R_IMF_VIEWS_STEREO_3D && views_count == 2)) {
execute_single_layer_multi_view_exr(result, format, base_path);
continue;
}
char image_path[FILE_MAX];
get_single_layer_image_path(base_path, format, image_path);
FileOutput &file_output = context().render_context()->get_file_output(
image_path, format, size, socket.save_as_render);
add_view_for_result(file_output, result, context().get_view_name().data());
}
}
/* -----------------------------------
* Single Layer Multi-View EXR Images.
*/
void execute_single_layer_multi_view_exr(const Result &result,
const ImageFormatData &format,
const char *base_path)
{
const bool has_views = format.views_format != R_IMF_VIEWS_INDIVIDUAL;
/* The EXR stores all views in the same file, so we supply an empty view to make sure the file
* name does not contain a view suffix. */
char image_path[FILE_MAX];
const char *path_view = has_views ? "" : context().get_view_name().data();
get_multi_layer_exr_image_path(base_path, path_view, image_path);
const int2 size = compute_domain().size;
FileOutput &file_output = context().render_context()->get_file_output(
image_path, format, size, false);
/* The EXR stores all views in the same file, so we add the actual render view. Otherwise, we
* add a default unnamed view. */
const char *view_name = has_views ? context().get_view_name().data() : "";
file_output.add_view(view_name);
add_pass_for_result(file_output, result, "", view_name);
}
/* -----------------------
* Multi-Layer EXR Images.
*/
void execute_multi_layer()
{
const bool store_views_in_single_file = is_multi_view_exr();
const char *view = context().get_view_name().data();
/* If we are saving all views in a single multi-layer file, we supply an empty view to make
* sure the file name does not contain a view suffix. */
char image_path[FILE_MAX];
const char *write_view = store_views_in_single_file ? "" : view;
get_multi_layer_exr_image_path(get_base_path(), write_view, image_path);
const int2 size = compute_domain().size;
const ImageFormatData format = node_storage(bnode()).format;
FileOutput &file_output = context().render_context()->get_file_output(
image_path, format, size, false);
/* If we are saving views in separate files, we needn't store the view in the channel names, so
* we add an unnamed view. */
const char *pass_view = store_views_in_single_file ? view : "";
file_output.add_view(pass_view);
for (const bNodeSocket *input : this->node()->input_sockets()) {
const Result &input_result = get_input(input->identifier);
/* We only write images, not single values. */
if (input_result.is_single_value()) {
continue;
}
const char *pass_name = (static_cast<NodeImageMultiFileSocket *>(input->storage))->layer;
add_pass_for_result(file_output, input_result, pass_name, pass_view);
}
}
/* Read the data stored in the GPU texture of the given result and add a pass of the given name,
* view, and read buffer. The pass channel identifiers follows the EXR conventions. */
void add_pass_for_result(FileOutput &file_output,
const Result &result,
const char *pass_name,
const char *view_name)
{
/* The image buffer in the file output will take ownership of this buffer and freeing it will
* be its responsibility. */
GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE);
float *buffer = static_cast<float *>(GPU_texture_read(result.texture(), GPU_DATA_FLOAT, 0));
const int2 size = result.domain().size;
switch (result.type()) {
case ResultType::Color:
file_output.add_pass(pass_name, view_name, "RGBA", buffer);
break;
case ResultType::Vector:
file_output.add_pass(pass_name, view_name, "XYZ", float4_to_float3_image(size, buffer));
break;
case ResultType::Float:
file_output.add_pass(pass_name, view_name, "V", buffer);
break;
default:
/* Other types are internal and needn't be handled by operations. */
BLI_assert_unreachable();
break;
}
}
/* Read the data stored in the GPU texture of the given result and add a view of the given name
* and read buffer. */
void add_view_for_result(FileOutput &file_output, const Result &result, const char *view_name)
{
/* The image buffer in the file output will take ownership of this buffer and freeing it will
* be its responsibility. */
GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE);
float *buffer = static_cast<float *>(GPU_texture_read(result.texture(), GPU_DATA_FLOAT, 0));
const int2 size = result.domain().size;
switch (result.type()) {
case ResultType::Color:
file_output.add_view(view_name, 4, buffer);
break;
case ResultType::Vector:
file_output.add_view(view_name, 3, float4_to_float3_image(size, buffer));
break;
case ResultType::Float:
file_output.add_view(view_name, 1, buffer);
break;
default:
/* Other types are internal and needn't be handled by operations. */
BLI_assert_unreachable();
break;
}
}
/* Given a float4 image, return a newly allocated float3 image that ignores the last channel. The
* input image is freed. */
float *float4_to_float3_image(int2 size, float *float4_image)
{
float *float3_image = static_cast<float *>(MEM_malloc_arrayN(
size_t(size.x) * size.y, sizeof(float[3]), "File Output Vector Buffer."));
threading::parallel_for(IndexRange(size.y), 1, [&](const IndexRange sub_y_range) {
for (const int64_t y : sub_y_range) {
for (const int64_t x : IndexRange(size.x)) {
for (int i = 0; i < 3; i++) {
const int pixel_index = y * size.x + x;
float3_image[pixel_index * 3 + i] = float4_image[pixel_index * 4 + i];
}
}
}
});
MEM_freeN(float4_image);
return float3_image;
}
/* Get the base path of the image to be saved, based on the base path of the node. The base name
* is an optional initial name of the image, which will later be concatenated with other
* information like the frame number, view, and extension. If the base name is empty, then the
* base path represents a directory, so a trailing slash is ensured. */
void get_single_layer_image_base_path(const char *base_name, char *base_path)
{
if (base_name[0]) {
BLI_path_join(base_path, FILE_MAX, get_base_path(), base_name);
}
else {
BLI_strncpy(base_path, get_base_path(), FILE_MAX);
BLI_path_slash_ensure(base_path, FILE_MAX);
}
}
/* Get the path of the image to be saved based on the given format. */
void get_single_layer_image_path(const char *base_path,
const ImageFormatData &format,
char *image_path)
{
BKE_image_path_from_imformat(image_path,
base_path,
BKE_main_blendfile_path_from_global(),
context().get_frame_number(),
&format,
use_file_extension(),
true,
nullptr);
}
/* Get the path of the EXR image to be saved. If the given view is not empty, its corresponding
* file suffix will be appended to the name. */
void get_multi_layer_exr_image_path(const char *base_path, const char *view, char *image_path)
{
const char *suffix = BKE_scene_multiview_view_suffix_get(&context().get_render_data(), view);
BKE_image_path_from_imtype(image_path,
base_path,
BKE_main_blendfile_path_from_global(),
context().get_frame_number(),
R_IMF_IMTYPE_MULTILAYER,
use_file_extension(),
true,
suffix);
}
bool is_multi_layer()
{
return node_storage(bnode()).format.imtype == R_IMF_IMTYPE_MULTILAYER;
}
const char *get_base_path()
{
return node_storage(bnode()).base_path;
}
/* Add the file format extensions to the rendered file name. */
bool use_file_extension()
{
return context().get_render_data().scemode & R_EXTENSION;
}
/* If true, save views in a multi-view EXR file, otherwise, save each view in its own file. */
bool is_multi_view_exr()
{
if (!is_multi_view_scene()) {
return false;
}
return node_storage(bnode()).format.views_format == R_IMF_VIEWS_MULTIVIEW;
}
bool is_multi_view_scene()
{
return context().get_render_data().scemode & R_MULTIVIEW;
}
};
static NodeOperation *get_compositor_operation(Context &context, DNode node)
{
return new OutputFileOperation(context, node);
return new FileOutputOperation(context, node);
}
} // namespace blender::nodes::node_composite_output_file_cc
} // namespace blender::nodes::node_composite_file_output_cc
void register_node_type_cmp_output_file()
{
namespace file_ns = blender::nodes::node_composite_output_file_cc;
namespace file_ns = blender::nodes::node_composite_file_output_cc;
static bNodeType ntype;
@ -477,8 +761,6 @@ void register_node_type_cmp_output_file()
&ntype, "NodeImageMultiFile", file_ns::free_output_file, file_ns::copy_output_file);
ntype.updatefunc = file_ns::update_output_file;
ntype.get_compositor_operation = file_ns::get_compositor_operation;
ntype.realtime_compositor_unsupported_message = N_(
"Node not supported in the Viewport compositor");
nodeRegisterType(&ntype);
}

View File

@ -6,6 +6,10 @@
#include <memory>
namespace blender::realtime_compositor {
class RenderContext;
}
struct bNodeTree;
struct Depsgraph;
struct Render;
@ -29,7 +33,8 @@ void RE_compositor_execute(Render &render,
const RenderData &render_data,
const bNodeTree &node_tree,
const bool use_file_output,
const char *view_name);
const char *view_name,
blender::realtime_compositor::RenderContext *render_context);
/* Free compositor caches. */
void RE_compositor_free(Render &render);

View File

@ -27,6 +27,7 @@
#include "COM_context.hh"
#include "COM_evaluator.hh"
#include "COM_render_context.hh"
#include "RE_compositor.hh"
#include "RE_pipeline.h"
@ -122,17 +123,20 @@ class ContextInputData {
const bNodeTree *node_tree;
bool use_file_output;
std::string view_name;
realtime_compositor::RenderContext *render_context;
ContextInputData(const Scene &scene,
const RenderData &render_data,
const bNodeTree &node_tree,
const bool use_file_output,
const char *view_name)
const char *view_name,
realtime_compositor::RenderContext *render_context)
: scene(&scene),
render_data(&render_data),
node_tree(&node_tree),
use_file_output(use_file_output),
view_name(view_name)
view_name(view_name),
render_context(render_context)
{
}
};
@ -433,6 +437,11 @@ class Context : public realtime_compositor::Context {
input_data_.node_tree->runtime->update_draw(input_data_.node_tree->runtime->udh);
}
}
realtime_compositor::RenderContext *render_context() const override
{
return input_data_.render_context;
}
};
/* Render Realtime Compositor */
@ -507,12 +516,13 @@ void Render::compositor_execute(const Scene &scene,
const RenderData &render_data,
const bNodeTree &node_tree,
const bool use_file_output,
const char *view_name)
const char *view_name,
blender::realtime_compositor::RenderContext *render_context)
{
std::unique_lock lock(gpu_compositor_mutex);
blender::render::ContextInputData input_data(
scene, render_data, node_tree, use_file_output, view_name);
scene, render_data, node_tree, use_file_output, view_name, render_context);
if (gpu_compositor == nullptr) {
gpu_compositor = new blender::render::RealtimeCompositor(*this, input_data);
@ -536,9 +546,11 @@ void RE_compositor_execute(Render &render,
const RenderData &render_data,
const bNodeTree &node_tree,
const bool use_file_output,
const char *view_name)
const char *view_name,
blender::realtime_compositor::RenderContext *render_context)
{
render.compositor_execute(scene, render_data, node_tree, use_file_output, view_name);
render.compositor_execute(
scene, render_data, node_tree, use_file_output, view_name, render_context);
}
void RE_compositor_free(Render &render)

View File

@ -66,6 +66,8 @@
#include "NOD_composite.hh"
#include "COM_render_context.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "DEG_depsgraph_debug.hh"
@ -1267,10 +1269,18 @@ static void do_render_compositor(Render *re)
/* If we have consistent depsgraph now would be a time to update them. */
}
blender::realtime_compositor::RenderContext compositor_render_context;
LISTBASE_FOREACH (RenderView *, rv, &re->result->views) {
ntreeCompositExecTree(
re, re->pipeline_scene_eval, ntree, &re->r, true, G.background == 0, rv->name);
ntreeCompositExecTree(re,
re->pipeline_scene_eval,
ntree,
&re->r,
true,
G.background == 0,
rv->name,
&compositor_render_context);
}
compositor_render_context.save_file_outputs(re->pipeline_scene_eval);
ntree->runtime->stats_draw = nullptr;
ntree->runtime->test_break = nullptr;

View File

@ -1055,6 +1055,7 @@ ImBuf *RE_render_result_rect_to_ibuf(RenderResult *rr,
if (rv->ibuf) {
IMB_assign_byte_buffer(ibuf, rv->ibuf->byte_buffer.data, IB_DO_NOT_TAKE_OWNERSHIP);
IMB_assign_float_buffer(ibuf, rv->ibuf->float_buffer.data, IB_DO_NOT_TAKE_OWNERSHIP);
ibuf->channels = rv->ibuf->channels;
}
/* float factor for random dither, imbuf takes care of it */

View File

@ -23,6 +23,10 @@
#include "tile_highlight.h"
namespace blender::realtime_compositor {
class RenderContext;
}
struct bNodeTree;
struct Depsgraph;
struct GSet;
@ -46,7 +50,8 @@ struct BaseRender {
const RenderData &render_data,
const bNodeTree &node_tree,
const bool use_file_output,
const char *view_name) = 0;
const char *view_name,
blender::realtime_compositor::RenderContext *render_context) = 0;
virtual void compositor_free() = 0;
virtual void display_init(RenderResult *render_result) = 0;
@ -93,11 +98,13 @@ struct ViewRender : public BaseRender {
return nullptr;
}
void compositor_execute(const Scene & /*scene*/,
const RenderData & /*render_data*/,
const bNodeTree & /*node_tree*/,
const bool /*use_file_output*/,
const char * /*view_name*/) override
void compositor_execute(
const Scene & /*scene*/,
const RenderData & /*render_data*/,
const bNodeTree & /*node_tree*/,
const bool /*use_file_output*/,
const char * /*view_name*/,
blender::realtime_compositor::RenderContext * /*render_context*/) override
{
}
void compositor_free() override {}
@ -141,7 +148,8 @@ struct Render : public BaseRender {
const RenderData &render_data,
const bNodeTree &node_tree,
const bool use_file_output,
const char *view_name) override;
const char *view_name,
blender::realtime_compositor::RenderContext *render_context) override;
void compositor_free() override;
void display_init(RenderResult *render_result) override;