Skip to content

dh.texture

Extension: .dh-tex
Type ID: dh.texture

Texture definitions describe compile-time texture operations. The compiler processes them during dh build; runtimes just load the pre-baked results.

Two modes: channel packing (pack) or single source (source). They’re mutually exclusive.

Textures are consumed by dh.material assets (via their textures channels). A dh.map does not reference textures directly — its material slots reference dh.material assets, which in turn reference textures.

PropertyTypeRequiredDescription
$type"dh.texture"noType identifier
namestringnoDisplay name
descriptionstringnoDescription
packobjectone ofChannel packing map (mutually exclusive with source)
sourcestringone ofSingle source texture path (mutually exclusive with pack)
size[w, h]noTarget dimensions. Omit to keep original size.
filterstringnoFilter mode: "point", "bilinear", "trilinear". Default: platform default.
wrapstringnoWrap mode: "repeat", "clamp", "mirror". Default: platform default.
sRGBboolnoColor space. true (default) = sRGB color, decoded to linear on sample. false = raw linear DATA texture (normal / roughness / metallic / occlusion / height / mask).

The type is inferred from the file extension, so $type is not needed in source files. The compiler adds it automatically during builds.

Combine channels from multiple source textures into a single output. Classic use case: ORM (Occlusion, Roughness, Metallic) packing.

body-orm.dh-tex
{
"name": "Body ORM",
"pack": {
"r": "textures/occlusion.png@r",
"g": "textures/roughness.png@r",
"b": "textures/metallic.png@r"
}
}

Each key is an output channel (r, g, b, a). Each value is either a texture reference (with optional @channel suffix) or a constant number (01). A key that is not one of r, g, b, a is a compile error, as is a constant outside [0, 1]. A texture reference given without an @channel suffix contributes its red channel.

Prefix the channel letter with ! to invert the extracted value (1 − x, byte-wise 255 - x) before it’s packed. This is the porting case that comes up constantly: a Unity _MetallicGlossMap stores metallic in R and smoothness in A, while DigitalHeaven’s metallicRoughness wants roughness in G and metallic in B. The channel move is a plain pack; the smoothness→roughness step needs the invert:

body-metallic-roughness.dh-tex
{
"name": "Body Metallic-Roughness",
"pack": {
"g": "textures/body-metallicGloss.png@!a",
"b": "textures/body-metallicGloss.png@r"
}
}

@!a reads smoothness from alpha and inverts it into roughness; @r reads metallic unchanged. See Porting from Unity.

Invert only applies to texture references — a constant pack value is already whatever number you wrote, so just write 1 - x yourself. A malformed invert (e.g. @!x, an invalid channel letter) is a compile error, same as any other malformed @channel reference.

Defaults for unspecified channels:

  • RGB channels → 0 (black)
  • Alpha channel → 255 (fully opaque)

Pack channels can be set to a constant intensity using a number from 0 to 1:

ValueByteMeaning
00Black / fully transparent
0.5128Mid-gray / half opacity
1255White / fully opaque

This is useful when you need a solid channel without creating a source texture:

nipple-base.dh-tex
{
"name": "Nipple base texture",
"pack": {
"r": 1,
"g": 1,
"b": 1,
"a": "body-extensions/nipples/mask.png@r"
}
}

White RGB with alpha from a mask. No separate white texture file needed.

Process a single texture, whether to resize it or just reference a source:

body-diffuse-1k.dh-tex
{
"name": "Body Diffuse (1K)",
"source": "textures/body-diffuse.png",
"size": [1024, 1024]
}

If only one dimension is given in size, the other is calculated to preserve the aspect ratio.

Sources are decoded by content sniffing, not by extension, so the common raster formats all read. What the extension does decide is what happens afterwards:

ExtensionBuild-time mip chainStorage in the pallet
.png, .jpg, .jpegYes — the full chain is generated at build and uploaded in one shotStored as-is (already compressed)
.gif, .webpNo — mips are generated at load insteadStored as-is
Anything elseNoDeflate-compressed into the pallet

A dh.texturepack or source — always compiles to a .png named after the definition, so a material references myTexture.png even though the source file was something else. Prefer PNG (or JPEG for photographic sources) for anything you want mipped at build time; the mip path is the one that stops distant surfaces shimmering.

The @channel suffix extracts a single channel from a source texture. This works anywhere texture paths are used — in pack values, in source, and in material texture references. Prefix the channel letter with ! to invert the extracted value (1 − x) — see Inverting a Channel.

SyntaxResult
textures/packed.pngFull RGBA texture
textures/packed.png@rRed channel as grayscale
textures/packed.png@gGreen channel as grayscale
textures/packed.png@bBlue channel as grayscale
textures/packed.png@aAlpha channel as grayscale
textures/packed.png@!rRed channel as grayscale, inverted (1 − x)

Works with cross-pallet barcodes too:

io.mltn.base:textures/packed-orm.png@r

The @ separator was chosen because it can’t appear in file paths and doesn’t conflict with : used for pallet references. The ! invert marker sits directly in front of the channel letter (@!r, not @r!) so it reads as “not red” rather than as part of a compound suffix.

Textures can specify filter mode and wrap mode to control how they’re sampled at runtime. These settings work with both pack and source modes.

Controls how the texture is interpolated when sampled:

ValueAliasesDescription
"point""nearest"Nearest-neighbor filtering. Sharp pixels, no interpolation. Best for pixel art.
"bilinear"Smooth interpolation between pixels (platform default).
"trilinear"Bilinear with mipmap blending.

Controls what happens when UV coordinates go outside [0, 1]:

ValueDescription
"repeat"Tiles the texture (platform default).
"clamp"Clamps to edge pixels.
"mirror"Mirrors the texture at boundaries.
sprite.dh-tex
{
"source": "textures/sprite.png",
"filter": "point",
"wrap": "clamp"
}

When omitted, the platform default is used (typically bilinear filtering and repeat wrapping).

The engine renders in a linear color pipeline: lighting math runs in linear light and the swapchain re-encodes to sRGB on present. A texture’s sRGB flag tells the GPU how to interpret its texels when sampling:

ValueMeaningUse for
true (default)sRGB color — texels are decoded sRGB→linear on every sample.Base color / albedo, and any texture that holds a color a human picked.
falseLinear data — texels are sampled verbatim, with no transfer.DATA maps whose values are not colors: normal, roughness, metallic, occlusion, height, and mask textures.

This mirrors the “sRGB” checkbox in Unity’s texture importer. Leaving it unset is correct for color textures — only turn it off for data maps, otherwise their values get silently gamma-shifted and light wrong.

rock_normal.dh-tex
{
"source": "textures/rock_normal.png",
"sRGB": false
}

Under the hood the compiler stamps a colorSpace token onto the compiled entry only for linear (data) textures; color textures (the default) leave it off, and at runtime a texture with no token is treated as sRGB color. sRGB textures upload as R8G8B8A8Srgb (GPU decodes on sample); linear textures upload as R8G8B8A8Unorm.

Plain texture files (.png, .jpg, etc.) can carry import settings through a sidecar file without needing a full .dh-tex definition. Name the sidecar by appending .dh-tex-settings to the texture filename:

  • Directorytextures/
    • sprite.png
    • sprite.png.dh-tex-settings

The sidecar is a JSON file with the same filter, wrap, and sRGB properties:

sprite.png.dh-tex-settings
{
"filter": "point",
"wrap": "clamp"
}
rock_normal.png.dh-tex-settings
{
"sRGB": false
}

Sidecar files are consumed during compilation and don’t appear in the compiled pallet. The settings are baked into the pallet’s file metadata.

When to use which:

  • .dh-tex — when you also need channel packing, resize, or other compile-time processing
  • .dh-tex-settings sidecar — when you just need import settings on an existing texture file
body-orm.dh-tex
{
"name": "Body ORM",
"pack": {
"r": "textures/body-ao.png@r",
"g": "textures/body-roughness.png@r",
"b": "textures/body-metallic.png@r"
}
}
body-diffuse-mobile.dh-tex
{
"name": "Body Diffuse (Mobile)",
"source": "textures/body-diffuse-4k.png",
"size": [512, 512]
}
decal.dh-tex
{
"name": "Decal with Transparency",
"pack": {
"r": "textures/decal-color.png@r",
"g": "textures/decal-color.png@g",
"b": "textures/decal-color.png@b",
"a": "textures/decal-mask.png@r"
}
}
pixel-sprite.dh-tex
{
"name": "Pixel Sprite",
"source": "textures/sprite-16x16.png",
"filter": "point",
"wrap": "clamp"
}

Point filtering preserves sharp pixel edges, no blurry interpolation.