Builtin Library
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
| field | effect |
|---|---|
radius | Capsule radius, world units. 0 removes the proxy. |
height | Length of the capsule's axis along world up. 0 gives a sphere. |
offsetX / offsetY / offsetZ | Offset of the capsule centre from the entity's origin. |
strength | How strongly one proxy occludes, in [0, 1]. |
intensity | How much of the occlusion reaches the image. 0 releases the pass. |
power | Contrast curve on the occlusion term. Above 1 keeps the falloff tighter to the proxy. |
minDistance | Distance from the camera before occlusion applies — use it to leave near geometry to the shadow maps. |
maxDistance | Distance 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