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.materialassets (via theirtextureschannels). Adh.mapdoes not reference textures directly — its material slots referencedh.materialassets, which in turn reference textures.
Properties
Section titled “Properties”| Property | Type | Required | Description |
|---|---|---|---|
$type | "dh.texture" | no | Type identifier |
name | string | no | Display name |
description | string | no | Description |
pack | object | one of | Channel packing map (mutually exclusive with source) |
source | string | one of | Single source texture path (mutually exclusive with pack) |
size | [w, h] | no | Target dimensions. Omit to keep original size. |
filter | string | no | Filter mode: "point", "bilinear", "trilinear". Default: platform default. |
wrap | string | no | Wrap mode: "repeat", "clamp", "mirror". Default: platform default. |
sRGB | bool | no | Color 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
$typeis not needed in source files. The compiler adds it automatically during builds.
Channel Packing
Section titled “Channel Packing”Combine channels from multiple source textures into a single output. Classic use case: ORM (Occlusion, Roughness, Metallic) packing.
{ "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 (0–1). 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.
Inverting a Channel
Section titled “Inverting a 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:
{ "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)
Constant Values
Section titled “Constant Values”Pack channels can be set to a constant intensity using a number from 0 to 1:
| Value | Byte | Meaning |
|---|---|---|
0 | 0 | Black / fully transparent |
0.5 | 128 | Mid-gray / half opacity |
1 | 255 | White / fully opaque |
This is useful when you need a solid channel without creating a source texture:
{ "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.
Single Source
Section titled “Single Source”Process a single texture, whether to resize it or just reference a source:
{ "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.
Source formats and compiled output
Section titled “Source formats and compiled output”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:
| Extension | Build-time mip chain | Storage in the pallet |
|---|---|---|
.png, .jpg, .jpeg | Yes — the full chain is generated at build and uploaded in one shot | Stored as-is (already compressed) |
.gif, .webp | No — mips are generated at load instead | Stored as-is |
| Anything else | No | Deflate-compressed into the pallet |
A dh.texture — pack 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.
@channel Syntax
Section titled “@channel Syntax”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.
| Syntax | Result |
|---|---|
textures/packed.png | Full RGBA texture |
textures/packed.png@r | Red channel as grayscale |
textures/packed.png@g | Green channel as grayscale |
textures/packed.png@b | Blue channel as grayscale |
textures/packed.png@a | Alpha channel as grayscale |
textures/packed.png@!r | Red channel as grayscale, inverted (1 − x) |
Works with cross-pallet barcodes too:
io.mltn.base:textures/packed-orm.png@rThe @ 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.
Import Settings
Section titled “Import Settings”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.
Filter Mode
Section titled “Filter Mode”Controls how the texture is interpolated when sampled:
| Value | Aliases | Description |
|---|---|---|
"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. |
Wrap Mode
Section titled “Wrap Mode”Controls what happens when UV coordinates go outside [0, 1]:
| Value | Description |
|---|---|
"repeat" | Tiles the texture (platform default). |
"clamp" | Clamps to edge pixels. |
"mirror" | Mirrors the texture at boundaries. |
{ "source": "textures/sprite.png", "filter": "point", "wrap": "clamp"}When omitted, the platform default is used (typically bilinear filtering and repeat wrapping).
Color Space (sRGB)
Section titled “Color Space (sRGB)”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:
| Value | Meaning | Use 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. |
false | Linear 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.
{ "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.
Sidecar Files
Section titled “Sidecar Files”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:
{ "filter": "point", "wrap": "clamp"}{ "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-settingssidecar — when you just need import settings on an existing texture file
Examples
Section titled “Examples”ORM Pack
Section titled “ORM Pack”{ "name": "Body ORM", "pack": { "r": "textures/body-ao.png@r", "g": "textures/body-roughness.png@r", "b": "textures/body-metallic.png@r" }}Downscale for Performance
Section titled “Downscale for Performance”{ "name": "Body Diffuse (Mobile)", "source": "textures/body-diffuse-4k.png", "size": [512, 512]}RGBA with Alpha
Section titled “RGBA with Alpha”{ "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 Art with Point Filtering
Section titled “Pixel Art with Point Filtering”{ "name": "Pixel Sprite", "source": "textures/sprite-16x16.png", "filter": "point", "wrap": "clamp"}Point filtering preserves sharp pixel edges, no blurry interpolation.