flightOcean
Adaptation of the installed Zero Oceans substrate. Preserves its FFT waves, foam, reflections, clipmap and resource lifecycle. Viewer position and near/far planes use the local flight camera, falling back to the gameplay camera, so editor-camera movement cannot truncate or move t…
Flight ocean
Adaptation of the installed Zero Oceans substrate. Preserves its FFT waves, foam, reflections, clipmap and resource lifecycle. Viewer position and near/far planes use the local flight camera, falling back to the gameplay camera, so editor-camera movement cannot truncate or move the player's sea. All other dependencies remain in the installed builtin Oceans package. Environment owns the attachment and destroys it on stop.
Interface
What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.
conforms to
zero/source-extract/v2Spectral ocean simulation and surface. Creates a live sea from a named sea state, renders it, and answers where its surface is.
spectrum( ) → void
evolve( ) → void
fft( ) → void
assemble( ) → void
normals( ) → void
displace( ) → void
probe( ) → void
foam( ) → void
foam_texture( ) → void
gpu(name: string) → any
| arg | type | description |
|---|---|---|
| name | string |
sweepUnownedSurfaces( ) → void
Retire every ocean surface in the scene that no live instance owns. Ownership is asked of THIS copy's registry, so after a reload that is every surface the previous copies left: exactly the ones nothing can reach to stop. Despawning the surface is also what the orphan itself notices — `_step` reads it and tears the whole instance down, handing back its buffers, its maps and its session material — so the sweep ends the simulation, not just the draw.
merge(base: any, over: any) → any
Deep merge: `over` wins leaf by leaf, so naming a preset and then setting one nested field overrides only that field. Arrays are merged by index for the same reason — `cascades = { { scale = 2000 } }` should retouch the first cascade rather than replace the pair.
| arg | type | description |
|---|---|---|
| base | any | |
| over | any |
spreadIntegral(exponent: number, backRatio: number) → number
Integral of the directional spreading function over [0, 2pi), as a 64-band midpoint sum.
| arg | type | description |
|---|---|---|
| exponent | number | |
| backRatio | number |
assignBands(cascades: { any }, waves: any) → void
Give each cascade a disjoint slice of wave-number space. Crossovers sit at the geometric mean of one cascade's Nyquist and the next one's fundamental, and each band is rescaled by `sqrt(total / band)` so that splitting the spectrum does not change the energy any one cascade carries. Without this the cascades synthesise the same long waves twice and neither produces the short ones.
| arg | type | description |
|---|---|---|
| cascades | { any } | |
| waves | any |
resolveSwell(gerstner: any, waves: any, count: number) → void
Resolve the Gerstner configuration into the per-band values both the GPU passes and the CPU sampler read. Three factors here are not obvious and each changes the swell visibly: the amplitude carries a `windSpeed / 10` term, so the same swell settings raise a bigger sea in a gale; steepness is simply `min(0.5 * choppiness, 1)` rather than anything derived from the wave number; and total energy is held constant across band counts by `1 / sqrt(n)`, so a higher quality tier resolves the same swell more finely instead of raising a larger one.
| arg | type | description |
|---|---|---|
| gerstner | any | |
| waves | any | |
| count | number |
viewerPosition( ) → void
The world position the surface centres on. The ON-SCREEN camera is the right one to follow: in edit mode that is the author's fly camera, so the sea stays under whoever is looking at it.
cameraPlanes( ) → void
_resolveCascades(self: ?) → void
Which cascades actually run, pairing the preset's bands with the tier's resolutions BY INDEX. A band the tier switched off is dropped without shifting the others, or band 1 would inherit band 0's scale and amplitude.
| arg | type | description |
|---|---|---|
| self | ? |
_packCascade(self: ?, c: any, slot: number) → void
| arg | type | description |
|---|---|---|
| self | ? | |
| c | any | |
| slot | number |
_allocate(self: ?) → void
Every buffer the pipeline runs on. Each is created on the shader that writes it first, and every later pass is handed that same handle — so what a stage reads is the thing an earlier stage was given, stated in the dispatch rather than agreed on through a name.
| arg | type | description |
|---|---|---|
| self | ? |
_seedSpectrum(self: ?) → void
Rebuild the static spectrum and the swell bank. Runs on creation and whenever the sea state changes — never per frame.
| arg | type | description |
|---|---|---|
| self | ? |
_writeSwell(self: ?, t: number) → void
| arg | type | description |
|---|---|---|
| self | ? | |
| t | number |
_buildMesh(self: ?) → void
| arg | type | description |
|---|---|---|
| self | ? |
_attach(self: ?, target: any) → void
| arg | type | description |
|---|---|---|
| self | ? | |
| target | any |
_ensureMaterial(self: ?) → void
Build the session material and bind everything to it. The ocean drives cascade maps, sampling tiles and the scene capture on its material every frame. Those are properties of THIS sea, not of the material asset, and writing them to the asset would persist per-frame state into content. So the entity renders a SESSION material: a runtime copy of the authored look, live for this engine session only. It is built on the first STEP rather than at attach, and that ordering is load-bearing. At attach the surface shader has not compiled, so the material is created against a pipeline that does not exist; the one that replaces it when the shader lands does not inherit the binding, and the mesh then draws with no material data at all — black, whatever the shader computes. Setting the properties again later does not rescue it either, because it is the material object itself that was built too early. By the first step the shader has landed and every compute map has been filled, which is the first moment this can stick.
| arg | type | description |
|---|---|---|
| self | ? |
_bindMaterial(self: ?) → void
Push everything the material cannot know on its own.
| arg | type | description |
|---|---|---|
| self | ? |
foamLayer(prefix: string, layer: any) → void
| arg | type | description |
|---|---|---|
| prefix | string | |
| layer | any |
_bindSimulationTextures(self: ?) → void
Bind the maps the simulation writes. These are compute textures, and a compute texture does not exist until the pass that fills it has run. Naming one before then binds a slot to nothing, which invalidates the whole bind group — the surface then draws with no material data at all and comes out black, whatever the shader computes. So the binding waits for the first frame's `copyBufferToTexture` and latches.
| arg | type | description |
|---|---|---|
| self | ? |
_bindUnderwater(self: ?) → void
| arg | type | description |
|---|---|---|
| self | ? |
_pushUnderwater(self: ?) → void
The sea state the underwater pass draws with. It renders the SAME water the surface does, from beneath it, off the same colours, the same caustics and the same sea level — so the push runs again whenever the configuration moves, and a sea recoloured mid-session looks the same from under it as it does from above.
| arg | type | description |
|---|---|---|
| self | ? |
_stepUnderwater(self: ?) → void
| arg | type | description |
|---|---|---|
| self | ? |
_buildFoamPatterns(self: ?) → void
Allocate the three foam patterns and write their first frame. Each is its own texture with its own seed, so the layers do not print the same shapes at three different scales. The patterns are LIVING fields — `_stepFoamPatterns` re-dispatches them every frame with the simulation clock, so the maps churn instead of standing as shapes stamped on the sea.
| arg | type | description |
|---|---|---|
| self | ? |
_stepFoamPatterns(self: ?, elapsed: number) → void
| arg | type | description |
|---|---|---|
| self | ? | |
| elapsed | number |
_bindScene(self: ?) → void
Bind the captured scene colour and depth, plus the camera planes needed to read them. The water column — and with it the colour, the refraction, the shoreline wash and the reflections — is measured from these, so until the capture has run a frame the surface correctly reads as uniformly deep.
| arg | type | description |
|---|---|---|
| self | ? |
isEntityId(s: string) → boolean
One entity however the caller holds it: an entityRef field value ({ __id }), a proxy, an id, or a name.
| arg | type | description |
|---|---|---|
| s | string |
resolveEntity(named: any) → any
| arg | type | description |
|---|---|---|
| named | any |
_bakeGround(self: ?) → void
Fill the ground grid over the region `config.ground` describes. The config only draws the RECT — an entity's footprint, the union of several, or an explicit `{ x, z, size }` — and the heights come from downward raycasts against the physics scene, so the ground the waves meet is EVERYTHING solid standing in the region at once: a landscape, a modelled seabed, a dock, a scatter of rocks. Dynamic bodies are skipped — a moored hull floats ON the waves and must not carve a shadow into them — and so is the sea's own entity. Sampling is chunked across frames; a sixty-thousand-ray gulp would stall the frame that spawned the sea. The grid serves two readers with one truth: the GPU buffer the displace pass shoals by, and the Luau-side table `sampleHeight` applies the same shoaling from.
| arg | type | description |
|---|---|---|
| self | ? |
resolveRegion( ) → void
The region: explicit rect, one entity's footprint, or a union of them. Resolved INSIDE the bake task, because an entity fresh out of a world load exists frames before its bounds do — the bake waits for them rather than killing the attach that asked for it.
grow(b: any) → void
| arg | type | description |
|---|---|---|
| b | any |
isGround(id: string) → boolean
| arg | type | description |
|---|---|---|
| id | string |
heightUnder(x: number, z: number) → number
| arg | type | description |
|---|---|---|
| x | number | |
| z | number |
_groundHeightAt(self: ?, x: number, z: number) → number
The ground's world height under an XZ point, from the baked grid — the CPU half of the same bilinear read the displace pass does.
| arg | type | description |
|---|---|---|
| self | ? | |
| x | number | |
| z | number |
_shoalAt(self: ?, x: number, z: number) → number
The CPU half of the displace pass's shoaling ramp, over the same grid.
| arg | type | description |
|---|---|---|
| self | ? | |
| x | number | |
| z | number |
step(self: ?) → void
Advance the simulation one frame of wall-clock time. Two drivers call it: the loop `attach` spawns, which keeps the sea moving while the engine is paused, and the Ocean component's update, which the engine owns and which therefore carries the sea across a module reload. Whichever runs first in a frame steps; the guard absorbs the other's call, so twin drivers never double-step. Calling it on a destroyed ocean is a no-op.
| arg | type | description |
|---|---|---|
| self | ? |
examples
ocean:step()
_step(self: ?, rawElapsed: number, dt: number) → void
| arg | type | description |
|---|---|---|
| self | ? | |
| rawElapsed | number | |
| dt | number |
_stepFoam(self: ?, dt: number) → void
Advance the persistent whitecap field. Its region follows the viewer, snapped to a whole texel so the previous frame can be reprojected by an exact offset.
| arg | type | description |
|---|---|---|
| self | ? | |
| dt | number |
_pumpProbe(self: ?, elapsed: number) → void
Keep the CPU-side wave grid fresh. One readback is in flight at a time; the result replaces the previous snapshot when it lands.
| arg | type | description |
|---|---|---|
| self | ? | |
| elapsed | number |
_probeAt(self: ?, x: number, z: number) → void
Bilinear read of the probe grid, which tiles over the largest cascade's sampling tile.
| arg | type | description |
|---|---|---|
| self | ? | |
| x | number | |
| z | number |
cell(cx: number, cz: number) → void
| arg | type | description |
|---|---|---|
| cx | number | |
| cz | number |
lerp(a: number, b: number, t: number) → number
| arg | type | description |
|---|---|---|
| a | number | |
| b | number | |
| t | number |
_swellAt(self: ?, x: number, z: number) → void
The swell, evaluated on the CPU exactly as the GPU evaluates it. The probe grid cannot carry it: that grid tiles over the largest cascade's sampling tile, and a swell longer than the tile does not repeat on it. Since the bank is analytic anyway, re-evaluating it here is both cheaper than widening the readback and exact rather than interpolated — which matters, because this is what a hull floats on.
| arg | type | description |
|---|---|---|
| self | ? | |
| x | number | |
| z | number |
sampleDisplacement(self: ?, x: number, z: number) →
The full displacement of the water surface at a world XZ position. of the surface that ended up above this position.
| arg | type | description |
|---|---|---|
| self | ? | |
| x | number | World X. |
| z | number | World Z. |
examples
local d = ocean:sampleDisplacement(hull.x, hull.z)
sampleHeight(self: ?, x: number, z: number) → number
The height of the water surface at a world XZ position.
| arg | type | description |
|---|---|---|
| self | ? | |
| x | number | World X. |
| z | number | World Z. |
examples
boat.position = { x, ocean:sampleHeight(x, z), z }configure(self: ?, options: any) → void
Change the sea. Any field of the configuration may be passed; anything omitted is left alone. Naming a preset re-applies that whole sea, with fields passed alongside it still winning.
| arg | type | description |
|---|---|---|
| self | ? | |
| options | any | Fields to change. |
examples
ocean:configure { preset = "gale", waves = { windDirection = 1.2 } }getConfig(self: ?) → any
The sea state this ocean is running, as a table.
| arg | type | description |
|---|---|---|
| self | ? |
examples
local wind = ocean:getConfig().waves.windSpeed
getEntity(self: ?) → any
The entity the surface renders on.
| arg | type | description |
|---|---|---|
| self | ? |
examples
local e = ocean:getEntity()
release(self: ?, what: string, free: () → void
One step of a teardown, held to the step it is. A release that runs into something says so in the log and the teardown carries on, so a single resource the engine will not take back costs its own line rather than the rest of the sea.
| arg | type | description |
|---|---|---|
| self | ? | |
| what | string | |
| free | ( |
_textureKeys(self: ?) → void
Every compute texture key this ocean holds a map under. A compute texture comes into being on the first `copyBufferToTexture` into its key and is owned by that key rather than by a handle, so the release is by name — and these names are the ocean's own, composed from its prefix. One list answers both what the sea allocated and what its teardown hands back, so the two cannot drift apart. It is the NAMES that answer, not the cascade count: `configure` re-resolves the cascades from the tier it is handed, so a sea that came up at three and was turned down to one is still holding the maps the other two wrote. Every key here carries this sea's own prefix, and handing back one nothing is filed under costs a name.
| arg | type | description |
|---|---|---|
| self | ? |
destroy(self: ?) → void
Tear the ocean down: the mesh, every GPU buffer and compute texture it owns, and the entity if this ocean spawned one. An ocean attached to an entity the caller supplied leaves that entity in place. The ocean stops simulating and leaves the registry first, so the teardown completes whether or not the entity it rendered on is still there, and a second call is a no-op.
| arg | type | description |
|---|---|---|
| self | ? |
examples
ocean:destroy()
attach(target: any, options: any?) → any
Bring up an ocean on an entity: the wave simulation, the clipmap surface that renders it, the material bound to the simulation, and the readback gameplay queries. One entity carries one sea, so an entity already carrying one has that sea torn down as this one takes its place; the entity itself stays, whichever sea spawned it. preset. Every block of the configuration may be overridden field by field. `getConfig`, `getEntity` and `destroy`.
| arg | type | description |
|---|---|---|
| target | any | The entity to render the surface on, or nil to spawn one. |
| options | any? | `{ preset, quality, seaLevel, followCamera, ... }` layered over the |
examples
local ocean = Ocean.attach(nil, { preset = "gale", quality = "high" })create(options: any?) → any
Spawn an entity carrying the `Ocean` component, which brings the sea up. The component owns the surface, so the sea survives a scene reload — the simulation and its GPU mesh are rebuilt from the component's own settings. `Ocean.forEntity(e.id)`.
| arg | type | description |
|---|---|---|
| options | any? | Sea state and surface settings. |
examples
local e = Ocean.create({ preset = "gale" })forEntity(entityId: string) → any
The ocean rendering on an entity.
| arg | type | description |
|---|---|---|
| entityId | string | The entity's id. |
examples
local ocean = Ocean.forEntity(e.id)
list( ) →
Every live ocean.
examples
for _, o in ipairs(Ocean.list()) do o:destroy() end
clear( ) → void
Tear down every live ocean.
examples
Ocean.clear()
presets( ) →
The named sea states this package ships.
examples
for _, name in ipairs(Ocean.presets()) do print(name) end
qualities( ) →
The quality tiers this package ships.
examples
for _, name in ipairs(Ocean.qualities()) do print(name) end
Sub-parts
Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.
Problems
Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.
agent_score is exposed.+ quality × 0.35
+ performance × 0.25
± compat factor
Usability ratings
Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".
Scoped to this part · feeds back into the world's score.