Skip to content
8 changes: 8 additions & 0 deletions core/io/image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2591,6 +2591,14 @@ Vector<uint8_t> Image::save_png_to_buffer() const {
return save_png_buffer_func(Ref<Image>((Image *)this));
}

Vector<uint8_t> Image::save_png_16bit_to_buffer() const {
if (save_png_16bit_buffer_func != nullptr) {
return save_png_16bit_buffer_func(Ref<Image>((Image *)this));
}
// Fall back to the 8-bit encoder if no 16-bit PNG backend is available.
return save_png_to_buffer();
}

Vector<uint8_t> Image::save_jpg_to_buffer(float p_quality) const {
if (save_jpg_buffer_func == nullptr) {
return Vector<uint8_t>();
Expand Down
5 changes: 5 additions & 0 deletions core/io/image.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class Image;
/// @{
typedef Error (*SavePNGFunc)(const String &p_path, const Ref<Image> &p_img);
typedef Vector<uint8_t> (*SavePNGBufferFunc)(const Ref<Image> &p_img);
typedef Vector<uint8_t> (*SavePNG16BitBufferFunc)(const Ref<Image> &p_img);

typedef Error (*SaveJPGFunc)(const String &p_path, const Ref<Image> &p_img, float p_quality);
typedef Vector<uint8_t> (*SaveJPGBufferFunc)(const Ref<Image> &p_img, float p_quality);
Expand Down Expand Up @@ -205,6 +206,7 @@ class Image : public Resource {
static inline SaveWebPFunc save_webp_func = nullptr;
static inline SaveDDSFunc save_dds_func = nullptr;
static inline SavePNGBufferFunc save_png_buffer_func = nullptr;
static inline SavePNG16BitBufferFunc save_png_16bit_buffer_func = nullptr;
static inline SaveEXRBufferFunc save_exr_buffer_func = nullptr;
static inline SaveJPGBufferFunc save_jpg_buffer_func = nullptr;
static inline SaveWebPBufferFunc save_webp_buffer_func = nullptr;
Expand Down Expand Up @@ -377,6 +379,9 @@ class Image : public Resource {
Error save_jpg(const String &p_path, float p_quality = 0.75) const;
Error save_dds(const String &p_path) const;
Vector<uint8_t> save_png_to_buffer() const;
// Encodes a 16-bit-per-channel sRGB PNG (deep color, avoids 8-bit banding). Falls back to
// the 8-bit encoder if no 16-bit PNG backend is available.
Vector<uint8_t> save_png_16bit_to_buffer() const;
Vector<uint8_t> save_jpg_to_buffer(float p_quality = 0.75) const;
Vector<uint8_t> save_exr_to_buffer(bool p_grayscale = false) const;
Vector<uint8_t> save_dds_to_buffer() const;
Expand Down
14 changes: 12 additions & 2 deletions doc/classes/ProjectSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,11 @@
If [code]true[/code], requests V-Sync to be disabled when writing a movie (similar to setting [member display/window/vsync/vsync_mode] to [b]Disabled[/b]). This can speed up video writing if the hardware is fast enough to render, encode and save the video at a framerate higher than the monitor's refresh rate.
[b]Note:[/b] [member editor/movie_writer/disable_vsync] has no effect if the operating system or graphics driver forces V-Sync with no way for applications to disable it.
</member>
<member name="editor/movie_writer/exr/bit_depth" type="int" setter="" getter="" default="0">
The channel storage used when writing an OpenEXR image sequence ([code].exr[/code] file extension): [code]0[/code] for 16-bit half-float channels (smaller files), [code]1[/code] for 32-bit full-float channels.
EXR output stores each frame in floating point, which avoids the color banding visible in 8-bit formats such as PNG on smooth gradients and shadows. For the added precision to be meaningful, enable [member rendering/viewport/hdr_2d]; otherwise the frame is captured at 8-bit precision and simply stored in a floating-point file.
[b]Note:[/b] Frames are the tonemapped, display-referred output stored at higher precision (deep color), not scene-referred high dynamic range. Requires the TinyEXR module to be enabled at build time.
</member>
<member name="editor/movie_writer/fps" type="int" setter="" getter="" default="60">
The number of frames per second to record in the video when writing a movie. Simulation speed will adjust to always match the specified framerate, which means the engine will appear to run slower at higher [member editor/movie_writer/fps] values. Certain FPS values will require you to adjust [member editor/movie_writer/mix_rate] to prevent audio from desynchronizing over time.
This can be specified manually on the command line using the [code]--fixed-fps &lt;fps&gt;[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url].
Expand All @@ -1131,10 +1136,11 @@
</member>
<member name="editor/movie_writer/movie_file" type="String" setter="" getter="" default="&quot;&quot;">
The output path for the movie. The file extension determines the [MovieWriter] that will be used.
Redot has 3 built-in [MovieWriter]s:
Redot has 4 built-in [MovieWriter]s:
- OGV container with Theora for video and Vorbis for audio ([code].ogv[/code] file extension). Lossy compression, medium file sizes, fast encoding. The lossy compression quality can be adjusted by changing [member ProjectSettings.editor/movie_writer/video_quality] and [member ProjectSettings.editor/movie_writer/ogv/audio_quality]. The resulting file can be viewed in Redot with [VideoStreamPlayer] and most video players, but not web browsers as they don't support Theora.
- AVI container with MJPEG for video and uncompressed audio ([code].avi[/code] file extension). Lossy compression, medium file sizes, fast encoding. The lossy compression quality can be adjusted by changing [member ProjectSettings.editor/movie_writer/video_quality]. The resulting file can be viewed in most video players, but it must be converted to another format for viewing on the web or by Redot with [VideoStreamPlayer]. MJPEG does not support transparency. AVI output is currently limited to a file of 4 GB in size at most.
- PNG image sequence for video and WAV for audio ([code].png[/code] file extension). Lossless compression, large file sizes, slow encoding. Designed to be encoded to a video file with another tool such as [url=https://ffmpeg.org/]FFmpeg[/url] after recording. Transparency is currently not supported, even if the root viewport is set to be transparent.
- PNG image sequence for video and WAV for audio ([code].png[/code] file extension). Lossless compression, large file sizes, slow encoding. Designed to be encoded to a video file with another tool such as [url=https://ffmpeg.org/]FFmpeg[/url] after recording. Can be written at 8-bit or, to avoid color banding, 16-bit per channel (see [member editor/movie_writer/png/bit_depth]). Transparency is currently not supported, even if the root viewport is set to be transparent.
- OpenEXR image sequence for video and WAV for audio ([code].exr[/code] file extension). Stores each frame in floating point (16-bit half or 32-bit full, see [member editor/movie_writer/exr/bit_depth]) to avoid the color banding of 8-bit formats on smooth gradients and shadows. Enable [member rendering/viewport/hdr_2d] for the added precision. Designed for compositing/grading; encode to a video file afterwards with another tool. Requires the TinyEXR module.
If you need to encode to a different format or pipe a stream through third-party software, you can extend this [MovieWriter] class to create your own movie writers.
When using PNG output, the frame number will be appended at the end of the file name. It starts from 0 and is padded with 8 digits to ensure correct sorting and easier processing. For example, if the output path is [code]/tmp/hello.png[/code], the first two frames will be [code]/tmp/hello00000000.png[/code] and [code]/tmp/hello00000001.png[/code]. The audio will be saved at [code]/tmp/hello.wav[/code].
</member>
Expand All @@ -1148,6 +1154,10 @@
<member name="editor/movie_writer/ogv/keyframe_interval" type="int" setter="" getter="" default="64">
Forces keyframes at the specified interval (in frame count). Higher values can improve compression up to a certain level at the expense of higher latency when seeking.
</member>
<member name="editor/movie_writer/png/bit_depth" type="int" setter="" getter="" default="8">
The bits per channel used when writing a PNG image sequence ([code].png[/code] file extension): [code]8[/code] for a standard 8-bit PNG, [code]16[/code] for a 16-bit sRGB PNG.
16-bit output is deep color and avoids the banding visible in 8-bit on smooth gradients and shadows, while remaining a normal PNG that opens in any image tool. For the added precision to be meaningful, enable [member rendering/viewport/hdr_2d]; otherwise the frame is captured at 8-bit precision and simply stored in a 16-bit file.
</member>
<member name="editor/movie_writer/speaker_mode" type="int" setter="" getter="" default="0">
The speaker mode to use in the recorded audio when writing a movie. See [enum AudioServer.SpeakerMode] for possible values.
</member>
Expand Down
81 changes: 81 additions & 0 deletions drivers/png/png_driver_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@
#include "png_driver_common.h"

#include "core/config/engine.h"
#include "core/math/math_funcs.h"
#include "core/templates/local_vector.h"

#include <png.h>
#include <string.h>

namespace PNGDriverCommon {

Expand Down Expand Up @@ -211,6 +214,84 @@ Error image_to_png(const Ref<Image> &p_image, Vector<uint8_t> &p_buffer) {
return OK;
}

Error image_to_png_16bit(const Ref<Image> &p_image, Vector<uint8_t> &p_buffer) {
ERR_FAIL_COND_V(p_image.is_null() || p_image->is_empty(), ERR_INVALID_PARAMETER);

// Float/HDR formats hold linear light and are sRGB-encoded below; 8-bit sources are already sRGB.
const Image::Format input_format = p_image->get_format();
const bool linear_input = input_format == Image::FORMAT_RGBH || input_format == Image::FORMAT_RGBAH ||
input_format == Image::FORMAT_RGBF || input_format == Image::FORMAT_RGBAF ||
input_format == Image::FORMAT_RGBE9995;
Ref<Image> source = p_image;
if (source->get_format() != Image::FORMAT_RGBAF) {
source = source->duplicate();
source->convert(Image::FORMAT_RGBAF);
}

const int width = source->get_width();
const int height = source->get_height();
const Vector<uint8_t> src_data = source->get_data();
const float *src = reinterpret_cast<const float *>(src_data.ptr());

// Convert to 16-bit sRGB integer samples (RGB is gamma-encoded, alpha stays linear).
LocalVector<uint16_t> samples;
samples.resize((uint32_t)width * (uint32_t)height * 4);
for (int64_t p = 0; p < (int64_t)width * height; p++) {
for (int c = 0; c < 4; c++) {
float v = src[p * 4 + c];
if (linear_input && c < 3) {
v = v <= 0.0031308f ? 12.92f * v : 1.055f * Math::pow(v, 1.0f / 2.4f) - 0.055f;
}
v = CLAMP(v, 0.0f, 1.0f);
samples[p * 4 + c] = (uint16_t)Math::round(v * 65535.0f);
}
}

png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
ERR_FAIL_NULL_V(png_ptr, FAILED);
png_infop info_ptr = png_create_info_struct(png_ptr);
if (info_ptr == nullptr) {
png_destroy_write_struct(&png_ptr, nullptr);
ERR_FAIL_V(FAILED);
}
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_write_struct(&png_ptr, &info_ptr);
ERR_FAIL_V_MSG(FAILED, "Failed to encode 16-bit PNG.");
}

png_set_write_fn(
png_ptr, &p_buffer,
[](png_structp p_png, png_bytep p_data, png_size_t p_length) {
Vector<uint8_t> *buffer = static_cast<Vector<uint8_t> *>(png_get_io_ptr(p_png));
const int64_t ofs = buffer->size();
if (buffer->resize(ofs + (int64_t)p_length) != OK) {
// Enter libpng's error path (setjmp) rather than writing past the allocation.
png_error(p_png, "Out of memory while encoding 16-bit PNG.");
}
memcpy(buffer->ptrw() + ofs, p_data, p_length);
},
nullptr);

png_set_IHDR(png_ptr, info_ptr, width, height, 16, PNG_COLOR_TYPE_RGB_ALPHA,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_set_sRGB_gAMA_and_cHRM(png_ptr, info_ptr, PNG_sRGB_INTENT_PERCEPTUAL);
png_write_info(png_ptr, info_ptr);
#ifndef BIG_ENDIAN_ENABLED
png_set_swap(png_ptr); // PNG stores 16-bit samples big-endian; swap on little-endian hosts.
#endif

LocalVector<png_bytep> rows;
rows.resize(height);
for (int y = 0; y < height; y++) {
rows[y] = reinterpret_cast<png_bytep>(&samples[(uint32_t)y * (uint32_t)width * 4]);
}
png_write_image(png_ptr, rows.ptr());
png_write_end(png_ptr, info_ptr);
png_destroy_write_struct(&png_ptr, &info_ptr);

return OK;
}

/// @name APNG functions
/// @{

Expand Down
5 changes: 5 additions & 0 deletions drivers/png/png_driver_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ Error png_to_image(const uint8_t *p_source, size_t p_size, bool p_force_linear,
/// Contents of p_buffer is unspecified if error returned.
Error image_to_png(const Ref<Image> &p_image, Vector<uint8_t> &p_buffer);

/// Append p_image, as a 16-bit-per-channel sRGB PNG, to p_buffer.
/// Float (linear) sources are sRGB-encoded; 8-bit sources are scaled up. Used for deep-color,
/// banding-free output. Contents of p_buffer is unspecified if error returned.
Error image_to_png_16bit(const Ref<Image> &p_image, Vector<uint8_t> &p_buffer);

/// Attempt to load apng from buffer (p_source, p_size) into p_frames
Error apng_to_image_frames(const uint8_t *p_source, size_t p_size, bool p_force_linear, uint32_t p_frame_limit, Ref<ImageFrames> p_frames);
} // namespace PNGDriverCommon
8 changes: 8 additions & 0 deletions drivers/png/resource_saver_png.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ Vector<uint8_t> ResourceSaverPNG::save_image_to_buffer(const Ref<Image> &p_img)
return buffer;
}

Vector<uint8_t> ResourceSaverPNG::save_image_16bit_to_buffer(const Ref<Image> &p_img) {
Vector<uint8_t> buffer;
Error err = PNGDriverCommon::image_to_png_16bit(p_img, buffer);
ERR_FAIL_COND_V_MSG(err, Vector<uint8_t>(), "Can't convert image to 16-bit PNG.");
return buffer;
}

bool ResourceSaverPNG::recognize(const Ref<Resource> &p_resource) const {
return (p_resource.is_valid() && p_resource->is_class("ImageTexture"));
}
Expand All @@ -93,4 +100,5 @@ void ResourceSaverPNG::get_recognized_extensions(const Ref<Resource> &p_resource
ResourceSaverPNG::ResourceSaverPNG() {
Image::save_png_func = &save_image;
Image::save_png_buffer_func = &save_image_to_buffer;
Image::save_png_16bit_buffer_func = &save_image_16bit_to_buffer;
}
1 change: 1 addition & 0 deletions drivers/png/resource_saver_png.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class ResourceSaverPNG : public ResourceFormatSaver {
public:
static Error save_image(const String &p_path, const Ref<Image> &p_img);
static Vector<uint8_t> save_image_to_buffer(const Ref<Image> &p_img);
static Vector<uint8_t> save_image_16bit_to_buffer(const Ref<Image> &p_img);

virtual Error save(const Ref<Resource> &p_resource, const String &p_path, uint32_t p_flags = 0) override;
virtual bool recognize(const Ref<Resource> &p_resource) const override;
Expand Down
Loading
Loading