Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit65083fe8f2

README.md

Volume

Per-entity attachment that renders a 3D voxel texture (cloud, smoke, nebula, imported VDB grid) into the scene as a volumetric body. The component does NOT own the volume's lifetime — producers (the volume module, a custom compute fill, or compute.writeTexture3D) build the texture and pass its name in volumeName. The entity's world transform defines the AABB the raymarcher samples.

volumeName is resolved through Volume.get, so a volume the volume module built is rendered as it stands — at its own dimensions, from its own density channel, through its occupancy grid, and out of its brick pool if it is paged. A name no volume answers to is taken as a bare 3D texture some other system owns, and occupancyName / isRgba describe it.

Usage

local Volume = require("@builtin::systems.volumetrics.volume")
local cloud = Volume.noise({ name = "cloud", size = 64, scale = 4 })
cloud:buildOccupancy({ brickSize = 8 })

local e = entity.spawn("cloud_entity")
e.localScale.set(8, 4, 8)
e.component.add("Volume", {
    volumeName    = "cloud",
    occupancyName = cloud.occupancyName,
    density       = 6.0,
})

A paged volume costs what it holds and is named the same way:

local cloud = Volume.noise({ name = "cloud", size = 128, threshold = 0.6 })
cloud:buildSparse({ brickSize = 8, threshold = 0.01 })
cloud:releaseDense()          -- the brick pool is now the only copy

local e = entity.spawn("cloud_entity")
entity(e.id).localScale = { 8, 4, 8 }
e.component.add("Volume", { volumeName = "cloud", density = 6.0 })

Public fields

FieldTypeSyncDefaultMeaning
volumeNamestringSync""Required. Name of the volume, or of a bare 3D texture, to render.
occupancyNamestringSync""R8 occupancy texture for a bare texture's empty-space skipping. Empty = no occupancy. A volume the module built carries its own, and a name set against one is reported as left aside.
densitynumberSync1.0Density gain multiplier.
absorptionnumberSync1.0Beer-Lambert absorption coefficient.
emissionnumberSync1.0Emission gain.
colorcolorSync{1,1,1,0}RGB tint + density floor in alpha.
stepSizenumberNoSync0World units per ray step. 0 = auto-derive from volume extent.
maxStepsnumberNoSync256Ray-march step cap.
targetstringSync"scene_volume_rt"Output render target name (composite key).
targetWidthnumberNoSync1280Output width (px).
targetHeightnumberNoSync720Output height (px).
useOccupancybooleanNoSynctrueToggle empty-space skipping.
isRgbabooleanSynctruefalse reads density from the red channel. Left true, a volume the module built is read from the channel it was built with, and a bare texture as RGBA.

All public fields use the Field.<kind>(default, Sync|NoSync) schema (PR #3019). Visual params (look, tint, density) propagate Sync; quality knobs (stepSize, maxSteps, target resolution, useOccupancy) are NoSync — each client picks its own perf/quality point.

Lifecycle

  • awake — no-op. The volume texture is owned by the producer and looked up lazily on the first update.
  • update(dt) — resolves volumeName through Volume.get, falling back to a wrapper over a bare texture of that name, and dispatches the compute raymarcher into target. The resolution runs every frame, so a volume built, paged or destroyed after this component attached is picked up on the next one.
  • onPropertyChanged(key, value, old) — drops the resolved volume when volumeName, occupancyName, or isRgba changes, forcing a re-resolve on the next frame.

Related

  • volume module — texture builders and name lookup (Volume.noise, Volume.get, Volume.wrap, :buildSparse, :render).
  • vdb_to_zvol.importer — VDB → dense voxel pipeline that produces textures consumable by this component.
  • volumeDemos / volumetricSky packages — worked examples.