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.
Rendering Pipelines
Section titled “Rendering Pipelines”DigitalHeaven.Unity supports all three Unity rendering pipelines:
| Pipeline | Default Shader |
|---|---|
| Built-in | Standard |
| URP | Universal Render Pipeline/Lit |
| HDRP | HDRP/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):
RenderPipelineDetector.Override(RenderPipeline.URP);Built-in (Standard)
Section titled “Built-in (Standard)”The legacy rendering pipeline, using the Standard shader.
| Semantic Property | Shader 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).
URP (Universal Render Pipeline)
Section titled “URP (Universal Render Pipeline)”Uses the Universal Render Pipeline/Lit shader.
| Semantic Property | Shader 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.
HDRP (High Definition Render Pipeline)
Section titled “HDRP (High Definition Render Pipeline)”Uses the HDRP/Lit shader.
| Semantic Property | Shader 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).
Metallic and smoothness
Section titled “Metallic and 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.
Texture memory
Section titled “Texture memory”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.
Materials
Section titled “Materials”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.HostWaterMaterialto 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):Property From 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 whenfoamWidthMetersis 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_BaseColorthe 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.
Diagnostics
Section titled “Diagnostics”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.
Mesh Regions
Section titled “Mesh Regions”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.
Humanoid Rig
Section titled “Humanoid Rig”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:
| Group | Bones |
|---|---|
| Core | Hips, Spine, Chest, Neck, Head |
| Arms | LeftUpperArm, LeftLowerArm, LeftHand, RightUpperArm, RightLowerArm, RightHand |
| Legs | LeftUpperLeg, 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.
Humanoid Avatars on IL2CPP
Section titled “Humanoid Avatars on IL2CPP”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:
| Unity | Games | Object arguments | Returned Avatar | Validity and Animator.avatar icalls |
|---|---|---|---|---|
| 2023.2 x64 | MegaBonk | native object pointer | Unity GC handle | get_isValid_Injected, set_avatar_Injected |
| 2021.3 x64 | BONELAB | IL2CPP managed object | IL2CPP managed object | get_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.
Blend Shape Import
Section titled “Blend Shape Import”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:
| Value | Behavior |
|---|---|
All (default) | Every morph target of every model |
Referenced | Only 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.
Spring Bones
Section titled “Spring Bones”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’sRadiusand each collider’s radius scale with the avatar root.CollisionPassesandSweepare host settings, defaulting to the engine’sclient.anim.springCollisionPassesandclient.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 resolveddh.springCollidercomponents 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. AnendpointPositiontakes 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.
Eyes and Blinks
Section titled “Eyes and Blinks”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,
lookUpandlookDownshapes, 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
LateUpdateunless the host setsHostSteppedand callsStepafter 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, afterArtRig.ArtOutputLateUpdate). - Heads.
OfferHeadstakes Core’sAvatarLookTargets, 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) andRevivedopens it; the defaults live in Core’sAvatarGameEventDefaults, one table a later override can replace. - Clones of a bound look.
DigitalHeavenEyeLooks.AttachLikegives 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.
CopyToreturns aFaceCopythat 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.
Development
Section titled “Development”The library project is at Unity/DigitalHeaven.Unity/ in the repository. It targets netstandard2.0, netstandard2.1, net8.0, and net10.0.
Unity Project Deployment
Section titled “Unity Project Deployment”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 Project | Target Framework |
|---|---|
Unity/Sandboxes/DigitalHeaven.Unity-2021.1-Project | netstandard2.0 |
Unity/Sandboxes/DigitalHeaven.Unity-6-Project | netstandard2.1 |
DLLs are deployed to Assets/Plugins/DigitalHeaven/ within each project. To disable deployment, set the UnityDeploymentEnabled MSBuild property to false.