Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit61a33237e0

readme.md

proxyOcclusion system

Occlusion from coarse proxy shapes, evaluated analytically instead of by rendering a depth map.

Shadowing is available only where a shadow map covers the pixel. Past the shadow range there is none at all, so a character standing out there receives nothing to ground it and reads as pasted onto the surface rather than resting on it. There is no other representation of where things are — the only alternative is a full shadow-map render, which is far too expensive to spend on a coarse contact.

A proxy is a capsule standing in for an entity's bulk. Cost scales with the number of proxies rather than with scene triangles, so the distance one works at is bounded by nothing.

Using it

A proxy that should follow an entity is a component:

local id = entity.spawn("boulder").id
entity(id).component.add("OcclusionProxy", {
    radius = 2.0,   -- capsule radius, world units
    height = 3.0,   -- axis length along world up; 0 gives a sphere
})

Every field is live, and the proxy follows its entity every frame, so a moving subject keeps its grounding without popping. A radius of 0 removes it.

For code placing proxies directly — a crowd system, a destruction event, anything without an entity to hang a component on — reach for the module:

local proxyOcclusion = require("@builtin::systems.proxyOcclusion.proxyOcclusion")

proxyOcclusion.set("boulder", { a = { 0, 2, 0 }, radius = 2 })
proxyOcclusion.set("limb", { a = { 0, 1, 0 }, b = { 0, 3, 0 }, radius = 0.4 })
proxyOcclusion.remove("boulder")
proxyOcclusion.configure({ intensity = 0.8, minDistance = 40 })
proxyOcclusion.clear()

Proxies are keyed by a caller-chosen string, and re-submitting a key moves that proxy rather than adding another.

What the knobs do

fieldeffect
radiusCapsule radius, world units. 0 removes the proxy.
heightLength of the capsule's axis along world up. 0 gives a sphere.
offsetX / offsetY / offsetZOffset of the capsule centre from the entity's origin.
strengthHow strongly one proxy occludes, in [0, 1].
intensityHow much of the occlusion reaches the image. 0 releases the pass.
powerContrast curve on the occlusion term. Above 1 keeps the falloff tighter to the proxy.
minDistanceDistance from the camera before occlusion applies — use it to leave near geometry to the shadow maps.
maxDistanceDistance past which occlusion stops applying. 0 means no limit.

At most 64 proxies are held at once, because the shader loops every proxy for every pixel. Past that the honest answer is a different technique, not a longer loop.

How it renders

capsuleOcclusion.renderFeature runs one compute pass after lighting, integrating the solid angle each capsule subtends over the hemisphere at every pixel. The sphere term is exact rather than the common inverse-square approximation, which collapses near and inside the proxy — exactly where a grounding contact is.

Proxies occlude independently, so what survives them all is the product of what survives each rather than the sum.

A pixel's existing light is estimated from its lit colour against its albedo, and the occlusion is scaled by it, so a surface already in shadow-map shadow is not darkened twice.

With no proxies registered the pass is released entirely, so a scene that authors none renders exactly as it did.

Files

proxyOcclusion.package/
  OcclusionProxy.component/       -- a proxy that follows an entity
  proxyOcclusion.module/          -- the proxy registry and settings
  capsuleOcclusion.renderFeature/ -- the compute pass