Image::get_color_at and Image::set_color_at: Support 16-bit float values (#17550)

# Objective

- Also support `f16` values when getting and setting colors.

## Solution

- Use the `half` crate to work with `f16` until it's in stable Rust.
This commit is contained in:
Sven Niederberger 2025-01-31 01:36:11 +01:00 committed by GitHub
parent ba1b0092e5
commit fcd1847a48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 40 additions and 6 deletions

View File

@ -79,6 +79,7 @@ ruzstd = { version = "0.7.0", optional = true }
# For transcoding of UASTC/ETC1S universal formats, and for .basis file support
basis-universal = { version = "0.3.0", optional = true }
tracing = { version = "0.1", default-features = false, features = ["std"] }
half = { version = "2.4.1" }
[dev-dependencies]
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }

View File

@ -1066,10 +1066,10 @@ impl Image {
/// Supports many of the common [`TextureFormat`]s:
/// - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear
/// - 16-bit and 32-bit unsigned integer
/// - 32-bit float
/// - 16-bit and 32-bit float
///
/// Be careful: as the data is converted to [`Color`] (which uses `f32` internally),
/// there may be issues with precision when using non-float [`TextureFormat`]s.
/// there may be issues with precision when using non-f32 [`TextureFormat`]s.
/// If you read a value you previously wrote using `set_color_at`, it will not match.
/// If you are working with a 32-bit integer [`TextureFormat`], the value will be
/// inaccurate (as `f32` does not have enough bits to represent it exactly).
@ -1080,7 +1080,6 @@ impl Image {
/// Other [`TextureFormat`]s are unsupported, such as:
/// - block-compressed formats
/// - non-byte-aligned formats like 10-bit
/// - 16-bit float formats
/// - signed integer formats
#[inline(always)]
pub fn get_color_at(&self, x: u32, y: u32) -> Result<Color, TextureAccessError> {
@ -1126,9 +1125,9 @@ impl Image {
/// Supports many of the common [`TextureFormat`]s:
/// - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear
/// - 16-bit and 32-bit unsigned integer (with possibly-limited precision, as [`Color`] uses `f32`)
/// - 32-bit float
/// - 16-bit and 32-bit float
///
/// Be careful: writing to non-float [`TextureFormat`]s is lossy! The data has to be converted,
/// Be careful: writing to non-f32 [`TextureFormat`]s is lossy! The data has to be converted,
/// so if you read it back using `get_color_at`, the `Color` you get will not equal the value
/// you used when writing it using this function.
///
@ -1137,7 +1136,6 @@ impl Image {
/// Other [`TextureFormat`]s are unsupported, such as:
/// - block-compressed formats
/// - non-byte-aligned formats like 10-bit
/// - 16-bit float formats
/// - signed integer formats
#[inline(always)]
pub fn set_color_at(&mut self, x: u32, y: u32, color: Color) -> Result<(), TextureAccessError> {
@ -1212,6 +1210,12 @@ impl Image {
f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
)),
TextureFormat::Rgba16Float => Ok(Color::linear_rgba(
half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32(),
half::f16::from_le_bytes([bytes[2], bytes[3]]).to_f32(),
half::f16::from_le_bytes([bytes[4], bytes[5]]).to_f32(),
half::f16::from_le_bytes([bytes[6], bytes[7]]).to_f32(),
)),
TextureFormat::Rgba16Unorm | TextureFormat::Rgba16Uint => {
let (r, g, b, a) = (
u16::from_le_bytes([bytes[0], bytes[1]]),
@ -1260,6 +1264,10 @@ impl Image {
let x = (x as f64 / u32::MAX as f64) as f32;
Ok(Color::linear_rgb(x, x, x))
}
TextureFormat::R16Float => {
let x = half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32();
Ok(Color::linear_rgb(x, x, x))
}
TextureFormat::R32Float => {
let x = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
Ok(Color::linear_rgb(x, x, x))
@ -1285,6 +1293,11 @@ impl Image {
let g = (g as f64 / u32::MAX as f64) as f32;
Ok(Color::linear_rgb(r, g, 0.0))
}
TextureFormat::Rg16Float => {
let r = half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32();
let g = half::f16::from_le_bytes([bytes[2], bytes[3]]).to_f32();
Ok(Color::linear_rgb(r, g, 0.0))
}
TextureFormat::Rg32Float => {
let r = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let g = f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
@ -1343,6 +1356,13 @@ impl Image {
bytes[2] = (r * u8::MAX as f32) as u8;
bytes[3] = (a * u8::MAX as f32) as u8;
}
TextureFormat::Rgba16Float => {
let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(r)));
bytes[2..4].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(g)));
bytes[4..6].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(b)));
bytes[6..8].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(a)));
}
TextureFormat::Rgba32Float => {
let [r, g, b, a] = LinearRgba::from(color).to_f32_array();
bytes[0..4].copy_from_slice(&f32::to_le_bytes(r));
@ -1400,6 +1420,14 @@ impl Image {
let r = (r as f64 * u32::MAX as f64) as u32;
bytes[0..4].copy_from_slice(&u32::to_le_bytes(r));
}
TextureFormat::R16Float => {
// Convert to grayscale with minimal loss if color is already gray
let linear = LinearRgba::from(color);
let luminance = Xyza::from(linear).y;
let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array();
let x = half::f16::from_f32(r);
bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(x));
}
TextureFormat::R32Float => {
// Convert to grayscale with minimal loss if color is already gray
let linear = LinearRgba::from(color);
@ -1427,6 +1455,11 @@ impl Image {
bytes[0..4].copy_from_slice(&u32::to_le_bytes(r));
bytes[4..8].copy_from_slice(&u32::to_le_bytes(g));
}
TextureFormat::Rg16Float => {
let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
bytes[0..2].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(r)));
bytes[2..4].copy_from_slice(&half::f16::to_le_bytes(half::f16::from_f32(g)));
}
TextureFormat::Rg32Float => {
let [r, g, _, _] = LinearRgba::from(color).to_f32_array();
bytes[0..4].copy_from_slice(&f32::to_le_bytes(r));