[Showcase] Voxel-Based Gas Simulation Service

Hey everyone,

I’ve been working on a gas simulation system for my game and wanted to share the current version.

The goal was to create gases that behave more naturally than a simple expanding sphere or particle emitter while still being practical for a large Roblox environment with StreamingEnabled.

The system supports:

  • Gas diffusion between voxel cells
  • Rising, sinking, and neutral gases
  • Pressure equalization
  • Obstacles, walls, doors, and openings
  • Natural dissipation
  • Multiple gas types in the same system
  • Player-specific gas behavior modules
  • Chunk-based simulation
  • Sleeping settled voxels
  • Delta replication
  • Client-side voxel merging
  • Pooled rendering parts
  • Cached obstruction raycasts

Current result

The simulation stores gas in a sparse three-dimensional voxel grid.

Each voxel can hold different amounts of one or more gas types. The server is responsible for diffusion, buoyancy, dissipation, obstruction checks, and gameplay effects. Clients only receive nearby visual state.

This separation is especially important with StreamingEnabled, since clients may not have distant walls or geometry loaded. Keeping the simulation server-authoritative prevents gas from incorrectly passing through geometry that has not streamed to a client yet.

Gas movement

Gas spreads according to the concentration difference between neighboring voxel cells.

A voxel with a higher concentration attempts to equalize with nearby lower-concentration voxels. Movement is supported through all six cardinal directions.

Buoyancy is applied as a directional influence rather than replacing normal diffusion.

This means:

  • Light gas tends to rise
  • Heavy gas tends to sink
  • Neutral gas spreads without a vertical preference

Because normal diffusion remains active in every direction, gases form clouds instead of narrow vertical pillars.

Pressure and overflow

Each voxel has a maximum capacity.

If more gas is added than a voxel can contain, the excess is immediately pushed into open neighboring voxels.

This avoids having one extremely overfilled cell slowly release its contents over many simulation updates. The result is a more immediate expansion when a large amount of gas is released at once.

Obstacles and openings

The simulation checks whether gas can move across each voxel boundary.

This allows gas to react to:

  • Walls
  • Floors
  • Ceilings
  • Doors
  • Windows
  • Vents
  • Small openings

Obstruction checks are cached.

Once the system determines whether a shared boundary is open or blocked, the result is reused by both neighboring voxels. For static geometry, that boundary generally never needs to be checked again.

Moving doors, destructible walls, and other dynamic geometry can invalidate the relevant cached boundaries when they change.

Chunk-based simulation

Voxels are grouped into spatial chunks.

Chunks are used for:

  • Simulation-distance checks
  • Nearby replication
  • Client-side rendering updates
  • Avoiding full-world voxel scans
  • Sleeping distant gas regions

This makes the system more suitable for larger environments because it does not need to search every gas voxel for every player or every replication update.

Active scheduling

Processing every voxel at the same frequency became too expensive once clouds reached several thousand cells.

The current system uses a unique round-robin scheduler. Each voxel can only appear once in the scheduler, preventing duplicate queue entries and update starvation.

Voxels can operate in three general states:

Hot

Actively moving gas. These voxels update frequently.

Warm

Mostly settled gas. These voxels update less frequently.

Sleeping

Fully settled gas. These voxels perform no movement calculations until something nearby changes.

Settled gas that still dissipates continues waking periodically so it can fade and eventually be removed. Settled non-dissipating gas can sleep indefinitely.

Sleeping voxels wake when:

  • Gas is added
  • Gas is removed
  • A neighboring voxel changes
  • Nearby geometry changes
  • A local obstruction cache is invalidated

Simulation budget

The service limits how many voxels can be processed during one simulation step.

This prevents large gas clouds from creating a single large server-frame spike. Rather than updating every voxel at once, the scheduler spreads the workload over time while prioritizing active gas.

Delta replication

The server does not send the complete gas state on every replication update.

Instead, it sends only:

  • Newly visible voxels
  • Voxels whose contents changed
  • Voxels that were removed
  • Voxels that left the player’s render range

Updated entries replace the client’s stored state for those voxel keys. Removed entries delete those voxel keys. Anything omitted from a packet remains unchanged.

This reduced network usage significantly compared with sending complete snapshots.

Rendering

The first version rendered one part per voxel.

That became expensive very quickly.

The current renderer groups adjacent voxels when they have:

  • The same dominant gas
  • Similar density
  • The same render chunk

Those voxels are greedily merged into larger blocks.

This can turn many small rendered parts into a much smaller number of larger parts.

Rendered parts are pooled rather than constantly created and destroyed. When gas dissipates or leaves the player’s render distance, the related blocks fade out and return to the pool.

Player gas behaviors

Each gas type can have its own gameplay behavior module.

A behavior can react when a player:

  • Enters the gas
  • Remains inside the gas
  • Leaves the gas

The behavior receives information such as:

  • Player
  • Character
  • Humanoid
  • Root part
  • Gas type
  • Gas amount
  • Gas density
  • Elapsed time

This keeps gameplay effects separate from the simulation itself.

Examples include:

  • Poison gas dealing density-scaled damage
  • Smoke reducing visibility
  • Fog obscuring distant objects
  • Sedative gas applying status effects
  • Flammable gas reacting to ignition sources

Dissipation

Each gas type has its own dissipation rate.

Settled gas does not stop dissipating.

Even when a voxel no longer needs frequent movement calculations, it continues waking at a lower frequency so its amount can decrease.

Once the remaining amount stays below the cleanup threshold for long enough, the voxel is removed and replicated to nearby clients.

Gas types with no dissipation can remain permanently after settling.

Debugging and monitoring

The service tracks information such as:

  • Processed voxels
  • Inspected voxels
  • Scheduled voxels
  • Total voxel count
  • Sleeping voxel count
  • Total gas amount
  • Chunk count
  • Cached face count

Tracking the total gas amount has been especially useful for distinguishing between actual simulation issues and visual rendering issues.

Performance improvements made so far

The current version includes:

  • Sparse voxel storage
  • Unique round-robin scheduling
  • Hot, warm, and sleeping update states
  • Maximum work per simulation step
  • Chunk-based spatial indexing
  • Distant simulation sleeping
  • Delta replication
  • Cached neighbor references
  • Cached obstruction boundaries
  • Reduced first-time face sampling
  • Minimum transfer required to create a new voxel
  • Immediate pressure overflow
  • Client-side greedy merging
  • Part pooling
  • Dirty-chunk-only rendering updates

Current limitations

The system is still voxel-based, so the outer shape can appear blocky.

Greedy merging reduces the number of rendered parts, but it does not create a perfectly smooth cloud surface.

I experimented with runtime EditableMesh rendering, but rebuilding large transparent meshes continuously was less predictable and more expensive than merged pooled parts.

Other current limitations include:

  • No wind fields
  • No fans or ventilation yet
  • Dynamic geometry requires cache invalidation
  • Smaller voxel sizes increase simulation cost significantly
  • Transparent geometry remains expensive when many layers overlap

I’d be interested to hear how other developers would approach the rendering side, especially for large transparent gas clouds without producing hundreds of over

8 Likes

I’ve added the ability to lerp gas positions now.

3 Likes

holy crap this is really cool, keep up the great work!