diff --git a/scripts/startup/bl_ui/properties_output.py b/scripts/startup/bl_ui/properties_output.py index 21891585f03..0975f466c33 100644 --- a/scripts/startup/bl_ui/properties_output.py +++ b/scripts/startup/bl_ui/properties_output.py @@ -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', diff --git a/source/blender/blenkernel/intern/image_format.cc b/source/blender/blenkernel/intern/image_format.cc index e65250e8fb2..db92af426cf 100644 --- a/source/blender/blenkernel/intern/image_format.cc +++ b/source/blender/blenkernel/intern/image_format.cc @@ -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"); + } + } } diff --git a/source/blender/blenkernel/intern/writeffmpeg.cc b/source/blender/blenkernel/intern/writeffmpeg.cc index c07a856e7b8..f3d56950e54 100644 --- a/source/blender/blenkernel/intern/writeffmpeg.cc +++ b/source/blender/blenkernel/intern/writeffmpeg.cc @@ -48,6 +48,7 @@ extern "C" { # include # include # include +# include # include # include # include @@ -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(rgb_frame->data[0] + dst_offset); + float *dst_b = reinterpret_cast(rgb_frame->data[1] + dst_offset); + float *dst_r = reinterpret_cast(rgb_frame->data[2] + dst_offset); + float *dst_a = reinterpret_cast(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: diff --git a/source/blender/imbuf/IMB_colormanagement.hh b/source/blender/imbuf/IMB_colormanagement.hh index db3b018f7fe..83976ac885c 100644 --- a/source/blender/imbuf/IMB_colormanagement.hh +++ b/source/blender/imbuf/IMB_colormanagement.hh @@ -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); /** \} */ diff --git a/source/blender/imbuf/intern/colormanagement.cc b/source/blender/imbuf/intern/colormanagement.cc index 9e0cae61824..9269c78e6b9 100644 --- a/source/blender/imbuf/intern/colormanagement.cc +++ b/source/blender/imbuf/intern/colormanagement.cc @@ -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 diff --git a/source/blender/makesdna/DNA_scene_types.h b/source/blender/makesdna/DNA_scene_types.h index 86906d3e642..60d522098a2 100644 --- a/source/blender/makesdna/DNA_scene_types.h +++ b/source/blender/makesdna/DNA_scene_types.h @@ -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'. */ diff --git a/source/blender/makesrna/intern/rna_scene.cc b/source/blender/makesrna/intern/rna_scene.cc index 182679f1c0c..afcc7e444c5 100644 --- a/source/blender/makesrna/intern/rna_scene.cc +++ b/source/blender/makesrna/intern/rna_scene.cc @@ -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); diff --git a/source/blender/render/intern/pipeline.cc b/source/blender/render/intern/pipeline.cc index c55407981d2..b153904d191 100644 --- a/source/blender/render/intern/pipeline.cc +++ b/source/blender/render/intern/pipeline.cc @@ -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]);