LabPBR Material Standard

From shaderLABS

This is a standard covering the material data storage in a single specular texture in resource packs as well as the decoding done by the shader pack.

This format has been created in the shaderLABS Discord (explaining the name) on the end of April 2019, and improved since. It is intended to replace all previous formats in order to achieve a more consistent support across different shader and resource packs.

In order to simplify the transition on the texture artists' end, a converter exists, allowing for an effortless transition from most older formats.

Specular Texture (_s)

Note that your shader pack is only required to correctly read the smoothness (red) and f0 (green) data to be LabPBR-ready. The other specified datasets are optional components. More details about that can be found in the LabPBR Implementation Requirements.

Red Channel

  • Represents "perceptual" smoothness.
    • Convert perceptual smoothness to linear roughness with roughness = pow(1.0 - perceptualSmoothness, 2.0)
    • Convert linear roughness to perceptual smoothness with perceptualSmoothness = 1.0 - sqrt(roughness)


For Texture Artists
A value of 255 (100%) results in a very smooth material (e.g. polished granite). A value of 0 (0%) creates a rough material (e.g. rocks).

Green Channel

  • Values from 0 to 229 represent F0, also known as reflectance.
    • This attribute is stored linearly. Please note that a value of 229 represents exactly 229 divided by 255, or approximately 90%, instead of 100%.
  • Values from 230 to 255 represent various different metals.
    • Details about this range is provided below.


For Texture Artists
This can be described as the minimal reflection strength of the material. So a low f0 value results in a less intense reflection when looking directly at the material while a higher f0 value causes a stronger and more visible reflection. Unlike traditional specularity however this is not a multiplier for the reflection strength, meaning that reflections at flat angles will always be stronger (called "Fresnel", more information about that here: Principle of the Fresnel Effect).

Blue Channel

  • On dielectrics:
    • Values from 0 to 64 represent porosity. Examples of the porosity effect can be found here.
    • Values from 65 to 255 represent subsurface-scattering.
    • Both porosity and subsurface-scattering are stored linearly.
  • On metals/conductors:
    • Reserved for future use.


For Texture Artists
The porosity value describes how much water a material can absorb. This manifests in the color of the material getting darker and less reflective when wet. This allows for a much more accurate behavior with shader packs supporting both porosity and weather based wetness (e.g. puddles). Below are some example values.

Material Porosity Value
Sand 64
Wool 38
Wood 12
Metals and other impermeable materials 0

Examples of the porosity effect in action can be found here.

Alpha Channel

  • It can have values ranging from 0 to 254; 0 being 0% emissiveness and 254 being 100%.
  • This is stored linearly.


For Texture Artists
The lower the value the less light it will emit, the higher the more luminescent it'll be, but never use a value of 255 since this will cause it to be ignored.

How metals work

In order to allow a more accurate representation of metals with the limited amount of information that can be provided, certain metals have been predefined and are selected by setting the green channel to specific values ranging from 230 to 254. In these cases, the albedo is used to tint the reflections instead of being used for diffuse shading.

If you want a metal that isn't among the predefined metals, you can also set the green channel to a value of 255. In this case, the albedo will instead be used as the F0. This is less accurate, but often still gives decent results.

Note that certain shader packs may not support these predefined metals, and will treat the entire range from 230 to 255 as though it had a value of 255.

Metal Bit Value N (R, G, B) K (R, G, B)
Iron 230 2.9114, 2.9497, 2.5845 3.0893, 2.9318, 2.7670
Gold 231 0.18299, 0.42108, 1.3734 3.4242, 2.3459, 1.7704
Aluminum 232 1.3456, 0.96521, 0.61722 7.4746, 6.3995, 5.3031
Chrome 233 3.1071, 3.1812, 2.3230 3.3314, 3.3291, 3.1350
Copper 234 0.27105, 0.67693, 1.3164 3.6092, 2.6248, 2.2921
Lead 235 1.9100, 1.8300, 1.4400 3.5100, 3.4000, 3.1800
Platinum 236 2.3757, 2.0847, 1.8453 4.2655, 3.7153, 3.1365
Silver 237 0.15943, 0.14512, 0.13547 3.9291, 3.1900, 2.3808

Reasoning for this Layout

For the red and green channels, it is fairly simple:

  • Red represents smoothness in most previous formats, and there isn't a good reason to change that.
  • Green represents metalness in most previous formats. Assigning values for partial metals, while not realistic, could often be used to somewhat control F0.

For the rest, we only had two channels left for emission, porosity, and subsurface-scattering, so two of them had to be stored in the same channel. We chose to have porosity and subsurface-scattering in the same channel.

Finally, placing emission in alpha allows the texture artist to easily make the emissive map separate from the rest and then merge it with _s as the alpha channel later, exactly like we do for parallax (POM) with _n.

Normal Texture (_n)

The normal map does not just contain the normals, but also ambient occlusion and a height map.

Material AO

  • Ambient occlusion gets stored in the blue channel of the normal map.
    • The z-component of the normal map can be calculated using sqrt(1.0 - dot(normal.xy, normal.xy))
  • This attribute is stored linearly; 0 being 100% AO and 255 being 0%.

Height Map

  • The height map used for POM is stored in the alpha channel of the normal map; a value of 0 (pure black on the height map) represents a depth of 25% in the block.
    • Be aware that a value of 0 on the height map will cause some issues with certain shader packs' POM implementations, so a minimum of 1 is recommended instead.

Reasoning for this Layout

The AO is stored in the blue channel because the first 3 components of a pixel in the normal map represent a vector of length 1. Since we know the length, we only need 2 of the 3 components to reconstruct the vector (thanks Pythagore). This means that there is one free channel in the normal map texture, it could have also been red or green, but blue was chosen, and is now used for AO.

Version History

LabPBR v1.3

  • f0 is now stored linearly
  • previously, linear f0 was stored by taking the square-root of it and decoded by squaring it


LabPBR v1.2

  • changed the way material AO is stored in the normal texture
  • previously, the material AO was encoded using this method:
    • the normal map is brought into the range of -1.0 to 1.0 and normalized afterwards
    • the material AO, which is stored in sqrt() in a range of 17 to 255 gets multiplied into this
    • afterwards, the normal map is brought back into the range of 0.0 to 1.0
    • the AO and normals are decoded like this after bringing the normal map into the range of -1.0 to 1.0:
      • normals = normalize(normalTexture.rgb)
      • ao = length(normalTexture.rgb)


LabPBR v1.1

  • added specification for hardcoded metals
  • reorganized emission, SSS and porosity in order to distribute the available precision based on importance and usage


LabPBR v1.0

  • initial specification