Skip to content

Unity

DigitalHeaven.Unity is the runtime integration library for Unity-based games and applications. It handles model loading, rig setup, material creation, and rendering pipeline adaptation at runtime.

It depends on DigitalHeaven.Core and ships as pre-compiled DLLs with no external dependencies — drop them into a Unity project or mod loader and they work. Its existing Unity-native runtime path remains separate from the portable DigitalHeaven.Content decoder composition. The optional DigitalHeaven.Unity.Content bridge targets netstandard2.1: it translates the VGltf parser’s CPU data into Content’s shared topology builder, decodes ordinary images through the host Unity runtime, and feeds folded material provenance into the same Unity material emitter used by existing consumers. It references only DigitalHeaven.Content and DigitalHeaven.Unity; SharpGLTF, StbImageSharp, and DigitalHeaven.Content.Decoders do not enter the Unity runtime graph. The bridge does not raise DigitalHeaven.Unity’s netstandard2.0 minimum.

For importing compiled pallets into the Unity Editor as native assets (materials, prefabs, meshes), see Unity Editor. That importer remains Unity-native. DigitalHeaven.Unity.Content now provides the bounded additive scene, spawn, cancellation, and unload seams used by the Schedule I integration, building a map as its source hierarchy (one GameObject per node, each mesh node with its own mesh, renderer and collider, materials shared per map); support remains host-composed rather than an automatic map loader in every Unity consumer. The catalog (which maps are installed, and one read into scene content) is shared too, so BONELAB, an IL2CPP game, offers the same maps as levels with the same builder, uploading meshes through IUnityPlatform and decoding images with StbImageSharp.

DigitalHeaven.Unity supports all three Unity rendering pipelines:

PipelineDefault Shader
Built-inStandard
URPUniversal Render Pipeline/Lit
HDRPHDRP/Lit

The active pipeline is auto-detected at runtime via RenderPipelineDetector. Detection checks the project’s GraphicsSettings render pipeline asset first, then falls back to shader probing. You can also override detection manually (e.g. in modding scenarios where reflection-based detection may not work):

DigitalHeaven.Unity.Rendering.RenderPipelineDetector
RenderPipelineDetector.Override(RenderPipeline.URP);

The legacy rendering pipeline, using the Standard shader.

Semantic PropertyShader Property
baseColor_MainTex
normal_BumpMap
metallicRoughness_MetallicGlossMap
occlusion_OcclusionMap
emissive_EmissionMap
tint_Color

The Built-in Standard shader uses a metallic-smoothness workflow. DigitalHeaven’s roughness property is automatically inverted to smoothness (_Glossiness), and its metallicRoughness texture is converted on upload (see Metallic and smoothness).

The Standard shader’s rendering mode is controlled via the _Mode property, which can be set through platform overrides. This accepts "opaque", "cutout", "fade", or "transparent". Platform overrides are the only way to select "fade" specifically (which fades out specular and reflections along with the surface), since alpha mode auto-inference maps to "transparent" (preserving specular).

Uses the Universal Render Pipeline/Lit shader.

Semantic PropertyShader Property
baseColor_BaseMap
normal_BumpMap
metallicRoughness_MetallicGlossMap
occlusion_OcclusionMap
emissive_EmissionMap
tint_BaseColor

URP Lit uses a metallic-smoothness workflow similar to Built-in but with different property names. Roughness is inverted to _Smoothness, and the metallicRoughness texture is converted the same way.

Uses the HDRP/Lit shader.

Semantic PropertyShader Property
baseColor_BaseColorMap
normal_NormalMap
metallicRoughness_MaskMap
occlusion_OcclusionMap
emissive_EmissiveColorMap
tint_BaseColor

HDRP packs metallic, occlusion, and smoothness into a single _MaskMap texture (R: metallic, G: occlusion, B: detail mask, A: smoothness).

DigitalHeaven’s metallicRoughness is packed the glTF way (roughness in G, metallic in B), which no Unity Lit shader reads. When the material is on the pipeline’s own shader, the loader converts the texture on upload to Unity’s layout: metallic in R, smoothness in A, and G and B at 1 for HDRP’s occlusion and detail mask. Unity’s Lit reads metallic from the map alone and only scales its smoothness, so the material’s metallic and roughness scalars are baked into the converted texture, and _Metallic and _Smoothness stay 1. A material on a host game’s own shader gets the texture unconverted, in DigitalHeaven’s layout, for the mod to repack.

Without a map, metallic and roughness take the engine’s defaults when the material does not author them (metallic 0, roughness 1), so an unauthored surface is fully rough, not URP’s default smoothness of 0.5.

A material’s texture is read from the pallet its reference names: a local path (textures/a.png) from the pallet of the material that declares the channel, and a qualified one (other.pallet:textures/a.png) from that pallet. Each texture is uploaded once per session and shared by every material and avatar that binds it. No uploaded texture keeps a CPU copy: the copy is dropped once the pixels are on the GPU, or after the block compression a host asked for, so a host that reads one back does so through a GPU blit.

Map geometry casts two-sided shadows (ShadowCastingMode.TwoSided), as the engine’s shadow pass culls nothing. A map’s brushes are not reliably closed, and a face culled from the light’s side would let the sun through a wall. A renderer whose materials are all water casts none. The map bridge does not apply baked lightmaps: a map that asks for them loads with its real-time lights and ambient only.

A map’s object instance records (the props a Source map import places, for one) are built from the object each one names, resolved by Content’s object loader as the engine resolves it. Each object is loaded and uploaded once, and every instance of it is a GameObject at the instance’s position, rotation and scale whose children draw that shared mesh with the object’s materials (a sub-mesh the object binds no material to draws with a plain default one), casting two-sided shadows like the rest of the map. An object with a dh.physicsProp body gets a static BoxCollider of that size, unstretched by the instance’s scale as in the engine; nothing simulates it. An object without a body gets no collider, again as in the engine, so imported Source props, which carry none, can be walked through. A disabled instance is not built, and an instance parented to another entity is not built and is reported, since the bridge does not build the entity hierarchy.

DigitalHeaven materials use semantic properties (baseColor, normal, roughness, etc.) that are automatically mapped to the correct shader properties for the active pipeline. See dh.material for the full material definition reference.

Alpha modes (opaque, mask, blend) are applied automatically, including the tint alpha fallback (a texture’s alpha channel never picks a mode). For pipeline-specific customization beyond what semantic properties offer, use platform overrides — files placed in platforms/unity/ that can set raw shader properties, custom shaders, and rendering modes. See Unity Platform Overrides.

A game’s build ships only the shader variants its own materials use, so a DH-built material on Standard or Lit can hit a stripped variant. Mods that render through the game’s own shader first read the built material back with MaterialReadback.Read(material). That returns its semantic surface, whatever shader it landed on:

  • base map, with its scale and offset
  • base color
  • normal map
  • emission map and color
  • cull mode
  • alpha surface (opaque, cutout or transparent), with premultiplication and the cutoff
  • render queue

Each mod keeps only its own target mapping. MegaBonk writes the surface onto MK.Toon, and ULTRAKILL writes it onto ULTRAKILL/Master.

A water material (the water shader, or a water block) has no base channels, so an ordinary Lit would draw it as an opaque white sheet. The loader reads the block’s medium instead, through the same Beer-Lambert law the engine shades with (UnityWaterLook):

  • Host template. A mod sets MaterialLoader.HostWaterMaterial to a function that returns the game’s own water material. Each DH water material is a clone of it, with these set where the template’s shader has the property (the names are Stylized Water 2’s):

    PropertyFrom the block
    _BaseColor / _DeepColorThe color the body settles to far from the eye: its scatter times its sediment color times its hue. Black for clear water, as in the engine.
    _ShallowColorThe body’s hue, with the opacity of 1 m of it.
    _DepthVerticalHow deep the clearest channel goes before 5% of it is left.
    _FoamColor, _IntersectionColorfoamColor. The shoreline color is transparent when foamWidthMeters is 0.
    _WaveHeight0. A map’s water mesh is not tessellated, so waves would only tilt the whole sheet.

    Waves, chop, flow, the index of refraction, dispersion, caustics and the thin film are not mapped, and the log names them once. The template keeps its own normal animation, reflections and foam texture.

  • Fallback. With no template, the material stays on the pipeline’s Lit, transparent, with roughness 0.05 and _BaseColor the medium seen through 2 m of water: the hue blended toward the deep color, at the opacity 2 m gives. Lit has no depth to read, so a deep pool and a shallow one look the same.

A Unity platform override that names its own shader takes the material off both paths.

DiagnosticSettings.LoggingEnabled turns on the library’s log lines. They go to Unity’s Debug.Log by default, which some release builds filter out. A host can set DiagnosticSettings.Sink to receive every line with its LogType instead, for example to write them to the mod loader’s log.

SkinnedMeshRegion.Extract(renderer, bones) returns a copy of a skinned mesh that keeps only the triangles whose vertices are weighted entirely to the given bones, such as an arm-only viewmodel. SkinnedMeshRegion.Subtrees collects every bone under a set of roots. The mesh must be CPU-readable, which meshes loaded from pallets are.

The steps are public for other cuts. Read loads a mesh’s bone influences, triangles and positions, and Cut copies the mesh with only the triangles of kept vertices. Pieces labels connected pieces, treating coincident seam vertices as one point. SidedPieces finds the pieces confined to one side’s bones, and IsMirrored reports a piece with a twin of the same vertex count on another side. The MegaBonk mod uses them to keep props modeled into a game character’s hand. Mesh access goes through IUnityPlatform, so the same cut runs on Mono and IL2CPP.

The Unity library creates a Unity Avatar and Animator from dh.rig definitions. A valid humanoid avatar requires the following 17 bones to be mapped:

GroupBones
CoreHips, Spine, Chest, Neck, Head
ArmsLeftUpperArm, LeftLowerArm, LeftHand, RightUpperArm, RightLowerArm, RightHand
LegsLeftUpperLeg, LeftLowerLeg, LeftFoot, RightUpperLeg, RightLowerLeg, RightFoot

Optional bones (shoulders, fingers, toes, face, etc.) are used when present. See dh.rig — Standard Bones for the full list of all 56 supported bones.

IL2CPP games often strip AvatarBuilder, or ship an interop wrapper that marshals HumanDescription wrongly. Il2CppUnityPlatform.CreateHumanoidAvatar therefore calls the native AvatarBuilder::BuildHumanAvatarInternal_Injected icall itself, with real IL2CPP HumanBone and SkeletonBone arrays. What differs between Unity versions lives in one per-version layout table (Il2CppHumanoidLayout), picked from Application.unityVersion:

UnityGamesObject argumentsReturned AvatarValidity and Animator.avatar icalls
2023.2 x64MegaBonknative object pointerUnity GC handleget_isValid_Injected, set_avatar_Injected
2021.3 x64BONELABIL2CPP managed objectIL2CPP managed objectget_isValid, set_avatar

The struct layouts are the same in both versions: HumanLimit is 44 bytes, HumanBone 64, SkeletonBone 56 and HumanDescription 64. Before its first build, the builder checks every HumanDescription offset and each struct size against the running game’s metadata. A mismatch, a missing icall or an unknown Unity version logs a [HumanoidAvatar] line and returns no avatar instead of calling native code with a guessed layout. Il2CppUnityPlatform.SetAnimatorAvatar uses the same table.

Each glTF morph target becomes a Unity blend shape named from the mesh’s extras.targetNames (BlendShape_<n> when unnamed). A mesh that has normals gets per-shape normal deltas recomputed from the deformed positions.

Which shapes an avatar build imports is a host policy, DigitalHeavenUnity.BlendShapeImport:

ValueBehavior
All (default)Every morph target of every model
ReferencedOnly the shapes the avatar looks up by name: the shapes of every dh.renderer in its resolved object (added records included) and its rigs’ visemes and eyelids, plus every shape the mouth resolution could use (viseme names, MMD vowels, a mouth-open shape). A model nothing references keeps only those

The names are collected before the first model imports. Built meshes are cached per model and shared between avatars. Each cached mesh records which shapes it carries:

  • A cached mesh that already has every needed shape is reused as is.
  • When shapes are missing, the build copies the closest cached mesh and adds only the missing shapes. A cached mesh is never modified.
  • A shared mesh can carry extra shapes. Consumers look shapes up by name on the mesh they are bound to, so blend shape indices can differ between two avatars built from the same model.

MegaBonk opts into Referenced. Other hosts keep All.

Every Unity mod runs dh.springBone chains on one component, DigitalHeavenSpringBone, which shares the engine solver’s arithmetic through DigitalHeaven.Core rather than repeating it:

  • The tick. Whole 1/64 s ticks from the shared clock. A tick lands before the frame’s end, so the chain’s anchor and every collider are wound back along their drawn velocity to where that tick happened.
  • The drawn pose. A frame shows the last two tick states carried onto where the anchor is drawn now and blended by how far past the last tick the clock stands, segment by segment (SpringBoneDrawnPose). A chain does not drift off a body that moved on a frame with no tick, and does not step at 64 Hz on a faster screen.
  • Colliders. A chain is pushed out of the colliders it lists inside every tick, by the engine’s own push (SpringBoneCollision), sweep included. The colliders live on the chain’s component as parallel arrays (ColliderBones, ColliderShapes, ColliderStarts, ColliderEnds, ColliderRadii), each point in its bone’s own space; the chain’s Radius and each collider’s radius scale with the avatar root. CollisionPasses and Sweep are host settings, defaulting to the engine’s client.anim.springCollisionPasses and client.anim.springSweep.
  • The import. The runtime and the editor pipeline attach chains through one SpringImport. It binds colliders once the whole tree is built, laying the avatar’s resolved dh.springCollider components over its fitted colliders as the engine does. A collider’s offsets are authored in the model’s glTF frame and read through the same Z mirror and half turn the model import applies (GltfUnityFrame), so one authored 1 cm in front of the chest stays in front. An endpointPosition takes the same mirror.

Under IL2CPP a clone keeps none of a component’s managed fields. DigitalHeavenSpringBoneSettings.Copy copies every field that holds no transform, and SpringBoneCloning.Rebind maps the chain and collider bones into the clone by sibling path. A collider whose bone has no counterpart is dropped with a warning; the chain keeps the rest. A test enumerates the component’s public fields, so a new one cannot go missing from clones. Each chain’s collider history is private solver state, rebuilt on its first tick.

Every Unity mod runs the rig’s eye look-at on one component, DigitalHeavenEyeLook, which hosts Core’s EyeLookBrain, the same brain the engine steps, and turns its angles into eye bone rotations through Core’s limit poses (EyeLookPoses). The tuning is DigitalHeavenEyeLooks.Settings; Core’s EyeLookSettingFields.All lists every field with its range, the table the engine’s client.anim.* preferences come from, so a mod exposing it offers the engine’s names and defaults.

  • The eyelid mesh. The rig’s target mesh, or, when the rig names none, the skinned mesh under the avatar carrying the most of its blink, lookUp and lookDown shapes, as the engine resolves it.
  • The report. One [Face] log line per avatar names the eye bones, or why they stay still, and the shapes with their mesh, with near names for any the mesh lacks.
  • The step. The component steps in its own LateUpdate unless the host sets HostStepped and calls Step after its game writes the pose; an IL2CPP-injected component’s execution order is not honored, so that is how a host runs after the game (BONELAB does, after ArtRig.ArtOutputLateUpdate).
  • Heads. OfferHeads takes Core’s AvatarLookTargets, the set the engine gathers its players’ heads into, and offers the nearest few as faces, each marked when it looks back.
  • Game events. DigitalHeavenEyeLooks.Report(eyeLook, AvatarGameEvent.Died) closes the face (lids held shut, eyes at rest, no brain) and Revived opens it; the defaults live in Core’s AvatarGameEventDefaults, one table a later override can replace.
  • Clones of a bound look. DigitalHeavenEyeLooks.AttachLike gives a clone of an object the eye look already resolved inside it, mapped by sibling path with a brain of its own, for a body the template no longer maps onto.
  • Copies. CopyTo returns a FaceCopy that carries the eye rotations and eyelid weights onto a same-shaped copy (a mirror reflection, a posed copy), so the copy shows the body’s face rather than a second brain’s.

The library project is at Unity/DigitalHeaven.Unity/ in the repository. It targets netstandard2.0, netstandard2.1, net8.0, and net10.0.

A shared build target in Directory.Build.targets automatically copies compiled DLLs to local Unity test projects after building DigitalHeaven.Unity. Each Unity project is mapped to a target framework:

Unity ProjectTarget Framework
Unity/Sandboxes/DigitalHeaven.Unity-2021.1-Projectnetstandard2.0
Unity/Sandboxes/DigitalHeaven.Unity-6-Projectnetstandard2.1

DLLs are deployed to Assets/Plugins/DigitalHeaven/ within each project. To disable deployment, set the UnityDeploymentEnabled MSBuild property to false.