Performant way to mark dark regions?

What are some theoretically and realistically (in the context of Roblox) performant approaches to caching/marking unilluminated 3D regions? To increase the difficulty, consider:

  • validating if an object can fit within a space connected to the closest “dark” region
  • illumination thresholds
  • dynamic light sources

I was inspired by this post to make this question.

Any info helps, thanks.

Roblox doesn’t expose a TRUE “light level at a point.” There’s no API to ask “how bright is it here?” If you need darkness/brightness logic, you’ll have to approximate it and keep your own cache.

I recommend using Coarse voxel lighting cache
and using get parts inbound and overlaps params for fitting and using Model:GetExtentsSize() for models

Heres a script I made that demonstrates it kind of i’m not sure if it works though


-- Very simple light estimate: sum of enabled Point/Spot/Surface lights with ray occlusion.
local function estimateLuminanceAtPoint(p: Vector3)
	local L = 0
	for _, l in ipairs(workspace:GetDescendants()) do
		if l.Enabled and (l:IsA("PointLight") or l:IsA("SpotLight") or l:IsA("SurfaceLight")) then
			local parentPart = l.Parent
			if parentPart and parentPart:IsA("BasePart") then
				local origin = parentPart.Position
				local dir = p - origin
				local dist = dir.Magnitude
				-- crude influence bounds; skip far points to save time
				if dist <= (l.Range or 60) then
					-- occlusion check
					local hit = workspace:Raycast(origin, dir, RaycastParams.new())
					if not hit or (hit.Position - origin).Magnitude >= dist - 0.01 then
						local spotMul = 1
						if l:IsA("SpotLight") then
							local fwd = parentPart.CFrame.LookVector
							local cosT = math.clamp((dir.Unit):Dot(fwd), -1, 1)
							local deg = math.deg(math.acos(cosT))
							if deg > (l.Angle/2) then
								spotMul = 0
							else
								spotMul = 1 - (deg/(l.Angle/2))
							end
						end
						local falloff = 1 / (dist*dist + 1)
						L += (l.Brightness or 1) * falloff * spotMul
					end
				end
			end
		end
	end
	-- add ambient (very rough)
	local amb = (Lighting.Ambient.R + Lighting.Ambient.G + Lighting.Ambient.B)/3
	local outAmb = (Lighting.OutdoorAmbient.R + Lighting.OutdoorAmbient.G + Lighting.OutdoorAmbient.B)/3
	return L + amb + outAmb
end

-- Average luminance over an AABB with N^3 samples
local function areaLuminance(cf: CFrame, size: Vector3, N: number)
	local half = size/2
	local sum, count = 0, 0
	for x = 0, N-1 do
		for y = 0, N-1 do
			for z = 0, N-1 do
				local u = (x+0.5)/N*2-1
				local v = (y+0.5)/N*2-1
				local w = (z+0.5)/N*2-1
				local worldPos = cf:PointToWorldSpace(Vector3.new(u*half.X, v*half.Y, w*half.Z))
				sum += estimateLuminanceAtPoint(worldPos)
				count += 1
			end
		end
	end
	return sum / math.max(1, count)
end

-- Fit test using bounding box overlap
local function canFit(cf: CFrame, size: Vector3, clearance: number, overlapParams: OverlapParams?)
	local padded = size + Vector3.new(clearance, clearance, clearance)
	local parts = workspace:GetPartBoundsInBox(cf, padded, overlapParams)
	return #parts == 0
end

-- Example usage:
-- local isDark = areaLuminance(targetCF, targetSize, 3) < 0.25
-- local fits   = canFit(targetCF, model:GetExtentsSize(), 0.5, overlapParams)
'''
1 Like

In Roblox handling unilluminated regions efficiently is weird, especially if you want dynamic interaction with lights and objects.

One approach is to discretize your world into a voxel grid or chunks. Each voxel can store a simple light value or a binary “illuminated/unilluminated” flag. You can update these values incrementally when lights move or change intensity instead of recalculating the whole scene every frame…

For fitting objects into dark spaces, you can combine the voxel grid with a simple spatial query. For example mark clusters of contiguous dark voxels and check bounding boxes against those clusters. This reduces expensive raycasting across the entire scene. To account for illumination thresholds, store light intensity per voxel and treat anything below a certain value as dark so you can dynamically adjust when lights fade or objects cast shadows.

local voxelSize = 4 -- size of each voxel
local threshold = 0.2 -- light intensity threshold for "dark"

local grid = {} -- grid[x][y][z] = lightLevel

-- initialize a 10x10x10 grid with random light values
for x = 1,10 do
    grid[x] = {}
    for y = 1,10 do
        grid[x][y] = {}
        for z = 1,10 do
            grid[x][y][z] = math.random() -- light intensity between 0 and 1
        end
    end
end

-- check if a voxel thingi region is "dark"
local function isDark(x, y, z)
    return grid[x] and grid[x][y] and grid[x][y][z] and grid[x][y][z] < threshold
end

-- check if an object can fit in a dark space (bounding box in voxels ofc)
local function canFitInDark(x, y, z, sizeX, sizeY, sizeZ)
    for i = x, x + sizeX - 1 do
        for j = y, y + sizeY - 1 do
            for k = z, z + sizeZ - 1 do
                if not isDark(i, j, k) then
                    return false
                end
            end
        end
    end
    return true
end

-- example
local startX, startY, startZ = 2, 2, 2
local objSizeX, objSizeY, objSizeZ = 2, 2, 2

if canFitInDark(startX, startY, startZ, objSizeX, objSizeY, objSizeZ) then
    print("Object can fit in this dark region")
else
    print("Region is too bright or blocked")
end

This script shows the basic idea of a grid stores light intensity, voxels below the threshold are dark and you can check if a space is contiguous and dark enough to fit an object!


Dynamic lights can be handled using a localized update strategy. When a light moves only update the voxels within its effective radius.


For performance, consider approximating light falloff rather than computing per-pixel illumination. You might also cache “dark regions” per chunk and refresh them on-demand when nearby lights change which avoids global recomputation.

This approach balances theoretical accuracy with Robloxs runtime limitations. The grid resolution and chunk size are key tuning parameters. Too coarse and you lose fidelity for object placement, too fine and you start hitting memory and update overhead.

Hope it this project works out well!!!

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.