mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
149 lines
6.3 KiB
Rust
149 lines
6.3 KiB
Rust
use common::util::{linear_to_srgba, srgba_to_linear};
|
|
use common_base::span;
|
|
/// Pixel art scaling
|
|
/// Note: The current ui is locked to the pixel grid with little animation, if
|
|
/// we want smoothly moving pixel art this should be done in the shaders
|
|
/// useful links: https://gitlab.com/veloren/veloren/issues/257
|
|
use image::RgbaImage;
|
|
use vek::*;
|
|
|
|
const EPSILON: f32 = 0.0001;
|
|
|
|
// Averaging colors with alpha such that when blending with the background color
|
|
// the same color will be produced as when the individual colors were blended
|
|
// with the background and then averaged
|
|
// Say we have two areas that we are combining to form a single pixel
|
|
// A1 and A2 where these are the fraction of the area of the pixel each color
|
|
// contributes to Then if the colors were opaque we would say that the final
|
|
// color output color o3 is
|
|
// E1: o3 = A1 * o1 + A2 * o2
|
|
// where o1 and o2 are the opaque colors of the two areas
|
|
// now say the areas are actually translucent and these opaque colors are
|
|
// derived by blending with a common background color b
|
|
// E2: o1 = c1 * a1 + b * (1 - a1)
|
|
// E3: o2 = c2 * a2 + b * (1 - a2)
|
|
// we want to find the combined color (c3) and combined alpha (a3) such that
|
|
// E4: o3 = c3 * a3 + b * (1 - a3)
|
|
// substitution of E2 and E3 into E1 gives
|
|
// E5: o3 = A1 * (c1 * a1 + b * (1 - a1)) + A2 * (c2 * a2 + b * (1 - a2))
|
|
// combining E4 and E5 then separating like terms into separate equations gives
|
|
// E6: c3 * a3 = A1 * c1 * a1 + A2 * c2 * a2
|
|
// E7: b * (1 - a3) = A1 * b * (1 - a1) + A2 * b * (1 - a2)
|
|
// dropping b from E7 and solving for a3
|
|
// E8: a3 = 1 - A1 * (1 - a1) + A2 * (1 - a2)
|
|
// we can now calculate the combined alpha value
|
|
// and E6 can then be solved for c3
|
|
// E9: c3 = (A1 * c1 * a1 + A2 * c2 * a2) / a3
|
|
pub fn resize_pixel_art(image: &RgbaImage, new_width: u32, new_height: u32) -> RgbaImage {
|
|
span!(_guard, "resize_pixel_art");
|
|
let (width, height) = image.dimensions();
|
|
let mut new_image = RgbaImage::new(new_width, new_height);
|
|
|
|
// Ratio of old image dimensions to new dimensions
|
|
// Also the sampling dimensions within the old image for a single pixel in the
|
|
// new image
|
|
let wratio = width as f32 / new_width as f32;
|
|
let hratio = height as f32 / new_height as f32;
|
|
|
|
for x in 0..new_width {
|
|
// Calculate sampling strategy
|
|
let xsmin = x as f32 * wratio;
|
|
let xsmax = (xsmin + wratio).min(width as f32);
|
|
// Min and max pixels covered
|
|
let xminp = xsmin.floor() as u32;
|
|
let xmaxp = ((xsmax - EPSILON).ceil() as u32).saturating_sub(1);
|
|
// Fraction of first pixel to use
|
|
let first_x_frac = if xminp != xmaxp {
|
|
1.0 - xsmin.fract()
|
|
} else {
|
|
xsmax - xsmin
|
|
};
|
|
let last_x_frac = xsmax - xmaxp as f32;
|
|
for y in 0..new_height {
|
|
// Calculate sampling strategy
|
|
let ysmin = y as f32 * hratio;
|
|
let ysmax = (ysmin + hratio).min(height as f32);
|
|
// Min and max of pixels covered
|
|
let yminp = ysmin.floor() as u32;
|
|
let ymaxp = ((ysmax - EPSILON).ceil() as u32).saturating_sub(1);
|
|
// Fraction of first pixel to use
|
|
let first_y_frac = if yminp != ymaxp {
|
|
1.0 - ysmin.fract()
|
|
} else {
|
|
ysmax - ysmin
|
|
};
|
|
let last_y_frac = ysmax - ymaxp as f32;
|
|
|
|
let mut linear_color = Rgba::new(0.0, 0.0, 0.0, wratio * hratio);
|
|
// Left column
|
|
// First pixel sample (top left assuming that is the origin)
|
|
linear_color += get_linear_with_frac(image, xminp, yminp, first_x_frac * first_y_frac);
|
|
// Left edge
|
|
for j in yminp + 1..ymaxp {
|
|
linear_color += get_linear_with_frac(image, xminp, j, first_x_frac);
|
|
}
|
|
// Bottom left corner
|
|
if yminp != ymaxp {
|
|
linear_color +=
|
|
get_linear_with_frac(image, xminp, ymaxp, first_x_frac * last_y_frac);
|
|
}
|
|
// Interior columns
|
|
for i in xminp + 1..xmaxp {
|
|
// Top edge
|
|
linear_color += get_linear_with_frac(image, i, yminp, first_y_frac);
|
|
// Inner (entire pixel is encompassed by sample)
|
|
for j in yminp + 1..ymaxp {
|
|
linear_color += get_linear_with_frac(image, i, j, 1.0);
|
|
}
|
|
// Bottom edge
|
|
if yminp != ymaxp {
|
|
linear_color += get_linear_with_frac(image, xminp, ymaxp, last_y_frac);
|
|
}
|
|
}
|
|
// Right column
|
|
if xminp != xmaxp {
|
|
// Top right corner
|
|
linear_color +=
|
|
get_linear_with_frac(image, xmaxp, yminp, first_y_frac * last_x_frac);
|
|
// Right edge
|
|
for j in yminp + 1..ymaxp {
|
|
linear_color += get_linear_with_frac(image, xmaxp, j, last_x_frac);
|
|
}
|
|
// Bottom right corner
|
|
if yminp != ymaxp {
|
|
linear_color +=
|
|
get_linear_with_frac(image, xmaxp, ymaxp, last_x_frac * last_y_frac);
|
|
}
|
|
}
|
|
// Divide summed color by area sample covers and convert back to srgb
|
|
// I wonder if precalculating the inverse of these divs would have a significant
|
|
// effect
|
|
linear_color = linear_color / wratio / hratio;
|
|
linear_color =
|
|
Rgba::from_translucent(linear_color.rgb() / linear_color.a, linear_color.a);
|
|
new_image.put_pixel(
|
|
x,
|
|
y,
|
|
image::Rgba(
|
|
linear_to_srgba(linear_color)
|
|
.map(|e| (e * 255.0).round() as u8)
|
|
.into_array(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
new_image
|
|
}
|
|
|
|
fn get_linear(image: &RgbaImage, x: u32, y: u32) -> Rgba<f32> {
|
|
srgba_to_linear(Rgba::<u8>::from(image.get_pixel(x, y).0).map(|e| e as f32 / 255.0))
|
|
}
|
|
|
|
// See comments above resize_pixel_art
|
|
fn get_linear_with_frac(image: &RgbaImage, x: u32, y: u32, frac: f32) -> Rgba<f32> {
|
|
let rgba = get_linear(image, x, y);
|
|
let adjusted_rgb = rgba.rgb() * rgba.a * frac;
|
|
let adjusted_alpha = -frac * (1.0 - rgba.a);
|
|
Rgba::from_translucent(adjusted_rgb, adjusted_alpha)
|
|
}
|