The File Output node doesn't write invalid and single value layers, which can cause differences in EXR structures when rendering animations. To fix this, we write a dummy image that has the same dimensions as the other layers in the file, filled with the value of the single value input. Pull Request: https://projects.blender.org/blender/blender/pulls/128454
479 lines
17 KiB
C++
479 lines
17 KiB
C++
/* SPDX-FileCopyrightText: 2011 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
#include <memory>
|
|
|
|
#include "BLI_assert.h"
|
|
#include "BLI_fileops.h"
|
|
#include "BLI_index_range.hh"
|
|
#include "BLI_path_utils.hh"
|
|
#include "BLI_string.h"
|
|
#include "BLI_string_utils.hh"
|
|
#include "BLI_task.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.hh"
|
|
|
|
#include "RE_pipeline.h"
|
|
|
|
#include "COM_FileOutputOperation.h"
|
|
#include "COM_render_context.hh"
|
|
|
|
namespace blender::compositor {
|
|
|
|
FileOutputInput::FileOutputInput(NodeImageMultiFileSocket *data,
|
|
DataType data_type,
|
|
DataType original_data_type)
|
|
: data(data), data_type(data_type), original_data_type(original_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."));
|
|
}
|
|
|
|
FileOutputOperation::FileOutputOperation(const CompositorContext *context,
|
|
const NodeImageMultiFile *node_data,
|
|
Vector<FileOutputInput> inputs)
|
|
: context_(context), node_data_(node_data), file_output_inputs_(inputs)
|
|
{
|
|
/* Inputs for multi-layer files need to be the same size, while they can be different for
|
|
* individual file outputs. */
|
|
const ResizeMode resize_mode = this->is_multi_layer() ? ResizeMode::Center : ResizeMode::None;
|
|
|
|
for (const FileOutputInput &input : inputs) {
|
|
add_input_socket(input.data_type, resize_mode);
|
|
}
|
|
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(
|
|
input.image_input->get_width(), input.image_input->get_height(), input.data_type);
|
|
}
|
|
}
|
|
|
|
void FileOutputOperation::update_memory_buffer(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, inputs[i]->get_width(), inputs[i]->get_height());
|
|
output_buf.copy_from(inputs[i], inputs[i]->get_rect(), 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()
|
|
{
|
|
/* It is possible that none of the inputs would have an image connected, which will materialize
|
|
* as a size of zero, so check this here and return early doing nothing. Just make sure to free
|
|
* the allocated buffers. */
|
|
const int2 size = int2(get_width(), get_height());
|
|
if (size == int2(0)) {
|
|
for (const FileOutputInput &input : file_output_inputs_) {
|
|
/* Ownership of outputs buffers are transferred to file outputs, so if we are not writing a
|
|
* file output, we need to free the output buffer here. */
|
|
if (input.output_buffer) {
|
|
MEM_freeN(input.output_buffer);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (is_multi_layer()) {
|
|
execute_multi_layer();
|
|
}
|
|
else {
|
|
execute_single_layer();
|
|
}
|
|
}
|
|
|
|
/* --------------------
|
|
* Single Layer Images.
|
|
*/
|
|
|
|
void FileOutputOperation::execute_single_layer()
|
|
{
|
|
for (const FileOutputInput &input : file_output_inputs_) {
|
|
/* We only write images, not single values. */
|
|
if (!input.image_input || input.image_input->get_flags().is_constant_operation) {
|
|
/* Ownership of outputs buffers are transferred to file outputs, so if we are not writing a
|
|
* file output, we need to free the output buffer here. */
|
|
if (input.output_buffer) {
|
|
MEM_freeN(input.output_buffer);
|
|
}
|
|
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 save_as_render = input.data->use_node_format ? node_data_->save_as_render :
|
|
input.data->save_as_render;
|
|
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);
|
|
|
|
const int2 size = int2(input.image_input->get_width(), input.image_input->get_height());
|
|
realtime_compositor::FileOutput &file_output = context_->get_render_context()->get_file_output(
|
|
image_path, format, size, 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(input.image_input->get_width(), input.image_input->get_height());
|
|
realtime_compositor::FileOutput &file_output = context_->get_render_context()->get_file_output(
|
|
image_path, format, size, true);
|
|
|
|
/* 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, true);
|
|
|
|
/* 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_) {
|
|
if (!input.image_input) {
|
|
/* Ownership of outputs buffers are transferred to file outputs, so if we are not writing a
|
|
* file output, we need to free the output buffer here. */
|
|
if (input.output_buffer) {
|
|
MEM_freeN(input.output_buffer);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
/* Given a float4 image, return a newly allocated float3 image that ignores the last channel. The
|
|
* input image is freed. */
|
|
static 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;
|
|
}
|
|
|
|
/* Allocates and fills an image buffer of the specified size with the value of the given constant
|
|
* input. */
|
|
static float *inflate_input(const FileOutputInput &input, const int2 size)
|
|
{
|
|
BLI_assert(input.image_input->get_flags().is_constant_operation);
|
|
|
|
switch (input.data_type) {
|
|
case DataType::Value: {
|
|
float *buffer = static_cast<float *>(MEM_malloc_arrayN(
|
|
size_t(size.x) * size.y, sizeof(float), "File Output Inflated Buffer."));
|
|
|
|
const float value = input.image_input->get_constant_value_default(0.0f);
|
|
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)) {
|
|
buffer[y * size.x + x] = value;
|
|
}
|
|
}
|
|
});
|
|
return buffer;
|
|
}
|
|
case DataType::Color: {
|
|
float *buffer = static_cast<float *>(MEM_malloc_arrayN(
|
|
size_t(size.x) * size.y, sizeof(float[4]), "File Output Inflated Buffer."));
|
|
|
|
const float *value = input.image_input->get_constant_elem_default(nullptr);
|
|
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)) {
|
|
copy_v4_v4(buffer + ((y * size.x + x) * 4), value);
|
|
}
|
|
}
|
|
});
|
|
return buffer;
|
|
}
|
|
default:
|
|
/* Vector types are not possible for File output, see get_input_data_type in
|
|
* COM_FileOutputNode.cc for more information. Other types are internal and needn't be
|
|
* handled by operations. */
|
|
break;
|
|
}
|
|
|
|
BLI_assert_unreachable();
|
|
return nullptr;
|
|
}
|
|
|
|
void FileOutputOperation::add_pass_for_input(realtime_compositor::FileOutput &file_output,
|
|
const FileOutputInput &input,
|
|
const char *pass_name,
|
|
const char *view_name)
|
|
{
|
|
/* For constant operations, we fill a buffer that covers the canvas of the operation with the
|
|
* value of the operation. */
|
|
const int2 size = input.image_input->get_flags().is_constant_operation ?
|
|
int2(this->get_width(), this->get_height()) :
|
|
int2(input.image_input->get_width(), input.image_input->get_height());
|
|
|
|
/* The image buffer in the file output will take ownership of this buffer and freeing it will be
|
|
* its responsibility. So if we don't use the output buffer, we need to free it here. */
|
|
float *buffer = nullptr;
|
|
if (input.image_input->get_flags().is_constant_operation) {
|
|
buffer = inflate_input(input, size);
|
|
MEM_freeN(input.output_buffer);
|
|
}
|
|
else {
|
|
buffer = input.output_buffer;
|
|
}
|
|
|
|
switch (input.original_data_type) {
|
|
case DataType::Color:
|
|
/* Use lowercase rgba for Cryptomatte layers because the EXR internal compression rules
|
|
* specify that all uppercase RGBA channels will be compressed, and Cryptomatte should not be
|
|
* compressed. */
|
|
if (input.image_input->get_meta_data() &&
|
|
input.image_input->get_meta_data()->is_cryptomatte_layer())
|
|
{
|
|
file_output.add_pass(pass_name, view_name, "rgba", buffer);
|
|
}
|
|
else {
|
|
file_output.add_pass(pass_name, view_name, "RGBA", buffer);
|
|
}
|
|
break;
|
|
case DataType::Vector:
|
|
if (input.image_input->get_meta_data() && input.image_input->get_meta_data()->is_4d_vector) {
|
|
file_output.add_pass(pass_name, view_name, "XYZW", buffer);
|
|
}
|
|
else {
|
|
file_output.add_pass(pass_name, view_name, "XYZ", float4_to_float3_image(size, buffer));
|
|
}
|
|
break;
|
|
case DataType::Value:
|
|
file_output.add_pass(pass_name, view_name, "V", buffer);
|
|
break;
|
|
case DataType::Float2:
|
|
/* An internal type that needn't be handled. */
|
|
BLI_assert_unreachable();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FileOutputOperation::add_view_for_input(realtime_compositor::FileOutput &file_output,
|
|
const FileOutputInput &input,
|
|
const char *view_name)
|
|
{
|
|
const int2 size = int2(input.image_input->get_width(), input.image_input->get_height());
|
|
switch (input.original_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, float4_to_float3_image(size, input.output_buffer));
|
|
break;
|
|
case DataType::Value:
|
|
file_output.add_view(view_name, 1, input.output_buffer);
|
|
break;
|
|
case DataType::Float2:
|
|
/* An internal type that needn't be handled. */
|
|
BLI_assert_unreachable();
|
|
break;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool FileOutputOperation::use_file_extension()
|
|
{
|
|
return context_->get_render_data()->scemode & R_EXTENSION;
|
|
}
|
|
|
|
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
|