Dissolve Animation System
The dissolve animation system provides smooth visual transitions for tree depletion and respawn using screen-door dithering. This system is shared betweenGLBTreeInstancer and GLBTreeBatchedInstancer to ensure consistent behavior.
Overview
When a tree is depleted (chopped down), it instantly becomes ~70% transparent using screen-door dithering. When the tree respawns, it smoothly fades back to full opacity over 0.3 seconds. This provides clear visual feedback without requiring expensive alpha blending or transparency sorting.Key Features
- Screen-Door Dithering: Uses Bayer 4×4 dithering pattern to discard fragments, keeping trees in the opaque render pass
- Opaque Pass Rendering: No transparency sorting overhead, full early-Z rejection benefits
- LOD Transition Preservation: Dissolve state carries over during LOD swaps to prevent visual pops
- Atomic Initial State: Trees loaded in depleted state have dissolve applied atomically (no 1-frame flash)
- Interrupt Handling: Reversing an in-progress animation continues from current progress (no visual pop)
API Reference
Module: packages/shared/src/systems/shared/world/DissolveAnimation.ts
DissolveAnim Interface
startDissolve()
Start or instantly apply a dissolve animation.
anims- The animation map to manageentityId- Entity to animatedirection-1for dissolve out (depletion),-1for appear in (respawn)instant- Iftrue, jump to target value immediately; iffalse, animate overDISSOLVE_DURATIONapplyFn- Callback that writes the dissolve value to the rendering backend
- If
instant=true: Immediately sets dissolve to target value and removes from animation map - If
instant=false: Starts animation from current progress (or 0/DISSOLVE_MAX if not animating) - If already animating in opposite direction: Continues from current progress to avoid visual pop
tickDissolveAnims()
Advance all active dissolve animations by deltaTime and apply values.
anims- The animation map to tickdeltaTime- Time elapsed since last tick (seconds)applyFn- Callback that writes the dissolve value to the rendering backend
- Advances each animation’s progress by
(direction * deltaTime) / DISSOLVE_DURATION - Clamps progress to
[0, DISSOLVE_MAX]range - Calls
applyFnfor each animating entity - Removes completed animations from the map
- Early-out if
anims.size === 0(no allocations) - Reuses module-level
_completedarray to avoid per-frame allocation - O(active animations), not O(total instances)
Configuration
Dissolve behavior is controlled byGPU_VEG_CONFIG in packages/shared/src/systems/shared/world/GPUMaterials.ts:
Implementation Details
Encoding
InstancedMesh (GLBTreeInstancer):- Uses dedicated
instanceDissolveFloat32 attribute per instance - Direct per-instance control with full precision
- Encodes dissolve in blue channel of per-instance batch color
blue = 1.0 - dissolveVal(1.0 = fully visible, 0.0 = fully dissolved)- R/G channels reserved for highlight intensity
- Uint8 precision (~256 levels) is sufficient for 0.3s animations at 60fps (~18 steps)
Shader Integration
The dissolve effect is implemented increateDissolveMaterial() with enableDepletionDissolve: true:
LOD Transition Handling
When a tree transitions between LOD levels, the dissolve state must be preserved:Initial Depleted State
Trees that spawn already depleted must have dissolve applied atomically:Performance Characteristics
- Opaque Pass: Trees stay in opaque render pass with full early-Z rejection (no transparency sorting)
- Zero Allocation: Reuses module-level
_completedarray to avoid per-frame allocation - Batched GPU Uploads:
dissolveDirtyflag batches attribute uploads per pool per frame - Early-Out:
if (anims.size === 0) returnskips work when no animations active - O(active animations): Tick cost scales with animating trees, not total tree count
Thread Safety
WARNING: The_completed array is a module-level singleton reused across ticks. This is safe because:
- Both instancers call
tickDissolveAnims()sequentially on the main thread - Never called concurrently or from workers
- Each instancer has its own
dissolveAnimsmap
_completed array.
Usage Example
Migration Notes
From Depleted Model Pool Approach
The old system used separate “depleted” model pools with stump meshes. This has been completely removed: Removed APIs:setDepleted(entityId, depleted)- UsestartDissolve()insteadhasDepleted(entityId)- No longer neededloadDepletedPool()- Depleted models no longer loadedpool.depleted- Depleted pool removed from TreeTypePool/ModelPool
- Eliminates ~316 lines of depleted pool management code
- No separate model loading for depleted state
- Smoother visual transitions
- Simpler architecture
See Also
packages/shared/src/systems/shared/world/GLBTreeInstancer.ts- InstancedMesh implementationpackages/shared/src/systems/shared/world/GLBTreeBatchedInstancer.ts- BatchedMesh implementationpackages/shared/src/systems/shared/world/GPUMaterials.ts- Shader configurationpackages/shared/src/entities/world/visuals/TreeGLBVisualStrategy.ts- Visual strategy integration