WIP: Video: 10/12 BPP and HDR video output support #120033

Draft
Aras Pranckevicius wants to merge 5 commits from aras_p/blender:ffmpeg_hdr into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
8 changed files with 222 additions and 29 deletions

View File

@ -460,6 +460,14 @@ class RENDER_PT_encoding_video(RenderOutputButtonsPanel, Panel):
if ffmpeg.codec == 'DNXHD':
layout.prop(ffmpeg, "use_lossless_output")
has_10_bit = ffmpeg.codec in {'H264', 'AV1'}
has_12_bit = ffmpeg.codec in {'AV1'}
has_hdr = ffmpeg.codec in {'H264', 'AV1'}
if has_10_bit or has_12_bit:
layout.prop(ffmpeg, "video_bpp")
if has_10_bit:
layout.prop(ffmpeg, "video_hdr")
# Output quality
use_crf = needs_codec and ffmpeg.codec in {
'H264',

View File

@ -907,4 +907,18 @@ void BKE_image_format_init_for_write(ImageFormatData *imf,
STRNCPY(imf->linear_colorspace_settings.name,
IMB_colormanagement_role_colorspace_name_get(COLOR_ROLE_SCENE_LINEAR));
}
if (imf_src == nullptr && scene_src->r.im_format.imtype == R_IMF_IMTYPE_FFMPEG) {
if (scene_src->r.ffcodecdata.video_bpp == FFM_VIDEO_BPP_10) {
imf->depth = R_IMF_CHAN_DEPTH_10;
}
if (scene_src->r.ffcodecdata.video_bpp == FFM_VIDEO_BPP_12) {
imf->depth = R_IMF_CHAN_DEPTH_12;
}
if (scene_src->r.ffcodecdata.video_hdr == FFM_VIDEO_HDR_REC2020_HLG) {
/* @TODO: until we get proper OCIO config to do full HLG stuff, use this and to HLG math
* manually. */
STRNCPY(imf->linear_colorspace_settings.name, "Linear Rec.2020");
}
}
}

View File

@ -48,6 +48,7 @@ extern "C" {
# include <libavutil/channel_layout.h>
# include <libavutil/cpu.h>
# include <libavutil/imgutils.h>
# include <libavutil/mastering_display_metadata.h>
# include <libavutil/opt.h>
# include <libavutil/rational.h>
# include <libavutil/samplefmt.h>
@ -398,9 +399,12 @@ static bool write_video_frame(FFMpegContext *context, AVFrame *frame, ReportList
/* read and encode a frame of video from the buffer */
static AVFrame *generate_video_frame(FFMpegContext *context, const ImBuf *image)
{
/* For now only 8-bit/channel images are supported. */
const uint8_t *pixels = image->byte_buffer.data;
if (pixels == nullptr) {
const float *pixels_fl = image->float_buffer.data;
/* Use float input if needed. */
const bool use_float = context->img_convert_frame != nullptr &&
context->img_convert_frame->format != AV_PIX_FMT_RGBA;
if ((!use_float && (pixels == nullptr)) || (use_float && (pixels_fl == nullptr))) {
return nullptr;
}
@ -417,31 +421,56 @@ static AVFrame *generate_video_frame(FFMpegContext *context, const ImBuf *image)
rgb_frame = context->current_frame;
}
/* Copy the Blender pixels into the FFMPEG data-structure, taking care of endianness and flipping
* the image vertically. */
int linesize = rgb_frame->linesize[0];
int linesize_src = rgb_frame->width * 4;
for (int y = 0; y < height; y++) {
uint8_t *target = rgb_frame->data[0] + linesize * (height - y - 1);
const uint8_t *src = pixels + linesize_src * y;
const size_t linesize_dst = rgb_frame->linesize[0];
if (use_float) {
/* Float image: need to split up the image into a planar format,
* because libswscale does not support RGBA->YUV conversions from
* packed float formats. */
BLI_assert_msg(rgb_frame->linesize[1] == linesize_dst && rgb_frame->linesize[2] == linesize_dst &&
rgb_frame->linesize[3] == linesize_dst,
"ffmpeg frame should be 4 same size planes for a floating point image case");
for (int y = 0; y < height; y++) {
size_t dst_offset = linesize_dst * (height - y - 1);
float *dst_g = reinterpret_cast<float *>(rgb_frame->data[0] + dst_offset);
float *dst_b = reinterpret_cast<float *>(rgb_frame->data[1] + dst_offset);
float *dst_r = reinterpret_cast<float *>(rgb_frame->data[2] + dst_offset);
float *dst_a = reinterpret_cast<float *>(rgb_frame->data[3] + dst_offset);
const float *src = pixels_fl + image->x * y * 4;
for (int x = 0; x < image->x; x++) {
*dst_g++ = src[1];
*dst_b++ = src[2];
*dst_r++ = src[0];
*dst_a++ = src[3];
src += 4;
}
}
}
else {
/* Byte image: flip the image vertically, possibly with endian
* conversion. */
const size_t linesize_src = rgb_frame->width * 4;
for (int y = 0; y < height; y++) {
uint8_t *target = rgb_frame->data[0] + linesize_dst * (height - y - 1);
const uint8_t *src = pixels + linesize_src * y;
# if ENDIAN_ORDER == L_ENDIAN
memcpy(target, src, linesize_src);
memcpy(target, src, linesize_src);
# elif ENDIAN_ORDER == B_ENDIAN
const uint8_t *end = src + linesize_src;
while (src != end) {
target[3] = src[0];
target[2] = src[1];
target[1] = src[2];
target[0] = src[3];
const uint8_t *end = src + linesize_src;
while (src != end) {
target[3] = src[0];
target[2] = src[1];
target[1] = src[2];
target[0] = src[3];
target += 4;
src += 4;
}
target += 4;
src += 4;
}
# else
# error ENDIAN_ORDER should either be L_ENDIAN or B_ENDIAN.
# endif
}
}
/* Convert to the output pixel format, if it's different that Blender's internal one. */
@ -1004,6 +1033,16 @@ static AVStream *alloc_video_stream(FFMpegContext *context,
c->pix_fmt = AV_PIX_FMT_YUV422P;
}
const bool bpp10 = rd->ffcodecdata.video_bpp == FFM_VIDEO_BPP_10;
const bool bpp12 = rd->ffcodecdata.video_bpp == FFM_VIDEO_BPP_12;
const bool hdrHLG = rd->ffcodecdata.video_hdr == FFM_VIDEO_HDR_REC2020_HLG;
if (bpp10) {
c->pix_fmt = AV_PIX_FMT_YUV420P10LE;
}
if (bpp12) {
c->pix_fmt = AV_PIX_FMT_YUV420P12LE;
}
if (context->ffmpeg_type == FFMPEG_XVID) {
/* Alas! */
c->pix_fmt = AV_PIX_FMT_YUV420P;
@ -1043,6 +1082,12 @@ static AVStream *alloc_video_stream(FFMpegContext *context,
else if (ELEM(codec_id, AV_CODEC_ID_H264, AV_CODEC_ID_VP9) && (context->ffmpeg_crf == 0)) {
/* Use 4:4:4 instead of 4:2:0 pixel format for lossless rendering. */
c->pix_fmt = AV_PIX_FMT_YUV444P;
if (bpp10) {
c->pix_fmt = AV_PIX_FMT_YUV444P10LE;
}
if (bpp12) {
c->pix_fmt = AV_PIX_FMT_YUV444P12LE;
}
}
if (codec_id == AV_CODEC_ID_PNG) {
@ -1056,6 +1101,13 @@ static AVStream *alloc_video_stream(FFMpegContext *context,
c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
if (hdrHLG) {
c->color_range = AVCOL_RANGE_JPEG; //@TODO: this or MPEG? or configurable?
c->color_primaries = AVCOL_PRI_BT2020;
c->color_trc = AVCOL_TRC_ARIB_STD_B67;
c->colorspace = AVCOL_SPC_BT2020_NCL;
}
/* xasp & yasp got float lately... */
st->sample_aspect_ratio = c->sample_aspect_ratio = av_d2q((double(rd->xasp) / double(rd->yasp)),
@ -1100,13 +1152,61 @@ static AVStream *alloc_video_stream(FFMpegContext *context,
}
else {
/* Output pixel format is different, allocate frame for conversion. */
context->img_convert_frame = alloc_picture(AV_PIX_FMT_RGBA, c->width, c->height);
AVPixelFormat src_format = bpp10 || bpp12 ? AV_PIX_FMT_GBRAPF32LE : AV_PIX_FMT_RGBA;
context->img_convert_frame = alloc_picture(src_format, c->width, c->height);
context->img_convert_ctx = BKE_ffmpeg_sws_get_context(
c->width, c->height, AV_PIX_FMT_RGBA, c->pix_fmt, SWS_BICUBIC);
c->width, c->height, src_format, c->pix_fmt, SWS_BICUBIC);
}
avcodec_parameters_from_context(st->codecpar, c);
/* Add side data indicating light levels and things. @TODO: not quite sure if this is really
* needed. */
if (hdrHLG) {
constexpr int hdr_peak_level = 1000;
size_t light_meta_size;
AVContentLightMetadata *light_meta = av_content_light_metadata_alloc(&light_meta_size);
light_meta->MaxCLL = hdr_peak_level;
light_meta->MaxFALL = hdr_peak_level;
# if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102)
av_stream_add_side_data(
st, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, (uint8_t *)light_meta, light_meta_size);
# else
av_packet_side_data_add(&st->codecpar->coded_side_data,
&st->codecpar->nb_coded_side_data,
AV_PKT_DATA_CONTENT_LIGHT_LEVEL,
light_meta,
light_meta_size,
0);
# endif
AVMasteringDisplayMetadata *mastering = av_mastering_display_metadata_alloc();
mastering->display_primaries[0][0] = av_make_q(17, 25);
mastering->display_primaries[0][1] = av_make_q(8, 25);
mastering->display_primaries[1][0] = av_make_q(53, 200);
mastering->display_primaries[1][1] = av_make_q(69, 100);
mastering->display_primaries[2][0] = av_make_q(3, 20);
mastering->display_primaries[2][1] = av_make_q(3, 50);
mastering->white_point[0] = av_make_q(3127, 10000);
mastering->white_point[1] = av_make_q(329, 1000);
mastering->min_luminance = av_make_q(0, 1);
mastering->max_luminance = av_make_q(hdr_peak_level, 1);
mastering->has_primaries = 1;
mastering->has_luminance = 1;
# if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102)
av_stream_add_side_data(
st, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, (uint8_t *)mastering, sizeof(*mastering));
# else
av_packet_side_data_add(&st->codecpar->coded_side_data,
&st->codecpar->nb_coded_side_data,
AV_PKT_DATA_MASTERING_DISPLAY_METADATA,
mastering,
sizeof(*mastering),
0);
# endif
}
context->video_time = 0.0f;
return st;
@ -1839,6 +1939,7 @@ void BKE_ffmpeg_end(void *context_v)
void BKE_ffmpeg_preset_set(RenderData *rd, int preset)
{
bool is_ntsc = (rd->frs_sec != 25);
rd->ffcodecdata.video_bpp = FFM_VIDEO_BPP_8;
switch (preset) {
case FFMPEG_PRESET_H264:

View File

@ -248,7 +248,8 @@ void IMB_colormanagement_imbuf_make_display_space(
ImBuf *IMB_colormanagement_imbuf_for_write(ImBuf *ibuf,
bool save_as_render,
bool allocate_result,
const ImageFormatData *image_format);
const ImageFormatData *image_format,
bool force_linear = false);
/** \} */

View File

@ -2581,7 +2581,8 @@ static ImBuf *imbuf_ensure_editable(ImBuf *ibuf, ImBuf *colormanaged_ibuf, bool
ImBuf *IMB_colormanagement_imbuf_for_write(ImBuf *ibuf,
bool save_as_render,
bool allocate_result,
const ImageFormatData *image_format)
const ImageFormatData *image_format,
bool force_linear)
{
ImBuf *colormanaged_ibuf = ibuf;
@ -2594,7 +2595,8 @@ ImBuf *IMB_colormanagement_imbuf_for_write(ImBuf *ibuf,
}
/* Detect if we are writing to a file format that needs a linear float buffer. */
const bool linear_float_output = BKE_imtype_requires_linear_float(image_format->imtype);
bool linear_float_output = force_linear ||
BKE_imtype_requires_linear_float(image_format->imtype);
/* Detect if we are writing output a byte buffer, which we would need to create
* with color management conversions applied. This may be for either applying the

View File

@ -112,6 +112,17 @@ typedef enum eFFMpegAudioChannels {
FFM_CHANNELS_SURROUND71 = 8,
} eFFMpegAudioChannels;
typedef enum eFFMpegVideoBpp {
FFM_VIDEO_BPP_8 = 0,
FFM_VIDEO_BPP_10 = 1,
FFM_VIDEO_BPP_12 = 2,
} eFFMpegVideoBpp;
typedef enum eFFMpegVideoHdr {
FFM_VIDEO_HDR_NONE = 0,
FFM_VIDEO_HDR_REC2020_HLG = 1,
} eFFMpegVideoHdr;
typedef struct FFMpegCodecData {
int type;
int codec;
@ -134,7 +145,8 @@ typedef struct FFMpegCodecData {
int rc_buffer_size;
int mux_packet_size;
int mux_rate;
void *_pad1;
int video_bpp; /* eFFMpegVideoBpp */
int video_hdr; /* eFFMpegVideoHdr */
} FFMpegCodecData;
/** \} */
@ -654,8 +666,6 @@ typedef enum eBakePassFilter {
typedef struct RenderData {
struct ImageFormatData im_format;
void *_pad;
struct FFMpegCodecData ffcodecdata;
/** Frames as in 'images'. */

View File

@ -6384,6 +6384,22 @@ static void rna_def_scene_ffmpeg_settings(BlenderRNA *brna)
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem ffmpeg_bpp_items[] = {
{FFM_VIDEO_BPP_8, "8", 0, "8", "8-bit color channels"},
{FFM_VIDEO_BPP_10, "10", 0, "10", "10-bit color channels"},
{FFM_VIDEO_BPP_12, "12", 0, "12", "12-bit color channels"},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem ffmpeg_hdr_items[] = {
{FFM_VIDEO_HDR_NONE, "NONE", 0, "SDR", "Standard Dynamic Range (no HDR)"},
{FFM_VIDEO_HDR_REC2020_HLG,
"HLG",
0,
"Rec.2020 HLG",
"Rec.2020 color space with Hybrid-Log Gamma HDR encoding"},
{0, nullptr, 0, nullptr, nullptr},
};
static const EnumPropertyItem ffmpeg_audio_codec_items[] = {
{AV_CODEC_ID_NONE, "NONE", 0, "No Audio", "Disables audio output, for video-only renders"},
{AV_CODEC_ID_AAC, "AAC", 0, "AAC", ""},
@ -6442,6 +6458,22 @@ static void rna_def_scene_ffmpeg_settings(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Bitrate", "Video bitrate (kbit/s)");
RNA_def_property_update(prop, NC_SCENE | ND_RENDER_OPTIONS, nullptr);
prop = RNA_def_property(srna, "video_bpp", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, nullptr, "video_bpp");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_enum_items(prop, ffmpeg_bpp_items);
RNA_def_property_enum_default(prop, FFM_VIDEO_BPP_8);
RNA_def_property_ui_text(prop, "Bits/Pixel", "Video bits per pixel color channel");
RNA_def_property_update(prop, NC_SCENE | ND_RENDER_OPTIONS, nullptr);
prop = RNA_def_property(srna, "video_hdr", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, nullptr, "video_hdr");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_enum_items(prop, ffmpeg_hdr_items);
RNA_def_property_enum_default(prop, FFM_VIDEO_HDR_NONE);
RNA_def_property_ui_text(prop, "HDR", "High Dynamic Range options");
RNA_def_property_update(prop, NC_SCENE | ND_RENDER_OPTIONS, nullptr);
prop = RNA_def_property(srna, "minrate", PROP_INT, PROP_NONE);
RNA_def_property_int_sdna(prop, nullptr, "rc_min_rate");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);

View File

@ -2077,6 +2077,20 @@ void RE_RenderFreestyleExternal(Render *re)
/** \name Read/Write Render Result (Images & Movies)
* \{ */
/* @TODO: this should be done by OCIO, but until we get the correct configuration, do Linear
* Rec.2020 -> HLG transform manually. */
static float do_hlg(float v)
{
if (v <= 0.0f)
return 0.0f;
if (v <= 1.0f)
return 0.5f * sqrtf(v);
const float ca = 0.17883277f;
const float cb = 0.28466892f;
const float cc = 0.55991073f;
return ca * logf(v - cb) + cc;
}
bool RE_WriteRenderViewsMovie(ReportList *reports,
RenderResult *rr,
Scene *scene,
@ -2097,6 +2111,7 @@ bool RE_WriteRenderViewsMovie(ReportList *reports,
const bool is_mono = BLI_listbase_count_at_most(&rr->views, 2) < 2;
const float dither = scene->r.dither_intensity;
const bool is_hdr = scene->r.ffcodecdata.video_hdr == FFM_VIDEO_HDR_REC2020_HLG;
if (is_mono || (image_format.views_format == R_IMF_VIEWS_INDIVIDUAL)) {
int view_id;
@ -2104,7 +2119,17 @@ bool RE_WriteRenderViewsMovie(ReportList *reports,
const char *suffix = BKE_scene_multiview_view_id_suffix_get(&scene->r, view_id);
ImBuf *ibuf = RE_render_result_rect_to_ibuf(rr, &rd->im_format, dither, view_id);
IMB_colormanagement_imbuf_for_write(ibuf, true, false, &image_format);
IMB_colormanagement_imbuf_for_write(ibuf, true, false, &image_format, is_hdr);
/* @TODO: this should be done by OCIO, but until we get the correct configuration, do Linear
* Rec.2020 -> HLG transform manually. */
if (ibuf->float_buffer.data && is_hdr) {
for (int idx = 0; idx < ibuf->x * ibuf->y * 4; idx += 4) {
ibuf->float_buffer.data[idx + 0] = do_hlg(ibuf->float_buffer.data[idx + 0]);
ibuf->float_buffer.data[idx + 1] = do_hlg(ibuf->float_buffer.data[idx + 1]);
ibuf->float_buffer.data[idx + 2] = do_hlg(ibuf->float_buffer.data[idx + 2]);
}
}
if (!mh->append_movie(movie_ctx_arr[view_id],
rd,
@ -2133,7 +2158,7 @@ bool RE_WriteRenderViewsMovie(ReportList *reports,
int view_id = BLI_findstringindex(&rr->views, names[i], offsetof(RenderView, name));
ibuf_arr[i] = RE_render_result_rect_to_ibuf(rr, &rd->im_format, dither, view_id);
IMB_colormanagement_imbuf_for_write(ibuf_arr[i], true, false, &image_format);
IMB_colormanagement_imbuf_for_write(ibuf_arr[i], true, false, &image_format, is_hdr);
}
ibuf_arr[2] = IMB_stereo3d_ImBuf(&image_format, ibuf_arr[0], ibuf_arr[1]);