


Build Piece Optimizer is a Valheim BepInEx plugin focused on reducing large-build performance costs caused by many loaded build pieces, lights, particles, shadows, and effects.
The plugin includes an in-game profiler overlay because optimization work needs measurement. The profiler is a diagnostic companion: it shows what is loaded, what is visible, what is build-piece-specific, and what the optimizer is currently doing.
This is an early optimization-first alpha.
The profiler is usable for testing and benchmarking. Optimization features are experimental and opt-in. They may change visual behavior, especially around torches, hearths, braziers, campfires, smoke, flames, shadows, and lights.
Use the optimization features carefully, test in a copy of your world if you are concerned, and report issues with screenshots, config values, and profiler metrics where possible.
StaticLight mode for fire-like build pieces.FullCull mode for aggressive testing.StaticLight.FullCull visual behavior.WearNTear behavior.Piece, WearNTear, ZNetView, support logic, or save/network state.v0.3.x - Fire/light optimization foundation
StaticLight mode.FullCull mode.v0.4.x - Stable StaticLight policy and safer proxy-light limits
v0.5.x - Particle/smoke-specific optimization
v0.6.x - Static build-piece visual simplification experiments.v0.7+ - Mesh proxy / batching experiments for safe static build clusters.WearNTear / support throttling.Install BepInExPack Valheim.
Start Valheim once so BepInEx creates its folders.
Copy BuildPieceProfiler.dll into:
Valheim/BepInEx/plugins/
Start Valheim.
Press F7 in-game to toggle the profiler overlay.
After the plugin runs once, a config file is created at:
Valheim/BepInEx/config/mikael.valheim.buildpieceprofiler.cfg
Depending on the plugin GUID used in your local build, the config filename may differ slightly.
[Profiler]
EnableProfiler = true
EnableConsoleLogging = false
ShowProfilerOnStart = true
PollIntervalSeconds = 1
ToggleProfilerKey = F7
Enables or disables the profiler system. If disabled, the overlay and profiler polling are not used.
Writes profiler counts to the BepInEx log each poll. This is useful for testing, but can create large log files during long sessions.
Controls whether the overlay is visible when the plugin starts.
Controls how often the profiler updates. Lower values update faster but cost more performance.
Controls the key used to show or hide the overlay.
[Optimizations]
EnableOptimizations = false
OptimizerUpdateIntervalSeconds = 0.5
Master switch for optimization features.
When disabled, the plugin should restore tracked fire optimization states and remove proxy lights created by the plugin.
Controls how often the optimizer scans fire candidates and updates their optimization state. Lower values react faster, but cost more performance.
[DistanceThresholds]
NearDistance = 10
MediumDistance = 25
FarDistance = 50
VeryFarDistance = 100
ExtremeDistance = 200
These thresholds are used by the profiler to group build pieces by distance from the local player.
Intended meaning:
[FireCulling]
FireCullingMode = StaticLight
UseVisibilityCulling = true
UseOcclusionCulling = true
VisibilityGraceSeconds = 1.5
RestoreGraceSeconds = 1.0
NeverOptimizeFireWithin = 2
MinimumFullCullDistance = 15
FireDistanceCullStart = 50
FireDistanceRestore = 40
StaticLightIntensityMultiplier = 0.45
StaticLightRangeMultiplier = 0.65
StaticLightProxyMaxDistance = 50
StaticLightOccludedProxyMaxDistance = 20
OcclusionRayRadius = 0.05
DebugFireOcclusion = false
Controls how fire-like build pieces are optimized.
Valid modes:
Off
StaticLight
FullCull
If true, fire effects may be optimized when a fire is hidden or irrelevant for long enough.
If true, fire candidates that are inside the camera view but blocked by solid geometry can be treated as hidden/irrelevant.
This is useful for cases such as torches behind walls.
How long a fire must remain hidden/irrelevant before visibility-based optimization can activate.
How long a fire must remain relevant before optimized fire effects are restored. This helps prevent rapid on/off cycling near occlusion edges.
Fire pieces closer than this distance are restored and should remain vanilla. This is a safety threshold.
Minimum distance required before FullCull mode may fully remove fire light/effects due to visibility or occlusion.
Distance beyond which fire optimization can activate regardless of visibility.
Distance within which fire optimization can restore to normal if the fire is relevant long enough. This should usually be lower than FireDistanceCullStart to avoid threshold flickering.
Multiplier applied to the original fire light intensity when creating a proxy static light.
Lower values are safer for performance and reduce over-brightening.
Multiplier applied to the original fire light range when creating a proxy static light.
Lower values reduce light overlap and improve performance.
Maximum distance at which StaticLight mode may create a proxy light for relevant/visible fires. Beyond this, optimized fires may receive no replacement light.
Maximum distance at which StaticLight mode may create a proxy light for hidden or occluded fires. Farther hidden fires receive no proxy light.
Radius used for fire occlusion spherecasts.
0 uses a normal raycast.0.01 to 0.05 may make occlusion checks more forgiving.Attempts to draw debug rays for occlusion checks. In a built Valheim game, Unity Debug.DrawLine may not be visible in the normal game view, so the overlay metrics are usually the more useful debugging tool.
These counts include all active loaded scene objects, not just player-built pieces.
Number of loaded Piece components. These are Valheim build-piece components.
Number of loaded WearNTear components. These usually handle health, damage, repair, rain decay, support/stability, destruction, and visual wear state.
Number of loaded ZNetView components. These represent networked/persistent objects.
Number of loaded MeshRenderer components in the scene.
Number of loaded MeshRenderers that are enabled and active in the hierarchy.
Number of enabled MeshRenderers currently considered visible by Unity.
This can change when turning the camera. A renderer can be enabled but not visible.
Number of loaded LODGroup components.
Number of loaded Collider components in the scene.
Number of colliders that are enabled and active.
Number of loaded rigidbodies that are active and not sleeping.
Number of loaded Light components.
Number of Light components that are enabled and active.
Number of loaded ParticleSystem components.
Number of ParticleSystem components that are alive.
Number of loaded AudioSource components.
These counts only include components found under loaded Piece objects and their children.
This section is important because it separates player-built structures from general world objects, terrain clutter, creatures, items, UI objects, and other scene components.
Total MeshRenderer components under Piece objects.
Enabled and active MeshRenderers under Piece objects.
This is a key metric for future renderer optimization.
Build-piece MeshRenderers currently considered visible by Unity.
If this changes when turning the camera, normal renderer visibility culling is happening. If enabled counts remain the same, the objects are still active even while not visible.
LODGroups under Piece objects.
These are cumulative distance buckets around the local player.
For example:
Pieces within Near: 10
Pieces within Medium: 40
Pieces within Far: 80
This means:
The buckets are useful for deciding optimization thresholds.
Total Collider components under Piece objects.
Enabled and active Colliders under Piece objects.
This is important for future collider simplification or distance-based collider disabling experiments.
Rigidbody components under Piece objects.
Build-piece rigidbodies that are active and not sleeping.
Light components under Piece objects.
Enabled and active Light components under Piece objects.
ParticleSystem components under Piece objects.
Particle systems under Piece objects that are alive.
AudioSource components under Piece objects.
Number of build pieces detected as fire-like candidates.
The current detection rule is component-based:
original build-piece lights > 0
AND
particle systems > 0
Proxy lights created by this plugin are ignored when detecting original fire candidates.
Fire candidates with at least one child MeshRenderer that Unity currently considers visible.
Renderer-visible fire candidates that appear to be blocked from the camera by geometry according to an occlusion raycast/spherecast.
Fire candidates that are renderer-visible and not occluded.
Fire candidates that are either not renderer-visible or are considered occluded.
Number of fire candidates currently in an optimized state.
Number of optimized fire pieces currently using StaticLight mode.
Number of optimized fire pieces currently using FullCull mode.
Number of plugin-created proxy static lights currently active.
If this number is too high, FPS can drop because even no-shadow lights still cost performance.
Number of original fire lights that were originally enabled and are currently disabled by the optimizer.
Number of tracked fire particle systems that were originally alive and are currently stopped.
This metric is mainly relevant to FullCull and particle-culling experiments. StaticLight particle behavior is still being refined.
Number of tracked fire renderers whose original shadow mode was not Off and is currently forced to Off.
Use repeatable testing conditions.
Suggested procedure:
tod 0.35.Recommended notes per screenshot:
If visible MeshRenderer count changes when turning the camera, Unity renderer visibility culling is working.
If enabled MeshRenderer, Collider, Piece, WearNTear, and ZNetView counts stay the same, then objects are still loaded and active even when not visible.
This means the game may avoid drawing off-screen objects, but it does not necessarily unload, disable, merge, or sleep build pieces.
If Renderer-visible fire candidates is high but Occluded fire candidates is also high, the plugin is detecting fires that are inside the camera frustum but blocked by geometry, such as torches behind walls.
If Fire proxy lights active is close to Optimized fire pieces, StaticLight may still be adding too many lights. Lower StaticLightProxyMaxDistance, StaticLightOccludedProxyMaxDistance, StaticLightIntensityMultiplier, or StaticLightRangeMultiplier.
Broad performance overhaul mods may overlap with this plugin.
Potentially overlapping systems include:
For clean testing, use separate mod profiles:
Develop and benchmark new features in Profile A first, then test compatibility in Profile C.
Renderer.isVisible can be affected by any camera, not only the main player camera.Debug.DrawLine may not be visible in the normal game view.