Light detection revamp - Optimized and more accurate

It’s been awhile since i have made any changes to the Light detection module i made.

It was working to some extended but after a while i found numerous bugs and frame drops due to the use of unoptimized functions and events.

I did a few updates as you some of you know. For example i made LightDetectionV2 which did some minor optimizations and added a few new methods. The problem is there is no documentation on how it works (since the base was rescripted). After awhile it broke due to roblox updating.

I was still not satisifed with the results so i chose to make ANOTHER version which at the end i didnt publish just because of how messy it was.

A few months Later and i chose to REVAMP it.

The new version, compared to the latest one (Version 3) is WAY faster

The changes i made are changing the math from linear algebra to using solid geometry.

it works just as the first version with just 1 new method

Anyway enough talking

local module = {}

local Lighting = game.Lighting
local Run_Service = game:GetService("RunService")

local function SpotLight(x : Vector3,dir : Vector3,p : Vector3,ang : number,range : number) -- Cone
	ang = ang > 0 and ang or 1
	local A = CFrame.lookAt(x,x + dir) * CFrame.Angles(0,math.rad(ang/2),0) *,0,-range)
	local B = CFrame.lookAt(x,x + dir) * CFrame.Angles(0,math.rad(-ang/2),0) *,0,-range)
	local r = (A.Position - B.Position).Magnitude/2
	local midPoint = A:Lerp(B,0.5).Position
	local h = (midPoint - x).Magnitude
	local dist = (p - x):Dot(dir)
	local cone_radius = (dist / h) * r
	local dist2 = ((p - x) - dist * dir).Magnitude
	return (dist2 < cone_radius) 

local function Side4D(p1,p2,p3,p4,p)
	local dp = (p1+p2+p3+p4)/4

	local up = (p2 - p1)
	local right = (p2 - p4)

	local dir = up:Cross(right)

	local dir2 = (dp - p)
	local dot = dir:Dot(dir2)

	return dot >= 1
local function SurfaceLight(p1,p2,p3,p4,p5,p6,p7,p8,p)
	local x,x1,x2,x3,x4,x5 = Side4D(p1,p2,p3,p4,p) ,Side4D(p1,p5,p6,p2,p) ,Side4D(p2,p6,p7,p3,p) ,Side4D(p3,p7,p8,p4,p) ,Side4D(p4,p8,p5,p1,p) ,Side4D(p5,p8,p7,p6,p) 

	return x and x1 and x2 and x3 and x4 and x5

local function PointLight(x,p,r)
	return x:Dot(p) < r^2

local function CheckObfuscated(t)
	for i,part : BasePart in t do
		if part.CastShadow == false then
		if part.Transparency >= 0.5 then
	return t

local function GetVectorFromNormal(BasePart : BasePart | Attachment,Normal)
	local Dirs = {}
	if BasePart:IsA("BasePart") then
		Dirs = {
			[Enum.NormalId.Top] = BasePart.CFrame.UpVector,
			[Enum.NormalId.Bottom] = -BasePart.CFrame.UpVector,
			[Enum.NormalId.Right] = BasePart.CFrame.RightVector,
			[Enum.NormalId.Left] = -BasePart.CFrame.RightVector,
			[Enum.NormalId.Front] = BasePart.CFrame.LookVector,
			[Enum.NormalId.Back] = -BasePart.CFrame.LookVector,
		Dirs = {
			[Enum.NormalId.Top] = BasePart.WorldCFrame.UpVector,
			[Enum.NormalId.Bottom] = -BasePart.WorldCFrame.UpVector,
			[Enum.NormalId.Right] = BasePart.WorldCFrame.RightVector,
			[Enum.NormalId.Left] = -BasePart.WorldCFrame.RightVector,
			[Enum.NormalId.Front] = BasePart.WorldCFrame.LookVector ,
			[Enum.NormalId.Back] = -BasePart.WorldCFrame.LookVector,
	return Dirs[Normal]

local function Side(v1 : Vector3,v2 : Vector3,v3 : Vector3,v4 : Vector3,p : Vector3)
	local normal =  (v2 - v1):Cross(v3 - v1)
	local dotV4 = normal:Dot(v4 - v1)
	local dotP = normal:Dot(p - v1)
	return math.sign(dotV4) == math.sign(dotP)

local function SurfaceLightAtt(v1 : Vector3,v2 : Vector3,v3 : Vector3,v4 : Vector3,p : Vector3)
	return Side(v1, v2, v3, v4, p) and
		Side(v2, v3, v4, v1, p) and
		Side(v3, v4, v1, v2, p) and
		Side(v4, v1, v2, v3, p)

local OVP =
OVP.FilterType = Enum.RaycastFilterType.Blacklist

local function CheckInSunlight(Point : Vector3)
	local sunlightDir = Lighting:GetSunDirection()
	local sp = Point + sunlightDir * 500
	local scf = CFrame.lookAt(sp,sp + sunlightDir)
	local ssize =,1,1000)
	local Det = workspace:GetPartBoundsInBox(,[1]
	OVP.FilterDescendantsInstances = {Det}
	local Sunlight = if Lighting.GlobalShadows then CheckObfuscated(workspace:GetPartBoundsInBox(scf,ssize,OVP)) else {}
	if Sunlight and 18 > Lighting.ClockTime and Lighting.ClockTime > 8 then
		return true
	return false

local AllLights : {Light} = {}

workspace.DescendantAdded:Connect(function(child : Light)
	if child:IsA("Light") then

for _,v : Light in workspace:GetDescendants() do
	if v:IsA("Light") then

export type LightObject = {
	LightLevel: number,
	Lights: {Light},
	Sunlight: boolean,
	InLight: boolean

function module:DetectLightAtObject(Object: BasePart)
	local self = {
		Part = Object,
	function self:Update()
		local info =  module:DetectLightAtPoint(Object.Position)
		return info
	return self

function module:DetectLightAtPoint(Point: Vector3)
	local Detected = {
		Lights = {},
		Sunlight = false,
		InLight = false,
		LightLevel = 0,
	} :: LightObject
	local SunLight = CheckInSunlight(Point)
	if SunLight then
		Detected.Sunlight = true
		Detected.LightLevel = 15
	for _,Light in AllLights do
		local LightParent = Light.Parent
		local LightPos = LightParent:IsA("BasePart") and LightParent.Position or (LightParent:IsA("Attachment") and LightParent.WorldPosition or nil)
		if not LightPos then continue end
		if not Light.Enabled then continue end
		local Brightness = Light.Brightness/2
		local range = Light.Range
		local magnitude = (Point - LightPos).Magnitude
		local unit = (LightPos - Point).Unit
		local p = Point:Lerp(LightPos,0.5)
		local cf = CFrame.lookAt(p,p + unit)
		local size =,1,magnitude)
		local Det = workspace:GetPartBoundsInBox(,[1]
		OVP.FilterDescendantsInstances = {Det,LightParent}
		local obfuscated = if Light.Shadows then CheckObfuscated(workspace:GetPartBoundsInBox(cf,size,OVP)) else {}
		if #obfuscated > 0 then continue end
		if Light:IsA("PointLight") then
			local InRadius = PointLight(LightPos,Point,Light.Range)
			if InRadius then
				Detected.LightLevel = math.min((Detected.LightLevel + (range - magnitude) * (1+Brightness/magnitude))*2,15)
		if Light:IsA("SpotLight") then
			local dir = GetVectorFromNormal(LightParent,Light.Face)
			local InCone = SpotLight(LightPos,dir,Point,Light.Angle,range)
			if InCone and magnitude  < range - range/10 then
				Detected.LightLevel = math.min((Detected.LightLevel + (range - magnitude) * (1+Brightness/magnitude))*2,15)
		if Light:IsA("SurfaceLight") then
			local dir = LightParent.CFrame * 
			if LightParent:IsA("BasePart") then	
				local ParentSize = LightParent.Size/2	
				local deg = Light.Angle
				local ang = math.rad(deg)
				local p1 = (dir *,-ParentSize.Y,0)).Position
				local p2 = (dir *,-ParentSize.Y,0)).Position
				local p3 = (,p2 + dir.LookVector) **10,0,-range) * CFrame.Angles(0,-ang/2,0)) *,-range*ang/2+math.max(0,deg - 45)/30,0)
				local p4 = (,p1 + dir.LookVector) **10,0,-range) * CFrame.Angles(0,ang/2,0)) *,-range*ang/2+math.max(0,deg - 45)/30,0)
				local p5 = (dir *,ParentSize.Y,0)).Position
				local p6 = (dir *,ParentSize.Y,0)).Position
				local p7 = p3.Position *,-1,1) +,p6.Y,0)
				local p8 = p4.Position *,-1,1) +,p5.Y,0)
				local InsideCube = SurfaceLight(p1,p2,p3.Position,p4.Position,p5,p6,p7,p8,Point)
				if InsideCube and magnitude < range - range/10 then
					Detected.LightLevel = math.min((Detected.LightLevel + (range - magnitude) * (1+Brightness/magnitude))*2,15)
				local dir = GetVectorFromNormal(LightParent,Light.Face)
				local InCone = SpotLight(LightPos,dir,Point,Light.Angle,range)

				if InCone and magnitude  < range - range/10 then
					Detected.LightLevel = math.min((Detected.LightLevel + (range - magnitude) * (1+Brightness/magnitude))*2,15)
	Detected.InLight = #Detected.Lights > 0 or Detected.Sunlight
	return Detected

return module
Code Example
local LightDetection = require(game.ReplicatedStorage.LightDetectionRevamp)

local Data = LightDetection:DetectLightAtPoint(,10,0)) -- returns table with contents
	LightLevel: number,
	Lights: {Light},
	Sunlight: boolean,
	InLight: boolean
local LightDetector = LightDetection:DetectLightAtObject(workspace.BasePlate) -- returns table with method :Update()
LightDetector:Update() -- returns the same thing as LightDetection:DetectLightAtPoint()

Link to the module: LightDetectionRevamp

and videos

The math can be optimized so feel free giving suggestions or just straight up making your own version of this module. I havent really tested it so please report here if there are any bugs you encontered


This is pretty cool and all but… How can a developer use it?..

I gave a code example here, you need either a Vector3 or a BasePart to detect the light on.
Thats all, if you want to customize anything you will have to dig in the module.


I feel like this can be used for automatic vehicle lights. I might check it out.

No, I meant in a game. Like for what

Hmmm not quite sure of that one

An example of using this is to make a sighting feature for stealth (horror), or hide and seek games.
Op could have said this at first place to make you understand more but whatever.

Or a game like Lightmatter, where if your not in light you die.

1 Like

Honestly, the opposite of that would be more realistic. That’s a good game idea.

1 Like

This doesn’t work for me, the part is always green, even when the part is not in light nor in the shadows.

local debugPart = workspace:WaitForChild("DebugPart")
local origin = debugPart.Position
local radius = 5
local m = require(script.ModuleScript)
local detection = m:DetectLightAtObject(debugPart)
	local now = time() * 5
	local pos = origin + * radius, 0, math.sin(now) * radius)
	debug.profileend() -- Move
	local metadata = detection:Update()
	debugPart.Color = if metadata.InLight or metadata.Sunlight then, 1, 0) else, 0, 0)
	debug.profileend() -- Scan

I also printed the data returned, InLight and Sunlight are also both true
The video attached below, is where the detection also seems to work only in the dark, and it only turns green on the right side of the light from where it’s facing.

Also, when a light is destroyed, errors occur.
Tl;dr every light besides SurfaceLight work fine, and sunlight detections do not work correctly.

1 Like

Dont copy the code from the dev forum, there was a bug that i found a few days after i made this post. The working version is the module

1 Like

Thanks for letting me know, will try