Document Texture Modifiers (#10)

* First draft

* Improve wording, back up claim

* Improve & reoder documentation

* Remove internal applyfiltersformesh modifier

* Fixes

* Fixes

* Fix [lowpart argument order in example

* Fix grammar

* Improvements

* Fix, clarify, simplify, add examples

* Minor formatting fixes

* Extend docs regarding [png and [combine

* Use literal monospace where appropriate
This commit is contained in:
Lars Müller 2022-09-01 12:53:59 +02:00 committed by GitHub
parent 13edf2b235
commit a91ee25e9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

443
doc/texture_modifiers.adoc Normal file

@ -0,0 +1,443 @@
= Texture Modifiers
include::../include/config.adoc[]
include::../include/types.adoc[]
:description: DSL for texture composition
:keywords: texture, image, modifier, generation, colorize
Texture modifiers - strings instructing Minetest to manipulate images -
are, besides dynamic media, the only way to dynamically generate textures at runtime.
They can also be used to simplify mods by generating repetitive textures
such as differently colored tool textures.
[IMPORTANT]
.Redundancy with hardware colorization
====
Texture modifiers are partially redundant with hardware colorization.
Hardware colorization should be preferred as it is more flexible
and presumably generates fewer garbage textures.
====
[IMPORTANT]
.Performance Issues
====
* All textures are generated on the CPU, not the GPU. Generation is therefore slow and may temporarily block the client thread.
* Texture modifiers are https://github.com/minetest/minetest/issues/11531[kept in client memory forever].
* Cached texture modifiers are https://github.com/minetest/minetest/issues/11587[not leveraged when generating new ones].
* Non-linear (at least quadratic) time complexity of the shotgun parser is possible.
Use texture modifiers mostly statically.
Keep dynamically generated textures to a minimum to not fill up the client cache over time.
Don't force the client to perform many expensive texture generations in a small timespan if you want a smooth client experience.
Do not create large, convoluted texture modifiers if possible.
====
[IMPORTANT]
.Bugs
====
* Large texture modifiers may https://github.com/minetest/minetest/issues/11829[freeze the client].
* Out-of-bound memory reads are possible using certain texture modifiers. Example: `[lowpart:0:blank.png`.
* Out-of-bound pixels are often considered "full white" (`#FFFFFFFF`)
* Certain invalid texture modifiers may lead to client segmentation faults.
* Syntactically invalid parts of texture modifiers may silently be ignored.
====
== Texture Packs
Texture modifiers can be used in node & item texture overrides.
[IMPORTANT]
.Limitations
====
Texture-modifier-generated textures can not be replaced by a texture pack (except through texture overrides);
only base textures can be properly replaced.
Usage of certain texture modifiers might require certain texture resolutions to be used by texture packs,
as texture modifiers operate on pixels instead of relative units.
Lower-res TPs can usually scale up but higher-res TPs will have to scale down.
====
TIP: Use `[resize` to forcibly resize textures to your required resolution
(e.g. for `[combine`) in order to be as TP-agnostic as possible.
== In Lua
=== API
==== `minetest.inventorycube(top, left, right)`
The only texture modifier utility function provided.
===== Arguments
[%autowidth, frame=none]
|===
| `top`, `left`, `right` | `{type-string}` | Texture modifiers for the respective node faces
|===
===== Returns
Texture modifier `[inventorycube{<top>{<left>{right>` with
`top`, `left`, `right` escaped according to the rules of `inventorycube` escaping.
=== String Notations
Lua provides three string notations: Quoted (single or double) and long.
The type of quotes doesn't really matter, as texture names shouldn't include either type of quotes anyways.
Keep in mind that to encode a backslash in quoted strings, you have to escape it with another backslash:
`+a.png^[mask:b.png\^c.png+` would be encoded as `+"a.png^[mask:b.png\\^c.png"+` using double quotes.
Long strings enclose their content with two pairs of square brackets (`[]`) with
a matching number of equals signs (`=`) between the two brackets of each pair:
`[[...]]` and `[===[...]===]` are examples of valid long strings.
Within them, no escaping applies, which means that texture modifiers can be written down literally;
a leading square bracket such as that of a generating texture modifier is not a problem.
Example: `[[[combine:1x1:0,0=a.png]]`.
Using equals signs is never necessary since texture modifiers won't contain `[[` or `]]`.
[TIP]
.String Builders / DSLs
====
You can use metamethods in order to implement a neat,
possibly OOP-ish Lua DSL that does the escaping and formatting for you.
====
== Syntax
[WARNING]
.Validation
====
Texture modifiers are only parsed clientside, where errors lead to poor behavior
(error messages in the best case, sometimes the wrong texture, crashes in the worst case).
They are not validated serverside. Take additional care to ensure no syntax errors or values which cause undefined behavior.
====
TIP: Use a string builder which guarantees valid texture modifiers.
=== Base textures
Texture modifiers work on _base textures_ which are specified in a string form as media file names. See the supported file formats.
[TIP]
.Naming convention
====
It is considered good practice for the texture name to be the modname
plus a series of lowercase terms separated by underscores.
Example: `mymod_mything_mytexture.png`.
This helps ensure that texture names don't collide,
in which case one texture will override the other.
====
`<...>` is used to denote texture modifier arguments below, `[...]` denotes optional parts.
All ranges denoted below are inclusive (i.e. `0` to `10` contains both `0` and `10`).
=== Overlaying
Overlaying is a binary operator using the exponentiation symbol (`+^+`).
The right-hand operand is overlaid over the left-hand operand.
Overlaying is associative: `+<a>^<b>^<c>+` is the same as both `+(<a>^<b>)^<c>+` and `+<a>^(<b>^<c>)+`.
Before overlaying, the texture with the lower pixel count is upscaled to
the dimensions of the texture with the higher pixel count.
Alpha blending is applied correctly.
=== Argument Escaping
Argument escaping uses the backslash (`+\+`). It is only allowed within "combining" texture modifiers.
_All characters can be escaped_;
only a few (`+^+`, `:` & `+\+`) must be escaped to allow the use of texture modifiers
as arguments (not base images) within combining texture modifiers.
Nested escaping is possible; escape each backslash with a backslash for this,
doubling the amount of backslashes: Nesting to a depth of `n` requires stem:[2^n] backslashes per character to be escaped.
The `inventorycube` texture modifier uses a different form of escaping for its arguments:
`+^+` is replaced with `&`. `minetest.inventorycube(top, left, right)` performs this escaping.
It is not possible to nest the `inventorycube` texture modifier within itself
as it uses curly braces (`{`) for separating its arguments but does not provide a way of escaping them.
Example escaping implementation:
[source,lua]
----
local function escape_argument(texture_modifier)
return texture_modifier:gsub(".", {["\\"] = "\\\\", ["^"] = "\\^", [":"] = "\\:"})
end
----
=== Grouping
The operands of the overlaying operator may be enclosed within parentheses to force them being evaluated first.
This is only reasonable to force evaluation of texture modifiers before an overlay operation.
Grouping can not be used to use texture modifiers within combining texture modifiers;
grouping will be ignored for delimiting purposes. You must use escaping for that.
Wrong: `+a^[lowpart:1:(b^c)+`, right: `+a^[lowpart:1:b\^c+`.
Also wrong: `+[combine:1x2:0,0=(a^b):0,1=(c^[multiply:red)+` -
the combine parsing will ignore the parentheses and misinterpret the colon `:`
before `red` as a delimiter for combine.
`+[combine:1x2:0,0=(a^b):0,1=(c^d)+` will work, but you shouldn't rely on it.
Grouping can however be used to enclose combining texture modifiers,
separating them from the containing texture modifier.
TIP: Use grouping for evaluating parts of the right-hand side first, like this: `+a^[multiply:green^(b^[multiply:red)+`
=== Modifiers
All texture modifiers create new textures which can be modified further
and do not modify the textures they operate on.
TIP: Use `string.format("%d", number)` - `("%d"):format(number)` in shorthand - to guarantee that integers are parsable.
In practice, `tostring(number)` or implicit conversion to string when concatenating will work as expected.
==== Combining Texture Modifiers
The following texture modifiers are considered "combining",
as they operate by combining multiple textures into one,
taking textures other than the "base texture" as arguments:
* `mask`: Bitwise masking (takes mask)
* `lowpart`: Blitting a lower part of one texture onto another (takes foreground)
* `combine`: Combining multiple textures through blitting at pixel locations (takes multiple textures)
* `inventorycube`: Rendering an inventorycube from three provided textures
==== Base Texture Modifiers
These texture modifiers all modify a base texture `<base>`,
which may in turn consist of texture modifiers.
===== `+<base>^[brighten+`
Interpolates 50-50 between the color of each pixel of the base texture and white.
===== `+<base>^[noalpha+`
Sets the alpha channel of the base texture to full (`255`).
As the red, green and blue channels aren't premultiplied with alpha in PNGs,
this might reveal hidden colors of otherwise transparent portions of an image.
===== `+<base>^[makealpha:<r>,<g>,<b>+`
* `r`, `g`, `b` are integers ranging from `0` to `255`.
Pixels of the base texture having the exact same RGB color will have their alpha set to `0`.
As the red, green and blue channels are kept, the original color can be restored using `[noalpha`
(which will however also make originally semitransparent portions of the image opaque).
===== `+<base>^[opacity:<ratio>+`
* `ratio` is an integer ranging from `0` to `255`
Multiplies the alpha value of each pixel of the base texture with `ratio/255` and rounds properly afterwards.
===== `+<base>^[invert:<mode>+`
* `mode` is a string which may contain the characters `r`, `g`, `b` and `a`.
The channels corresponding to the occurring characters (red, green, blue and alpha) will be inverted (set to `255 - value`).
===== `+<base>^[transform<t>+`
`t` is either a number or a name identifying a transformation from the following table:
[cols="1,1,1"]
|===
| Number | Name | Transformation
| 0 | I | Identity (no transformation)
| 1 | R90 | Rotate by 90° counterclockwise
| 2 | R180 | Rotate by 180° counterclockwise
| 3 | R270 | Rotate by 270° counterclockwise
| 4 | FX | Flip X (horizontally)
| 5 | FXR90 | Flip X, then rotate by 90° counterclockwise
| 6 | FY | Flip Y (vertically)
| 7 | FYR90 | Flip Y, then rotate by 90° counterclockwise
|===
===== `+<base>^[verticalframe:<framecount>:<frame>+`
* `framecount`: Animation frame count
* `frame`: Current animation frame, 0-indexed
Result: Vertically crops the texture by dividing the base texture height through the frame count to determine the frame height.
As the division is an integer division, a remaining fractional frame will be discarded.
WARNING: Specifying a `framecount` of `0` will trigger a floating point exception, crashing the client.
===== `+<base>^[crack[<opacity>]:[<framecount>:]<tilecount>:<frame>+`
Shorthand for overlaying a scaled frame of the crack texture, `crack_anylength.png`,
over a texture, with options for alpha and blitting on all frames.
* `opacity`: Optional. If set to `o` (modifier name becomes `cracko`) the crack will only be overlaid over fully opaque base texture regions.
* `framecount`: Optional. Vertical animation frame count of the crack texture; usually omitted.
* `tilecount`: Vertical & horizontal tile count of the base texture. The crack will be blit on each tile of the base texture. Usually `1`.
* `frame`: Current animation frame ("crack progression").
NOTE: This always scales the crack to the size of the base texture (or the tiles of the base texture, if `tilesize` is provided).
===== `+<base>^[sheet:<w>x<h>:<x>,<y>+`
* `w`, `h`: Tile dimensions (positive integers)
* `x`, `y`: Tile position, 0-indexed
Retrieves the tile at position `x`, `y`. Can be used to retrieve single pixels by setting `w`, `h` to `1`.
WARNING: Setting `w` or `h` to `0` will trigger a floating point exception, crashing the client.
===== `+<base>^[multiply:<color>+`
* `color` is a `ColorString`
Multiplies the RGB values of the base texture per pixel with the RGB values of `color`; the alpha value of `color` is ignored.
===== `+<base>^[colorize:<color>[:<ratio>]+`
* `color` is a `ColorString`
* `ratio` is an optional integer from `0` to `255` or the string `alpha`
Interpolates between `color` and the pixel colors of the base texture as specified by the `ratio`:
* Defaults to the alpha of `color` if omitted;
* If it is an integer from `0` (only pixel color) to `255` (only `color`), it is directly used as interpolation ratio:
The resulting color of a pixel is `ratio` times `color` plus `(255 - ratio)` times pixel color;
* If it is the string `alpha`, the texture pixel's alpha value determines the `ratio` per pixel
===== `+<base>^[mask:<texture>+`
* `texture` is an escaped texture modifier
Applies _bitwise and_ (`bit.band`) to all RGBA values of `texture` and the base texture.
The dimensions of the resulting texture are determined by the base texture.
If a pixel of the base texture is out of bounds on `texture`, it is preserved.
Masking is associative and commutative if all involved textures have the same dimensions.
===== `+<base>^[lowpart:<percent>:<texture>+`
* `percent` is an integer from `0` to `100`
* `texture` is an escaped texture modifier
Overlays the lower `percent` part of `texture` on the base texture `x`.
TIP: Use `blank.png` as base texture `x` if you do not want a background.
==== Base Texture Generators
These modifiers do not accept a base texture as they generate one from their arguments.
===== `[png:<data>`
* `data` is a base64-encoded PNG bytestring
Creates a texture from an embedded base64-encoded PNG image.
`data` can be built by combining `minetest.encode_base64` and `minetest.encode_png`:
[source,lua]
====
local function embedded_png(width, height, data)
return "[png:" .. minetest.encode_base64(minetest.encode_png(width, height, data))
end
====
IMPORTANT: Not supported by Minetest 5.4 and older clients. May lead to client crashes if used in node tiles.
Minetest 5.5 and newer servers will automatically prepend `+blank.png^+` to `[png` tiles to mitigate this.
WARNING: Do not use this for large textures. If used as an object texture,
the texture modifier will get sent arbitrarily often, putting a strain on the network.
TIP: Consider using other texture modifiers cleverly or using dynamic media instead.
===== `[combine:<w>x<h>:<textures>`
* `w`: Width of the resulting texture
* `h`: Height of the resulting texture
* `textures`: Colon (`:`)-separated list of locations `x`, `y` (both integer, may be negative)
and escaped texture modifiers `texture` to blit in the form `<x>,<y>=<texture>`. Can be empty.
A _combining_ texture modifier accepting other texture modifiers as arguments.
Nesting `combine` is possible through escaping.
Generates a texture of dimensions `w`, `h` on which all `textures` have been blit at the specified locations.
The background is black and transparent (`#00000000`).
IMPORTANT: Node tiles starting with `[combine` are broken on Minetest 5.4 and older clients connecting to 5.5 servers
due to the aforementioned `[png` workaround accidentally being applied to `[combine` as well.
TIP: To work around this, you can enclose your node tile texture modifiers using `[combine` in parentheses: `([combine:...)`.
A generic patch which loops over node definitions and wraps matching texture modifiers
https://gist.github.com/appgurueu/dea1d1d9d8494e9c00114d36d58c5932[is available as well].
===== `[inventorycube{<top>{<left>{<right>`
Renders a cube with the three given textures using simple software rendering.
The resulting image will be 9 times the nearest power of 2
large enough to contain the dimensions of the largest image,
clamped to a range of at least 4 and at most 64.
As a formula: stem:[9 * max(4, min(64, 2^ceil(log_2(max(d)))))]
where stem:[d] is the set of dimensions (width & height) of all faces.
== Examples
=== Cracked Nodes
* Cracked Stone: `+default_stone.png^[crack:1:2+`
** Use the third crack progression (`2` if 0-indexed)...
** ... and draw it on a stone texture consisting of `1` tile
* Cracked Lava: `+default_lava_source_animated.png^[crack:1:8:2+`
** The `default_lava_source_animated.png` texture has `8` animated vertical frames...
** ... and exactly `1` "tile" horizontally (frames are not tiles)
If in doubt, prefer an explicit `verticalframe` and overlaying over `crack`.
=== Cropping
Cropping can be accomplished using the `[combine` texture modifier:
Examples (cropping `default_stone.png`, 16x16):
* Remove left & top 6px: `[combine:8x8:-8,-8=default_stone.png`
* Remove right & bottom 6px: `[combine:8x8:0,0=default_stone.png`
=== Progress Bars
Combine or lowpart and rotate (the latter more TP-agnostic)
The below examples use `progress_bar.png` & `progress_bar_bg.png` from Minetest's base textures, both 256x48.
* Using nested ``combine``s: `+[combine:256x48:0,0=progress_bar_bg.png:0,0=[combine\:128x48\:0,0=progress_bar.png+`
** First crop the foreground to its left half (`128` of `256` pixels); escape this
** Then create a new blank image with the progress bar resolution, blit the background and after that the cropped foreground
** *Downside: Heavily resolution-dependent*; making it (mostly) resolution-independent requires (up)scaling textures first
* Using `lowpart` and `transform` to rotate: `+progress_bar_bg.png^[transformR90^[lowpart:progress_bar.png\^[transformR90:50^[transformR270+`
** First take the background (horizontal) and rotate it by 90° counterclockwise to orient it vertically
** Now do the same for the foreground, again to convert horizontal into vertical orientation; escape this (`+\^+`)
** Use the rotated foreground as argument to `lowpart`; blit `50` % of the vertical foreground on the vertical background
** Rotate everything by 270° to undo the vertical orientation
** *Advantage over using `combine`*: Due to the usage of `lowpart`, this is resolution-agnostic (will work with texture packs of different resolutions)
NOTE: For HUDs, ``statbar``s should be preferred over ``image``s using texture modifiers.