Rendering Pipeline and Glow Implementation¶
A. Render pipeline identification¶
Stationeers uses Unity's built-in render pipeline on Unity 2021.2 LTS (same toolchain as ../Patterns/AsyncEnumerator472.md).
Key evidence:
- Bloom post-processing: the third-party UltimateBloom asset is attached via
CameraEffectCollection.Bloom. TheCameraController.SetBloom(bool)API toggles it. Enabled by default on vanilla gameplay cameras. - No URP / HDRP: only Legacy Shaders and built-in-pipeline shader references appear in the decompile.
- Camera post-processing uses the old
UnityStandardAssets.ImageEffectsfamily, not the newer Post-Processing V2 stack.
UltimateBloom is what turns bright emissive pixels into a visible halo. Without bloom, an emissive material renders self-lit but produces no glow halo. Any glow feature for painted objects depends on the camera's bloom pass being on. The runtime read accessor is CameraController.Instance.CameraEffects[0].Bloom; check .enabled to confirm the component is active. See ../GameClasses/CameraController.md section "Singleton and effect collections" for the full accessor chain.
B. Shaders and emission on paintable objects¶
Every ColorSwatch in GameManager.CustomColors can ship up to two materials per color (see ../GameClasses/ColorSwatch.md):
ColorSwatch.Normal- standard diffuse, always set on live swatches.ColorSwatch.Emissive- pre-baked emissive material, OPTIONAL: some swatches ship withEmissive == nulland cannot be rendered in emissive mode via the material swap.
The game's shader system respects the _EmissionColor property. From the Thing decompile:
public static readonly int EMISSION_COLOR = Shader.PropertyToID("_EmissionColor");
EmissionColor = Color.white * (emissive ? 1f : 0f);
foreach (ThingRenderer renderer in Renderers)
{
renderer.SetShaderVectorProperty(EMISSION_COLOR, EmissionColor);
}
On every SetCustomColor call, the game drives the renderer with two independent signals: the material-swap (via CustomColorMapping.SetEmissive(material) on colors that have an Emissive variant) and the shader property write (_EmissionColor on every ThingRenderer). The property write happens unconditionally regardless of whether the swatch has an Emissive material.
Mod code that wants to read the post-call renderer state goes through ThingRenderer.Materials / sharedMaterials / GetMaterial() on each entry in Thing.Renderers; see ../GameClasses/ThingRenderer.md for the accessor shapes and null-safety caveats.
C. How existing glowing things work in-game¶
Vanilla uses the ColorSwatch.Emissive + _EmissionColor approach for ChemLights and RoadFlares only (see ../GameClasses/ColorSwatch.md section "Vanilla callers of SetCustomColor with emissive: true"). When a flare is toggled on, OnInteractableUpdated calls SetCustomColor(index, emissive: true); the renderer picks up the Emissive material (if present) and gets _EmissionColor = Color.white. UltimateBloom renders the halo.
No per-object point lights are used for this effect. The halo is entirely shader-driven.
Painted structures (walls, pipes, cables, frames) never receive emissive: true in vanilla. The API supports it but no caller invokes it; that is the gap SprayPaintPlus would fill.
D. Mod prior art: PowerTransmitterPlus beam visuals¶
PowerTransmitterPlus renders its power beam with a LineRenderer using Legacy Shaders/Particles/Additive:
var shader = Shader.Find("Legacy Shaders/Particles/Additive")
?? Shader.Find("Particles/Additive")
?? Shader.Find("Sprites/Default")
?? Shader.Find("Hidden/Internal-Colored");
Additive shading produces bloom-friendly brightness, so the beam appears to glow under UltimateBloom. Beam color is set via startColor / endColor; no _EmissionColor manipulation is used. This is a proof point that runtime-created additive materials work under the game's rendering and bloom into a visible halo.
E. Material-per-instance pattern¶
For per-instance material mutation (glow intensity per-Thing, custom emission color per-paint), follow ../Patterns/UnityMaterialPerInstance.md:
- Read
renderer.materialonce to trigger the clone; assignment to subsequent renderers on the same GameObject can share. - Cache the cloned
Materialreference (e.g. keyed byThing.ReferenceId). - Mutate via
SetColor("_EmissionColor", ...),EnableKeyword("_EMISSION"), etc. Destroy(material)inOnDestroyor the clone leaks.
F. The practical decision: how to implement glow¶
Three viable techniques, in order of preference:
-
Call vanilla
Thing.SetCustomColor(index, emissive: true). Cleanest; uses the existing API; swaps toEmissivematerial and sets_EmissionColor. Limitations: the flag is transient and re-enters on every color change / sync / load, so a mod must re-apply after every vanillaSetCustomColorcall. Swatches whoseEmissiveis null produce no material swap (only the_EmissionColorwrite), which may or may not glow depending on whether theNormalmaterial's shader honors the property. -
Per-instance material clone + direct
_EmissionColorwrite. BypassSetCustomColorentirely on the glow side: fetchrenderer.materialonce, callSetColor("_EmissionColor", CustomColor.Color * intensity). Gives full intensity control, works for swatches that have nullEmissiveas long as theirNormalshader declares_EmissionColor. Costs one cloned material per glowing Thing; cleanup per../Patterns/UnityMaterialPerInstance.md. -
Shader swap to additive (fallback). If neither (1) nor (2) produces visible glow on some Things (batched structures, custom-shader children), mirror PowerTransmitterPlus's approach: create a new
MaterialwithShader.Find("Legacy Shaders/Particles/Additive")and swap. Breaks normal diffuse lighting in lit areas, so reserve for cases where the Thing is only visible when glow matters.
What will NOT work:
- Encoding glow as a high bit in the color index.
GameManager.IsValidColorclamps; see../Protocols/ThingColorMessage.md. - Attaching per-object
Lightcomponents. TODO-flagged as unacceptable for network-painting paint counts. - Enabling the
_EMISSIONshader keyword without also ensuring the material variant exposes the keyword. On Unity Standard,EnableKeyword("_EMISSION")+SetColor("_EmissionColor", c)both are required; on a custom shader the keyword may be redundant or not present at all. Probe first, don't assume.
G. Persistence and multiplayer gaps¶
Vanilla's emissive parameter is transient (see ../GameClasses/ColorSwatch.md). A mod feature that wants glow to survive save/load and multiplayer must:
- Store an
IsGlowingflag per Thing (customThingSaveDatasubclass; see../Patterns/SaveDataIsinstInheritance.mdandSaveDataRegistration.md). - Sync the flag via a free
Thing.NetworkUpdateFlagsbit + postfixes onThing.BuildUpdate/ProcessUpdate/SerializeOnJoin/DeserializeOnJoin(pattern perMods/SprayPaintPlus/RESEARCH.mdsection 3.4, applied one level up onThing). - Re-apply the emissive effect after every
SetCustomColorpostfix for Things whose flag is set, since the vanilla sync and load paths clear it.
The re-apply hook is what makes the feature work: any path that mutates color (paint, message receive, save load) passes emissive: false and clobbers the glow, so a postfix must read the glow flag and re-apply.
H. Visibility proof recipe¶
To verify glow is visible in-game, use the probe plugin at Plans/GlowPaintProbe/:
- Apply emissive to a painted pipe via direct
SetCustomColor(index, emissive: true)or via per-instance_EmissionColorwrite. - Place the pipe in a dark room (no external lights).
- A visible halo around the pipe confirms the bloom path is live.
- In a lit room, the glow is a subtle brightness lift from the bloom pass.
If step 3 produces no halo, check:
- Is
CameraController.SetBloomenabled? Confirm via the probe plugin's bloom log line (the runtime read accessor is documented in../GameClasses/CameraController.md). - Does
CustomColor.Emissiveexist for the swatch? Confirm via the probe plugin's swatch enumeration (which logsemissive=yes|noper entry). - Does the pipe's mesh have a
ThingRendererthat was enumerated bySetCustomColor?Thing.Renderersis a public list; iterate and log via the probe's F9 handler (see../GameClasses/ThingRenderer.md).
Verification history¶
- 2026-04-21: page created from decompiled Assembly-CSharp.dll and PowerTransmitterPlus source.
- 2026-04-21: corrected section C. Original claim "Every color has both Normal and Emissive materials" was not accurate; vanilla code contains
if (CustomColor.Emissive == null)null-checks, soEmissiveis optional per swatch. Page now links to../GameClasses/ColorSwatch.mdwhich documents the class fully, and section F distinguishes the two glow paths (material swap vs. property write) based on whetherEmissiveis present. Also restamped section formatting to the<!-- verified: ... -->HTML-comment form required byResearch/CLAUDE.md. - 2026-04-21: added runtime read accessor chain for bloom to section A (
CameraController.Instance.CameraEffects[0].Bloomwith.enabledcheck) and cross-references to the new../GameClasses/CameraController.mdand../GameClasses/ThingRenderer.mdpages. Section H updated to reference thePlans/GlowPaintProbe/probe plugin now that InspectorPlus is off-limits. Additive; no prior claim changed. - 2026-04-21: resolved the "fraction of swatches with non-null
Emissive" open question via GlowPaintProbe plugin logs. All 12 shipping swatches in v0.2.6228.27061 carry non-nullNormalandEmissivematerials. Full inventory (names + presence) is documented in../GameClasses/ColorSwatch.mdsection "Vanilla swatch inventory (v0.2.6228.27061)". Approach F.1 is therefore viable for every vanilla paint color; approach F.2 only matters for mod-added swatches that leaveEmissivenull. - 2026-04-21: approach F.1 visually and programmatically confirmed via GlowPaintProbe. Calling
Thing.SetCustomColor(index, emissive: true)on a paintedPipinginstance in a dark room produces a visible bloom halo;Plans/GlowPaintProbe/log lines captured the runtime material swap fromTextureArrayColorSwatch(shaderCustom/StandardTextureArray) toColorPurpleEmissive(shaderStandardInstanced) with_EmissionColor=(1.051, 0, 2.290, 1)and_EMISSION=on. Reverts cleanly onemissive: false. Full details in../GameClasses/ColorSwatch.mdsection "Vanilla swatch material naming (runtime)". F.1 is green-lit for the SprayPaintPlus glow-paint implementation.
Open questions¶
- Does setting
_EmissionColoralone on the sharedNormalmaterial (approach F.2 without the material swap) produce visible glow? The Normal shader isCustom/StandardTextureArray(a custom shader); whether it honors the property without the_EMISSIONkeyword is unknown. The Normal material is shared across all painted Things, so this path would need a per-instance material clone (see../Patterns/UnityMaterialPerInstance.md). Relevant only for mod-added swatches whoseEmissiveis null, since every vanilla swatch ships both. - Where is
UltimateBloomactually attached at runtime? GlowPaintProbe observedCameraController.Instance.CameraEffects.Count == 0throughout gameplay, yet bloom halo is visibly active on emissive materials. The decompile-derived accessorCameraController.Instance.CameraEffects[0].Bloomreturns empty; see../GameClasses/CameraController.mdsection "Runtime attachment (unresolved)". Not blocking for F.1 (bloom is working), but worth identifying before shipping a feature that needs to validate "bloom is on" from code.