Skip to main content

Native Dynamic Lights

Nebulon ships its own clean-room dynamic light renderer for Minecraft 1.21.11 — Veil is architectural reference only, not a dependency. The backend supports point, spot, and capsule-shaped beam lights through mutable ownership handles, with optional screen-space or world-space shadows.

Lights are client-local and world-scoped. All service and handle calls are thread-safe.


Light shapes

Point lights

PointLightHandle light = NebulonLights.lights().add(world,
NebulonLights.point(position)
.color(ColorRgba.rgba(0xff6633ff))
.radius(7) // default 6
.brightness(1.8f) // default 1
.maxRenderDistance(96) // default 96
.build());

Spotlights

Spotlights illuminate a cone. Inner and outer angles create a soft edge and must stay below 90°:

SpotLightHandle spotlight = NebulonLights.lights().add(world,
NebulonLights.spot(position, direction)
.color(ColorRgba.rgba(0xffdd99ff))
.range(18)
.coneDegrees(14, 28) // or cone(innerRad, outerRad)
.brightness(2.2f)
.build());

Beam lights

Beam lights illuminate a capsule around a segment — laser-shaped lighting. They do not draw a visible beam core; pair one with ArcChain.between(...) when the laser itself must be visible:

BeamLightHandle laserLight = NebulonLights.lights().add(world,
NebulonLights.beam(start, end)
.color(ColorRgba.rgba(0x44eeffff))
.radius(2.5f)
.brightness(1.5f)
.build());

Descriptions are immutable and validated. Handles mirror effect handles (id, isAlive, light(), set, update, remove/close):

light.update(value -> value.at(nextPosition).brightness(nextBrightness));
light.close();

Entity-attached lights

The service owns common attachments. Attached point lights follow an entity position, attached spotlights also follow its look direction, and connected beams follow two endpoints:

PointLightHandle aura = NebulonLights.lights().attach(
world, player, new Vec3d(0, 1.0, 0), pointTemplate);

SpotLightHandle flashlight = NebulonLights.lights().attach(
world, player, new Vec3d(0, 1.6, 0), spotTemplate);

BeamLightHandle tether = NebulonLights.lights().connect(
world, source, Vec3d.ZERO, target, new Vec3d(0, 1, 0), beamTemplate);

Offsets use world axes. Removing an entity, changing worlds, or closing the returned handle removes the binding.

note

An attached point light makes a player illuminate themselves and nearby surfaces. Selectively glowing texture pixels are an emissive entity-material feature, not a light shape, and would require a separate texture-mask hook.


Shadows

World-space shadows (stable for blocks)

Each selected light receives a bounded 32×32×32 client-world occupancy volume, so a block keeps casting its shadow even when the light or the lit side is off-screen:

PointLight shadowed = NebulonLights.point(position)
.radius(8)
.shadows(LightShadow.worldSpace(0.85f, 16).bias(0.04f))
.build();

Receivers snap to a world-aligned shadow-texel grid. The default density of 16 texels per block matches Minecraft's 16×16 block textures and stays stable as the camera moves; pass 1–16 for deliberately chunkier steps (the overload without density uses 16).

Block-update packets invalidate overlapping occupancy volumes, which rebuild before the next frame. The atlas also refreshes every 20 client ticks as a safety net (-Dnebulon.lights.worldShadowRefreshTicks=<ticks>) and rebuilds immediately when its light crosses a block boundary. Geometry outside the local volume falls back to the screen-space trace.

Screen-space shadows

For effects that prefer no CPU world sampling:

.shadows(LightShadow.screenSpace(0.85f, 8).bias(0.03f))

Strength is in [0, 1]; the screen-space sample count is capped at 16. Screen-space shadows respond to visible blocks and entities but can leak across thin or off-screen geometry.

Shadow budget

At most two shadowed lights per frame by default, hard-capped at eight. Override with -Dnebulon.lights.maxShadowed=<count>.


Renderer and performance policy

The backend copies scene depth once per frame, reconstructs surface positions and discontinuity-safe normals, and accumulates all selected lights in one additive fullscreen pass (Lambert response with a smooth finite-range inverse-square falloff). World-shadowed lights add a bounded voxel DDA lookup in a shared occupancy atlas. No per-light meshes are allocated or drawn.

Selection: lights beyond their render distance are removed first; the rest rank by influence radius, brightness, and camera distance. The default frame budget is 16 lights (-Dnebulon.lights.maxVisible=<count>), hard-clamped to the shader capacity of 32.

Current boundaries:

  • Point, spot, and beam only; area and directional lights are future types.
  • World-space shadows use hard voxel edges and treat any non-empty block collision cell as occupied — partial blocks cast blockier shadows. Entities are shadowed only by the screen-space path.
  • Normals reconstructed from depth are approximate on very thin or translucent geometry.
  • Translucent particles that do not write depth are not independently relit.
  • With a shader pack active, lighting uses Nebulon's late custom pass; each Iris pack still deserves visual testing.

Diagnostics

NebulonLights.lights().stats() reports active, visible, rendered, distance-culled, and budget-culled lights, shadow usage and shadow-budget culls, rendered frames, and depth copies. Press F8 in a development client for the live view.