Workspace
Your DigitalHeaven workspace (default: Documents/DigitalHeaven) contains source pallets, compiled output, configuration, and cache data. It’s shared across all tools — Studio, CLI, and game mods.
Layout
Section titled “Layout”DirectoryDigitalHeaven/
DirectorySource/ Your source pallets live here
Directorydev.example/
Directoryavatars/
Directorymayu-custom/ Pallet ID:
dev.example.avatars.mayu-custom- pallet.dh Pallet manifest
- mayu-custom.dh-avatar Avatar definition
Directorytextures/
- base-color-1001.png
- base-color-1002.png
Directorymaterials/
- mayu-custom-body.dh-mat
- mayu-custom-head.dh-mat
Directoryaccessories/
Directoryround-glasses/
- round-glasses.dh-obj
- model.fbx
Directorytech.azuki/
Directoryavatars/
Directorymayu/ Pallet ID:
tech.azuki.avatars.mayu- pallet.dh
- mayu-tora.db-avatar
Directorymodels/
- body-tora.fbx
Directoryrigs/
- tora.dh-rig
Directorytextures/
- tora-base-color-1001.png
- tora-base-color-1002.png
Directorymaterials/
- mayu-body.dh-mat
- mayu-head.dh-mat
DirectoryPallets/ Compiled
.palletfiles- dev.example.avatars.mayu-custom.pallet
- tech.azuki.avatars.mayu.pallet
DirectoryScripts/ Compiler-managed scripts
- fbx_to_glb.py FBX→GLB conversion script (generated; rewritten on every build)
- fbx_to_glb.user.py Optional user override (absent by default; never touched by the compiler)
Directory.dh/ Compiler data and cache
Directoryfbx-cache/ Cached GLB conversions of FBX source files
- …
Directoryreceived/ Pallets received from other players via transfers
- …
Directorytemp/ Temporary build artifacts
- …
Directory.vscode/ Workspace settings
- …
- dh-config.jsonc Main configuration
- dh-studio.jsonc Studio preferences
- dh-friends.jsonc Friend list for pallet sharing
- .gitignore
- .gitattributes LFS tracking for binary assets
The workspace also includes a convenience script:
| Script | Command |
|---|---|
dh.bat / dh.sh | dh interactive |
This launches the interactive menu, where you can build, watch, validate, and more. See CLI: Interactive Mode for details.
If the script doesn’t work,
dhprobably isn’t in your PATH. See the Setup page.
Pallets are organized by reverse-DNS ID: dev.example.avatars.mayu-custom lives at Source/dev.example/avatars/mayu-custom/. See Pallets & Assets for details.
Customizing FBX Conversion
Section titled “Customizing FBX Conversion”Scripts/fbx_to_glb.py is compiler-managed — it’s rewritten from DigitalHeaven’s built-in template whenever that template changes, so fixes always reach you on your next build. Don’t edit it directly; your changes would be overwritten.
To customize the conversion instead, copy fbx_to_glb.py to fbx_to_glb.user.py in the same Scripts/ folder and edit the copy. The managed script checks for that file every time it runs and, if it’s there, hands the whole conversion to it — same arguments, same environment — instead of running its own logic. The compiler never creates, edits, or deletes fbx_to_glb.user.py; it only checks whether it exists, so your override survives every build. Deleting it goes back to the managed conversion.
The conversion cache keys on the content of whichever script actually ran, so editing your override (or removing it) reconverts affected FBX files on the next build, same as an update to the managed script would.
Configuration: dh-config.jsonc
Section titled “Configuration: dh-config.jsonc”Always loaded from the default workspace root (Documents/DigitalHeaven, or DH_WORKSPACE when set) — never from workspaceRoot itself. All properties are optional; omit them to use defaults.
| Property | Type | Default | Description |
|---|---|---|---|
workspaceRoot | string? | Documents/DigitalHeaven | Workspace root directory |
sourceDirectory | string? | {workspaceRoot}/Source | Source pallets location |
palletsDirectory | string? | {workspaceRoot}/Pallets | Compiled output location |
cacheDirectory | string? | {workspaceRoot}/.dh | Cache directory |
watchDebounceMs | int? | 1000 | Milliseconds a change must settle before it is acted on (source auto-compile, and compiled-pallet discovery) |
cookCacheBudgetBytes | long? | 4294967296 (4 GiB) | Byte budget shared by the cooked-collision (one entry per map cook), map-picture and model stand-in caches. Only swept when over budget, and only entries the current build is not using are ever deleted |
importCacheBudgetBytes | long? | 8589934592 (8 GiB) | Byte budget of the importers’ cache (import-cache/): the models and textures dh import converted, kept for every map and later import that uses the same files (What an import reuses). Swept the same way |
textureCacheBudgetBytes | long? | 34359738368 (32 GiB) | Byte budget of the compiler’s texture encode cache (texture-cache/), kept apart from the cook cache’s because one large imported pallet’s encodes outgrow it. Swept the same way |
proxyTriangles | int? | 512 | The most triangles a model’s compiled stand-in may carry. A model with fewer keeps its own; a larger one is simplified to at most this many. 0 ships bounds alone |
proxyBytes | int? | 6144 | The most bytes a model’s compiled stand-in may encode to, header included; a silhouette over it is simplified further |
textureEncode | { imported?, authored? } | { "imported": "fast", "authored": "balanced" } | The UASTC encode effort of the texture roles that default to balanced (how): imported for pallets an importer made, authored for the rest. Each is "fast", "balanced" or "best"; an unknown value warns and takes the default. Changing it re-encodes only the textures whose effort changed |
additionalSourcePaths | string[] | [] | Extra directories the compiler searches for source pallets (cross-pallet refs). |
additionalPalletDirectories | string[] | [] | Extra directories every host (engine, Unity runtime, game mods) discovers compiled .pallet files in. Relative entries resolve against workspaceRoot; a missing or unreadable entry is skipped with a warning naming it |
import | object? | unset | Settings of the importers that turn another game’s content into pallets. Today one setting: a cap on texture size, with overrides per game |
blenderPath | string? | auto-detect | Path to Blender executable, used to convert FBX to GLB |
enginePath | string? | unset | Path to DigitalHeaven.Engine.Host — the build shells out to it to photograph map thumbnails and to cook mesh-mode collision. Unset is not an error: the build warns, packs the PNG already beside the map, and ships mesh-mode maps without a cook, so they cook at load. When set, the engine’s bundled content folder next to it is also a compiled-pallet directory, so its built-in maps reach game mods with no other setup |
Example:
{ "watchDebounceMs": 500, "textureEncode": { "imported": "fast", "authored": "balanced" }, "additionalSourcePaths": [ "D:/SharedPalletSources" ], "additionalPalletDirectories": [ "D:/SharedPallets" ], "blenderPath": "C:/Program Files/Blender Foundation/Blender 4.0/blender.exe", "enginePath": "C:/DigitalHeaven/DigitalHeaven.Engine.Host.exe"}Import texture size
Section titled “Import texture size”dh import keeps every texture at the size the game stores it unless you opt in to a cap. The import block holds the cap and its per-game overrides:
{ "import": { "maxTextureSize": null, "games": { "com.facepunch.sbox": { "maxTextureSize": 1024 } } }}| Property | Type | Default | Description |
|---|---|---|---|
import.maxTextureSize | int? | unset | The longest side, in texels, an imported texture keeps. Unset, null and 0 cap nothing. A value that is not a power of two is rounded down to one |
import.games.<game>.maxTextureSize | int? | the default above | The cap for one game. 0 lifts the default for that game; a game with no size inherits the default |
A game is named by its platform ID or an alias (com.facepunch.sbox or sbox, as --game takes them) — the same ids the importers write into a pallet’s game and originGame metadata. The Source importer looks a map up by its originGame, so a Half-Life 2 map imported through Garry’s Mod takes Half-Life 2’s cap.
A texture whose longest side is over the cap is scaled down before it is written, both sides by the same factor so the aspect ratio holds and the longer side lands on the cap. The scaling is a Lanczos filter: a color texture is filtered in linear light, a mask, roughness or height as the numbers it is, and a normal map as vectors that are made unit length again. A texture that fits is not touched, byte for byte. Every kind of texture gets the same cap, and a texture made from capped ones (a metallic-roughness pack, an emission, a flipbook atlas) comes out capped too, so layers and masks stay aligned. The cap is part of the key the import cache files a texture under only when it changes the texture, so changing the cap redoes just the textures it touches and imports under different caps share one cache.
What the cap leaves alone, because the engine needs it at its exact size: lightmap atlases, sky images, reflection cubemaps and probe data, and high dynamic range textures (the cap filters 8-bit images). The import report and the console summary say the cap and how many textures it scaled down.
Compiled pallet directories
Section titled “Compiled pallet directories”Every host discovers compiled pallets through one shared list, in this order:
palletsDirectory(the workspace)- Folders the host ships itself (the engine’s own
content) additionalPalletDirectories, in file order- The
contentfolder next toenginePath - Pallets received from other players (
.dh/received)
A folder reached twice (say, the engine’s content listed by hand as well) is scanned once, at its first position; paths compare case-insensitively on Windows. When two folders hold a pallet with the same id, the one earlier in the list is the one you see, so a workspace build of a pallet shadows the engine’s bundled copy, and a received pallet never shadows anything installed locally. Every listed folder is watched, so a rebuilt pallet in any of them hot reloads.
Environment Variables
Section titled “Environment Variables”| Variable | Description |
|---|---|
DH_WORKSPACE | Override the workspace root directory. Takes priority over workspaceRoot in dh-config.jsonc. Useful for Proton/Wine where Documents resolves to the Wine prefix instead of the real user directory. |
File Extensions
Section titled “File Extensions”Definition files use custom .dh-* extensions. Each type has its own:
| Asset Type | Extension | Type ID |
|---|---|---|
| Pallet manifest | pallet.dh | — |
| Avatar | .dh-avatar | dh.avatar |
| Object | .dh-obj | dh.object |
| Material | .dh-mat | dh.material |
| Texture | .dh-tex | dh.texture |
| Rig | .dh-rig | dh.rig |
| Map | .dh-map | dh.map |
| Look | .dh-look | dh.look |
| Voxel | .dh-vox | dh.voxel |
| Block | .dh-block | dh.block |
See the Definition Types pages for what goes in each file.
File Format
Section titled “File Format”All definition files are JSONC, JSON with two extras:
- Comments:
// lineand/* block */ - Trailing commas: allowed in arrays and objects
{ // This is fine "name": "My Avatar", "tags": [ "custom", "v2", // trailing comma OK ],}Parsed with System.Text.Json. Relaxed on formatting, strict on validation. The compiler catches errors at build time with file paths, line numbers, and helpful messages.