Add additional texture modifiers (#10100)

* Adjust hue, saturation, and lightness
* Colorize using hue, saturation, and lightness
* Adjust contrast & brightness
* Hard light
* Overlay
* Screen
* Create texture of a given size and color
This commit is contained in:
Treer 2023-05-30 05:17:39 +10:00 committed by GitHub
parent a8ec6092e2
commit 8cd1296049
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 529 additions and 36 deletions

@ -594,6 +594,29 @@ Example:
Creates an inventorycube with `grass.png`, `dirt.png^grass_side.png` and
`dirt.png^grass_side.png` textures
#### `[fill:<w>x<h>:<x>,<y>:<color>`
* `<w>`: width
* `<h>`: height
* `<x>`: x position
* `<y>`: y position
* `<color>`: a `ColorString`.
Creates a texture of the given size and color, optionally with an <x>,<y>
position. An alpha value may be specified in the `Colorstring`.
The optional <x>,<y> position is only used if the [fill is being overlaid
onto another texture with '^'.
When [fill is overlaid onto another texture it will not upscale or change
the resolution of the texture, the base texture will determine the output
resolution.
Examples:
[fill:16x16:#20F02080
texture.png^[fill:8x8:4,4:red
#### `[lowpart:<percent>:<file>`
Blit the lower `<percent>`% part of `<file>` on the texture.
@ -636,6 +659,22 @@ the word "`alpha`", then each texture pixel will contain the RGB of
`<color>` and the alpha of `<color>` multiplied by the alpha of the
texture pixel.
#### `[colorizehsl:<hue>:<saturation>:<lightness>`
Colorize the texture to the given hue. The texture will be converted into a
greyscale image as seen through a colored glass, like "Colorize" in GIMP.
Saturation and lightness can optionally be adjusted.
`<hue>` should be from -180 to +180. The hue at 0° on an HSL color wheel is
red, 60° is yellow, 120° is green, and 180° is cyan, while -60° is magenta
and -120° is blue.
`<saturation>` and `<lightness>` are optional adjustments.
`<lightness>` is from -100 to +100, with a default of 0
`<saturation>` is from 0 to 100, with a default of 50
#### `[multiply:<color>`
Multiplies texture colors with the given color.
@ -644,6 +683,76 @@ Result is more like what you'd expect if you put a color on top of another
color, meaning white surfaces get a lot of your new color while black parts
don't change very much.
A Multiply blend can be applied between two textures by using the overlay
modifier with a brightness adjustment:
textureA.png^[contrast:0:-64^[overlay:textureB.png
#### `[screen:<color>`
Apply a Screen blend with the given color. A Screen blend is the inverse of
a Multiply blend, lightening images instead of darkening them.
`<color>` is specified as a `ColorString`.
A Screen blend can be applied between two textures by using the overlay
modifier with a brightness adjustment:
textureA.png^[contrast:0:64^[overlay:textureB.png
#### `[hsl:<hue>:<saturation>:<lightness>`
Adjust the hue, saturation, and lightness of the texture. Like
"Hue-Saturation" in GIMP, but with 0 as the mid-point.
`<hue>` should be from -180 to +180
`<saturation>` and `<lightness>` are optional, and both percentages.
`<lightness>` is from -100 to +100.
`<saturation>` goes down to -100 (fully desaturated) but may go above 100,
allowing for even muted colors to become highly saturated.
#### `[contrast:<contrast>:<brightness>`
Adjust the brightness and contrast of the texture. Conceptually like
GIMP's "Brightness-Contrast" feature but allows brightness to be wound
all the way up to white or down to black.
`<contrast>` is a value from -127 to +127.
`<brightness>` is an optional value, from -127 to +127.
If only a boost in contrast is required, an alternative technique is to
hardlight blend the texture with itself, this increases contrast in the same
way as an S-shaped color-curve, which avoids dark colors clipping to black
and light colors clipping to white:
texture.png^[hardlight:texture.png
#### `[overlay:<file>`
Applies an Overlay blend with the two textures, like the Overlay layer mode
in GIMP. Overlay is the same as Hard light but with the role of the two
textures swapped, see the `[hardlight` modifier description for more detail
about these blend modes.
#### `[hardlight:<file>`
Applies a Hard light blend with the two textures, like the Hard light layer
mode in GIMP.
Hard light combines Multiply and Screen blend modes. Light parts of the
`<file>` texture will lighten (screen) the base texture, and dark parts of the
`<file>` texture will darken (multiply) the base texture. This can be useful
for applying embossing or chiselled effects to textures. A Hard light with the
same texture acts like applying an S-shaped color-curve, and can be used to
increase contrast without clipping.
Hard light is the same as Overlay but with the roles of the two textures
swapped, i.e. `A.png^[hardlight:B.png` is the same as `B.png^[overlay:A.png`
#### `[png:<base64>`
Embed a base64 encoded PNG image in the texture string.

@ -556,6 +556,32 @@ static void apply_colorize(video::IImage *dst, v2u32 dst_pos, v2u32 size,
static void apply_multiplication(video::IImage *dst, v2u32 dst_pos, v2u32 size,
const video::SColor &color);
// Perform a Screen blend with the given color. The opposite effect of a
// Multiply blend.
static void apply_screen(video::IImage *dst, v2u32 dst_pos, v2u32 size,
const video::SColor &color);
// Adjust the hue, saturation, and lightness of destination. Like
// "Hue-Saturation" in GIMP.
// If colorize is true then the image will be converted to a grayscale
// image as though seen through a colored glass, like "Colorize" in GIMP.
static void apply_hue_saturation(video::IImage *dst, v2u32 dst_pos, v2u32 size,
s32 hue, s32 saturation, s32 lightness, bool colorize);
// Apply an overlay blend to an images.
// Overlay blend combines Multiply and Screen blend modes.The parts of the top
// layer where the base layer is light become lighter, the parts where the base
// layer is dark become darker.Areas where the base layer are mid grey are
// unaffected.An overlay with the same picture looks like an S - curve.
static void apply_overlay(video::IImage *overlay, video::IImage *dst,
v2s32 overlay_pos, v2s32 dst_pos, v2u32 size, bool hardlight);
// Adjust the brightness and contrast of the base image. Conceptually like
// "Brightness-Contrast" in GIMP but allowing brightness to be wound all the
// way up to white or down to black.
static void apply_brightness_contrast(video::IImage *dst, v2u32 dst_pos, v2u32 size,
s32 brightness, s32 contrast);
// Apply a mask to an image
static void apply_mask(video::IImage *mask, video::IImage *dst,
v2s32 mask_pos, v2s32 dst_pos, v2u32 size);
@ -1106,43 +1132,53 @@ static std::string unescape_string(const std::string &str, const char esc = '\\'
return out;
}
/*
Replaces the smaller of the two images with one upscaled to match the
dimensions of the other.
Ensure no other references to these images are being held, as one may
get dropped and switched with a new image.
*/
void upscaleImagesToMatchLargest(video::IImage *& img1,
video::IImage *& img2)
{
core::dimension2d<u32> dim1 = img1->getDimension();
core::dimension2d<u32> dim2 = img2->getDimension();
if (dim1 == dim2) {
// image dimensions match, no scaling required
}
else if (dim1.Width * dim1.Height < dim2.Width * dim2.Height) {
// Upscale img1
video::IImage *scaled_image = RenderingEngine::get_video_driver()->
createImage(video::ECF_A8R8G8B8, dim2);
img1->copyToScaling(scaled_image);
img1->drop();
img1 = scaled_image;
} else {
// Upscale img2
video::IImage *scaled_image = RenderingEngine::get_video_driver()->
createImage(video::ECF_A8R8G8B8, dim1);
img2->copyToScaling(scaled_image);
img2->drop();
img2 = scaled_image;
}
}
void blitBaseImage(video::IImage* &src, video::IImage* &dst)
{
//infostream<<"Blitting "<<part_of_name<<" on base"<<std::endl;
upscaleImagesToMatchLargest(dst, src);
// Size of the copied area
core::dimension2d<u32> dim = src->getDimension();
//core::dimension2d<u32> dim(16,16);
core::dimension2d<u32> dim_dst = dst->getDimension();
// Position to copy the blitted to in the base image
core::position2d<s32> pos_to(0,0);
// Position to copy the blitted from in the blitted image
core::position2d<s32> pos_from(0,0);
// Blit
/*image->copyToWithAlpha(baseimg, pos_to,
core::rect<s32>(pos_from, dim),
video::SColor(255,255,255,255),
NULL);*/
core::dimension2d<u32> dim_dst = dst->getDimension();
if (dim == dim_dst) {
blit_with_alpha(src, dst, pos_from, pos_to, dim);
} else if (dim.Width * dim.Height < dim_dst.Width * dim_dst.Height) {
// Upscale overlying image
video::IImage *scaled_image = RenderingEngine::get_video_driver()->
createImage(video::ECF_A8R8G8B8, dim_dst);
src->copyToScaling(scaled_image);
blit_with_alpha(scaled_image, dst, pos_from, pos_to, dim_dst);
scaled_image->drop();
} else {
// Upscale base image
video::IImage *scaled_base = RenderingEngine::get_video_driver()->
createImage(video::ECF_A8R8G8B8, dim);
dst->copyToScaling(scaled_base);
dst->drop();
dst = scaled_base;
blit_with_alpha(src, dst, pos_from, pos_to, dim);
}
blit_with_alpha(src, dst, pos_from, pos_to, dim_dst);
}
bool TextureSource::generateImagePart(std::string part_of_name,
@ -1312,6 +1348,44 @@ bool TextureSource::generateImagePart(std::string part_of_name,
}
}
}
/*
[fill:WxH:color
[fill:WxH:X,Y:color
Creates a texture of the given size and color, optionally with an <x>,<y>
position. An alpha value may be specified in the `Colorstring`.
*/
else if (str_starts_with(part_of_name, "[fill"))
{
s32 x = 0;
s32 y = 0;
Strfnd sf(part_of_name);
sf.next(":");
u32 width = stoi(sf.next("x"));
u32 height = stoi(sf.next(":"));
std::string color_or_x = sf.next(",");
video::SColor color;
if (!parseColorString(color_or_x, color, true)) {
x = stoi(color_or_x);
y = stoi(sf.next(":"));
std::string color_str = sf.next(":");
if (!parseColorString(color_str, color, false))
return false;
}
core::dimension2d<u32> dim(width, height);
video::IImage *img = driver->createImage(video::ECF_A8R8G8B8, dim);
img->fill(color);
if (baseimg == nullptr) {
baseimg = img;
} else {
blit_with_alpha(img, baseimg, v2s32(0, 0), v2s32(x, y), dim);
img->drop();
}
}
/*
[brighten
*/
@ -1584,10 +1658,16 @@ bool TextureSource::generateImagePart(std::string part_of_name,
}
/*
[multiply:color
multiplys a given color to any pixel of an image
or
[screen:color
Multiply and Screen blend modes are basic blend modes for darkening and lightening
images, respectively.
A Multiply blend multiplies a given color to every pixel of an image.
A Screen blend has the opposite effect to a Multiply blend.
color = color as ColorString
*/
else if (str_starts_with(part_of_name, "[multiply:")) {
else if (str_starts_with(part_of_name, "[multiply:") ||
str_starts_with(part_of_name, "[screen:")) {
Strfnd sf(part_of_name);
sf.next(":");
std::string color_str = sf.next(":");
@ -1603,13 +1683,18 @@ bool TextureSource::generateImagePart(std::string part_of_name,
if (!parseColorString(color_str, color, false))
return false;
apply_multiplication(baseimg, v2u32(0, 0), baseimg->getDimension(), color);
if (str_starts_with(part_of_name, "[multiply:")) {
apply_multiplication(baseimg, v2u32(0, 0),
baseimg->getDimension(), color);
} else {
apply_screen(baseimg, v2u32(0, 0), baseimg->getDimension(), color);
}
}
/*
[colorize:color
[colorize:color:ratio
Overlays image with given color
color = color as ColorString
ratio = optional string "alpha", or a weighting between 0 and 255
*/
else if (str_starts_with(part_of_name, "[colorize:"))
{
@ -1876,6 +1961,115 @@ bool TextureSource::generateImagePart(std::string part_of_name,
}
pngimg->drop();
}
/*
[hsl:hue:saturation:lightness
or
[colorizehsl:hue:saturation:lightness
Adjust the hue, saturation, and lightness of the base image. Like
"Hue-Saturation" in GIMP, but with 0 as the mid-point.
Hue should be from -180 to +180, though 0 to 360 is also supported.
Saturation and lightness are optional, with lightness from -100 to
+100, and sauration from -100 to +100-or-higher.
If colorize is true then saturation is from 0 to 100, and the image
will be converted to a grayscale image as though seen through a
colored glass, like "Colorize" in GIMP.
*/
else if (str_starts_with(part_of_name, "[hsl:") ||
str_starts_with(part_of_name, "[colorizehsl:")) {
if (baseimg == nullptr) {
errorstream << "generateImagePart(): baseimg == NULL "
<< "for part_of_name=\"" << part_of_name
<< "\", cancelling." << std::endl;
return false;
}
bool colorize = str_starts_with(part_of_name, "[colorizehsl:");
// saturation range is 0 to 100 when colorize is true
s32 defaultSaturation = colorize ? 50 : 0;
Strfnd sf(part_of_name);
sf.next(":");
s32 hue = mystoi(sf.next(":"), -180, 360);
s32 saturation = sf.at_end() ? defaultSaturation : mystoi(sf.next(":"), -100, 1000);
s32 lightness = sf.at_end() ? 0 : mystoi(sf.next(":"), -100, 100);
apply_hue_saturation(baseimg, v2u32(0, 0), baseimg->getDimension(),
hue, saturation, lightness, colorize);
}
/*
[overlay:filename
or
[hardlight:filename
"A.png^[hardlight:B.png" is the same as "B.png^[overlay:A.Png"
Applies an Overlay or Hard Light blend between two images, like the
layer modes of the same names in GIMP.
Overlay combines Multiply and Screen blend modes. The parts of the
top layer where the base layer is light become lighter, the parts
where the base layer is dark become darker. Areas where the base
layer are mid grey are unaffected. An overlay with the same picture
looks like an S-curve.
Swapping the top layer and base layer is a Hard Light blend
*/
else if (str_starts_with(part_of_name, "[overlay:") ||
str_starts_with(part_of_name, "[hardlight:")) {
if (baseimg == nullptr) {
errorstream << "generateImage(): baseimg == NULL "
<< "for part_of_name=\"" << part_of_name
<< "\", cancelling." << std::endl;
return false;
}
Strfnd sf(part_of_name);
sf.next(":");
std::string filename = unescape_string(sf.next_esc(":", escape), escape);
video::IImage *img = generateImage(filename, source_image_names);
if (img) {
upscaleImagesToMatchLargest(baseimg, img);
bool hardlight = str_starts_with(part_of_name, "[hardlight:");
apply_overlay(img, baseimg, v2s32(0, 0), v2s32(0, 0),
img->getDimension(), hardlight);
img->drop();
} else {
errorstream << "generateImage(): Failed to load \""
<< filename << "\".";
}
}
/*
[contrast:C:B
Adjust the brightness and contrast of the base image. Conceptually
like GIMP's "Brightness-Contrast" feature but allows brightness to
be wound all the way up to white or down to black.
C and B are both values from -127 to +127.
B is optional.
*/
else if (str_starts_with(part_of_name, "[contrast:")) {
if (baseimg == nullptr) {
errorstream << "generateImagePart(): baseimg == NULL "
<< "for part_of_name=\"" << part_of_name
<< "\", cancelling." << std::endl;
return false;
}
Strfnd sf(part_of_name);
sf.next(":");
s32 contrast = mystoi(sf.next(":"), -127, 127);
s32 brightness = sf.at_end() ? 0 : mystoi(sf.next(":"), -127, 127);
apply_brightness_contrast(baseimg, v2u32(0, 0),
baseimg->getDimension(), brightness, contrast);
}
else
{
errorstream << "generateImagePart(): Invalid "
@ -1984,7 +2178,7 @@ static void blit_with_interpolate_overlay(video::IImage *src, video::IImage *dst
#endif
/*
Apply color to destination
Apply color to destination, using a weighted interpolation blend
*/
static void apply_colorize(video::IImage *dst, v2u32 dst_pos, v2u32 size,
const video::SColor &color, int ratio, bool keep_alpha)
@ -2022,7 +2216,7 @@ static void apply_colorize(video::IImage *dst, v2u32 dst_pos, v2u32 size,
}
/*
Apply color to destination
Apply color to destination, using a Multiply blend mode
*/
static void apply_multiplication(video::IImage *dst, v2u32 dst_pos, v2u32 size,
const video::SColor &color)
@ -2042,6 +2236,196 @@ static void apply_multiplication(video::IImage *dst, v2u32 dst_pos, v2u32 size,
}
}
/*
Apply color to destination, using a Screen blend mode
*/
static void apply_screen(video::IImage *dst, v2u32 dst_pos, v2u32 size,
const video::SColor &color)
{
video::SColor dst_c;
for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++)
for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) {
dst_c = dst->getPixel(x, y);
dst_c.set(
dst_c.getAlpha(),
255 - ((255 - dst_c.getRed()) * (255 - color.getRed())) / 255,
255 - ((255 - dst_c.getGreen()) * (255 - color.getGreen())) / 255,
255 - ((255 - dst_c.getBlue()) * (255 - color.getBlue())) / 255
);
dst->setPixel(x, y, dst_c);
}
}
/*
Adjust the hue, saturation, and lightness of destination. Like
"Hue-Saturation" in GIMP, but with 0 as the mid-point.
Hue should be from -180 to +180, or from 0 to 360.
Saturation and Lightness are percentages.
Lightness is from -100 to +100.
Saturation goes down to -100 (fully desaturated) but can go above 100,
allowing for even muted colors to become saturated.
If colorize is true then saturation is from 0 to 100, and destination will
be converted to a grayscale image as seen through a colored glass, like
"Colorize" in GIMP.
*/
static void apply_hue_saturation(video::IImage *dst, v2u32 dst_pos, v2u32 size,
s32 hue, s32 saturation, s32 lightness, bool colorize)
{
video::SColorf colorf;
video::SColorHSL hsl;
f32 norm_s = core::clamp(saturation, -100, 1000) / 100.0f;
f32 norm_l = core::clamp(lightness, -100, 100) / 100.0f;
if (colorize) {
hsl.Saturation = core::clamp((f32)saturation, 0.0f, 100.0f);
}
for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++)
for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) {
if (colorize) {
f32 lum = dst->getPixel(x, y).getLuminance() / 255.0f;
if (norm_l < 0) {
lum *= norm_l + 1.0f;
} else {
lum = lum * (1.0f - norm_l) + norm_l;
}
hsl.Hue = 0;
hsl.Luminance = lum * 100;
} else {
// convert the RGB to HSL
colorf = video::SColorf(dst->getPixel(x, y));
hsl.fromRGB(colorf);
if (norm_l < 0) {
hsl.Luminance *= norm_l + 1.0f;
} else{
hsl.Luminance = hsl.Luminance + norm_l * (100.0f - hsl.Luminance);
}
// Adjusting saturation in the same manner as lightness resulted in
// muted colours being affected too much and bright colours not
// affected enough, so I'm borrowing a leaf out of gimp's book and
// using a different scaling approach for saturation.
// https://github.com/GNOME/gimp/blob/6cc1e035f1822bf5198e7e99a53f7fa6e281396a/app/operations/gimpoperationhuesaturation.c#L139-L145=
// This difference is why values over 100% are not necessary for
// lightness but are very useful with saturation. An alternative UI
// approach would be to have an upper saturation limit of 100, but
// multiply positive values by ~3 to make it a more useful positive
// range scale.
hsl.Saturation *= norm_s + 1.0f;
hsl.Saturation = core::clamp(hsl.Saturation, 0.0f, 100.0f);
}
// Apply the specified HSL adjustments
hsl.Hue = fmod(hsl.Hue + hue, 360);
if (hsl.Hue < 0)
hsl.Hue += 360;
// Convert back to RGB
hsl.toRGB(colorf);
dst->setPixel(x, y, colorf.toSColor());
}
}
/*
Apply an Overlay blend to destination
If hardlight is true then swap the dst & blend images (a hardlight blend)
*/
static void apply_overlay(video::IImage *blend, video::IImage *dst,
v2s32 blend_pos, v2s32 dst_pos, v2u32 size, bool hardlight)
{
video::IImage *blend_layer = hardlight ? dst : blend;
video::IImage *base_layer = hardlight ? blend : dst;
v2s32 blend_layer_pos = hardlight ? dst_pos : blend_pos;
v2s32 base_layer_pos = hardlight ? blend_pos : dst_pos;
for (u32 y = 0; y < size.Y; y++)
for (u32 x = 0; x < size.X; x++) {
s32 base_x = x + base_layer_pos.X;
s32 base_y = y + base_layer_pos.Y;
video::SColor blend_c =
blend_layer->getPixel(x + blend_layer_pos.X, y + blend_layer_pos.Y);
video::SColor base_c = base_layer->getPixel(base_x, base_y);
double blend_r = blend_c.getRed() / 255.0;
double blend_g = blend_c.getGreen() / 255.0;
double blend_b = blend_c.getBlue() / 255.0;
double base_r = base_c.getRed() / 255.0;
double base_g = base_c.getGreen() / 255.0;
double base_b = base_c.getBlue() / 255.0;
base_c.set(
base_c.getAlpha(),
// Do a Multiply blend if less that 0.5, otherwise do a Screen blend
(u32)((base_r < 0.5 ? 2 * base_r * blend_r : 1 - 2 * (1 - base_r) * (1 - blend_r)) * 255),
(u32)((base_g < 0.5 ? 2 * base_g * blend_g : 1 - 2 * (1 - base_g) * (1 - blend_g)) * 255),
(u32)((base_b < 0.5 ? 2 * base_b * blend_b : 1 - 2 * (1 - base_b) * (1 - blend_b)) * 255)
);
dst->setPixel(base_x, base_y, base_c);
}
}
/*
Adjust the brightness and contrast of the base image.
Conceptually like GIMP's "Brightness-Contrast" feature but allows brightness to be
wound all the way up to white or down to black.
*/
static void apply_brightness_contrast(video::IImage *dst, v2u32 dst_pos, v2u32 size,
s32 brightness, s32 contrast)
{
video::SColor dst_c;
// Only allow normalized contrast to get as high as 127/128 to avoid infinite slope.
// (we could technically allow -128/128 here as that would just result in 0 slope)
double norm_c = core::clamp(contrast, -127, 127) / 128.0;
double norm_b = core::clamp(brightness, -127, 127) / 127.0;
// Scale brightness so its range is -127.5 to 127.5, otherwise brightness
// adjustments will outputs values from 0.5 to 254.5 instead of 0 to 255.
double scaled_b = brightness * 127.5 / 127;
// Calculate a contrast slope such that that no colors will get clamped due
// to the brightness setting.
// This allows the texture modifier to used as a brightness modifier without
// the user having to calculate a contrast to avoid clipping at that brightness.
double slope = 1 - fabs(norm_b);
// Apply the user's contrast adjustment to the calculated slope, such that
// -127 will make it near-vertical and +127 will make it horizontal
double angle = atan(slope);
angle += norm_c <= 0
? norm_c * angle // allow contrast slope to be lowered to 0
: norm_c * (M_PI_2 - angle); // allow contrast slope to be raised almost vert.
slope = tan(angle);
double c = slope <= 1
? -slope * 127.5 + 127.5 + scaled_b // shift up/down when slope is horiz.
: -slope * (127.5 - scaled_b) + 127.5; // shift left/right when slope is vert.
// add 0.5 to c so that when the final result is cast to int, it is effectively
// rounded rather than trunc'd.
c += 0.5;
for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++)
for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) {
dst_c = dst->getPixel(x, y);
dst_c.set(
dst_c.getAlpha(),
core::clamp((int)(slope * dst_c.getRed() + c), 0, 255),
core::clamp((int)(slope * dst_c.getGreen() + c), 0, 255),
core::clamp((int)(slope * dst_c.getBlue() + c), 0, 255)
);
dst->setPixel(x, y, dst_c);
}
}
/*
Apply mask to destination
*/