Pretty cool, I tried optimizing it and getting it working with editable images and got a solid 60ish fps
Code
--!native
--!optimize 2
--!strict
local Vector3new = Vector3.new
local Color3new = Color3.new
local Color3fromRGB = Color3.fromRGB
local Vector2new = Vector2.new
local mathfloor = math.floor
local mathsqrt = math.sqrt
local mathcos = math.cos
local mathsin = math.sin
local mathlog = math.log
local mathmax = math.max
local mathabs = math.abs
local bufferwriteu8 = buffer.writeu8
local buffercreate = buffer.create
local workspaceRaycast = workspace.Raycast
local AssetService = game:GetService("AssetService")
-- Constants
local SCREEN_WIDTH = 101
local SCREEN_HEIGHT = 101
local PIXEL_SIZE = 1.0
local MAX_DISTANCE = 5000
local G = 10
local C = 300.0
local C2 = 90000.0 -- C * C
local C3 = 27000000.0 -- C * C * C
local C4 = 8100000000.0 -- C * C * C * C
local USE_2PN = true
local SOFTENING = 1
local STRONG_SWITCH_FACTOR = 100
local WRAP_THRESHOLD = 2.827433388 -- math.pi * 0.9
local EPSILON = 1e-9
local EPSILON_SQ = 1e-18
local PI_15_4 = 11.78097245096 -- (15 * math.pi) / 4
local SCREEN_CENTER_X = 50.0 -- (SCREEN_WIDTH - 1) / 2
local SCREEN_CENTER_Y = 50.0 -- (SCREEN_HEIGHT - 1) / 2
local TOTAL_PIXELS = 10201 -- SCREEN_WIDTH * SCREEN_HEIGHT
local BUFFER_SIZE = 40804 -- TOTAL_PIXELS * 4
local ScreenPart = workspace:FindFirstChild("ScreenPart")
if not ScreenPart then
ScreenPart = Instance.new("Part")
ScreenPart.Name = "ScreenPart"
ScreenPart.Size = Vector3new(SCREEN_WIDTH * PIXEL_SIZE, SCREEN_HEIGHT * PIXEL_SIZE, 0.1)
ScreenPart.Position = Vector3new(0, 0, 0)
ScreenPart.Anchored = true
ScreenPart.CanCollide = false
ScreenPart.Parent = workspace
end
local SurfaceGui = ScreenPart:FindFirstChild("ScreenGui")
if not SurfaceGui then
SurfaceGui = Instance.new("SurfaceGui")
SurfaceGui.Name = "ScreenGui"
SurfaceGui.Face = Enum.NormalId.Front
SurfaceGui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud
SurfaceGui.PixelsPerStud = 1 / PIXEL_SIZE
SurfaceGui.Parent = ScreenPart
end
local ImageLabel = SurfaceGui:FindFirstChild("Screen")
if not ImageLabel then
ImageLabel = Instance.new("ImageLabel")
ImageLabel.Name = "Screen"
ImageLabel.Size = UDim2.fromScale(1, 1)
ImageLabel.BackgroundTransparency = 1
ImageLabel.Parent = SurfaceGui
end
local EditableImage = AssetService:CreateEditableImage({
Size = Vector2new(SCREEN_WIDTH, SCREEN_HEIGHT)
})
ImageLabel.ImageContent = Content.fromObject(EditableImage)
local OriginPart = workspace:FindFirstChild("Origin")
if not OriginPart then
OriginPart = Instance.new("Part")
OriginPart.Name = "Origin"
OriginPart.Size = Vector3new(1, 1, 1)
OriginPart.Transparency = 1
OriginPart.Anchored = true
OriginPart.CanCollide = false
OriginPart.Parent = workspace
end
OriginPart.Position = Vector3new(0, 0, -1000)
-- Preallocate pixel data arrays
local PixelDirs = table.create(TOTAL_PIXELS)
-- Precompute pixel directions
local function ComputePixelDirs()
local O = OriginPart.Position
local OX, OY, OZ = O.X, O.Y, O.Z
local ScreenPos = ScreenPart.Position
local SPX, SPY, SPZ = ScreenPos.X, ScreenPos.Y, ScreenPos.Z
local Epsilon = EPSILON
local Index = 1
local CenterX, CenterY = SCREEN_CENTER_X, SCREEN_CENTER_Y
local PixelSize = PIXEL_SIZE
for Y = 0, SCREEN_HEIGHT - 1 do
for X = 0, SCREEN_WIDTH - 1 do
local WorldX = SPX + (X - CenterX) * PixelSize
local WorldY = SPY + (CenterY - Y) * PixelSize
local VX = WorldX - OX
local VY = WorldY - OY
local VZ = SPZ - OZ
local M = mathsqrt(VX * VX + VY * VY + VZ * VZ)
if M > Epsilon then
local InvM = 1 / M
PixelDirs[Index] = Vector3new(VX * InvM, VY * InvM, VZ * InvM)
else
PixelDirs[Index] = Vector3new(0, 0, 1)
end
Index += 1
end
end
end
-- Find dominant BH
local function FindDominantBH()
local SC = workspace:FindFirstChild("Scenario")
if not SC then return nil end
local Best, BestMass = nil, -math.huge
for _, P in ipairs(SC:GetChildren()) do
if P:IsA("BasePart") then
local NV = P:FindFirstChild("MassV")
local M = if NV and typeof(NV.Value) == "number" then NV.Value else P:GetMass()
if M > BestMass then
BestMass, Best = M, P
end
end
end
return Best
end
local BHPart = FindDominantBH()
if not BHPart then
warn("No BH part found")
return
end
local BH_Pos = BHPart.Position
local BH_PosX, BH_PosY, BH_PosZ = BH_Pos.X, BH_Pos.Y, BH_Pos.Z
local BH_GM, BH_GM_C2, BH_Bcrit, BH_BcritSq
local BH_Alpha1Coeff, BH_Alpha2Coeff, BH_AlphaQCoeff
local BH_Bswitch, BH_BswitchSq, BH_A, BH_B
local BH_SpinCoeff, BH_JaxisX, BH_JaxisY, BH_JaxisZ
local BH_HasCharge = false
local function BuildBH()
BH_Pos = BHPart.Position
BH_PosX, BH_PosY, BH_PosZ = BH_Pos.X, BH_Pos.Y, BH_Pos.Z
local MassNV = BHPart:FindFirstChild("MassV")
local M = if MassNV and typeof(MassNV.Value) == "number" then MassNV.Value else BHPart:GetMass()
local ChargeNV = BHPart:FindFirstChild("Charge")
local Q = if ChargeNV and typeof(ChargeNV.Value) == "number" then ChargeNV.Value else 0
BH_HasCharge = mathabs(Q) > 0
local SpinNV = BHPart:FindFirstChild("SpinParam")
local SpinParam = if SpinNV and typeof(SpinNV.Value) == "number" then SpinNV.Value else 0
-- Precomputed constants
BH_GM = G * M
BH_GM_C2 = BH_GM / C2
BH_Bcrit = 3 * mathsqrt(3) * BH_GM_C2
BH_BcritSq = BH_Bcrit * BH_Bcrit
BH_Alpha1Coeff = 4 * BH_GM / C2
BH_Alpha2Coeff = if USE_2PN then PI_15_4 * (BH_GM * BH_GM) / C4 else 0
BH_AlphaQCoeff = Q * Q
-- Strong-field matching
local Bc = BH_Bcrit
local Bswitch = mathmax(Bc * STRONG_SWITCH_FACTOR, Bc + EPSILON)
local BswitchSq = Bswitch * Bswitch
local Alpha1 = BH_Alpha1Coeff / (Bswitch + SOFTENING)
local Alpha2 = if USE_2PN then BH_Alpha2Coeff / (BswitchSq + SOFTENING) else 0
local AlphaQ = if BH_HasCharge then BH_AlphaQCoeff / (BswitchSq + SOFTENING) else 0
local Alphaw = Alpha1 + Alpha2 + AlphaQ
local Dalpha1 = -BH_Alpha1Coeff / BswitchSq
local Dalpha2 = if USE_2PN then -2 * BH_Alpha2Coeff / (BswitchSq * Bswitch) else 0
local DalphaQ = if BH_HasCharge then -2 * BH_AlphaQCoeff / (BswitchSq * Bswitch) else 0
local Dalphaw = Dalpha1 + Dalpha2 + DalphaQ
local A = -(Bswitch - Bc) * Dalphaw
local Arg = mathmax(EPSILON, (Bswitch / Bc) - 1)
local B = Alphaw + A * mathlog(Arg)
BH_Bswitch = Bswitch
BH_BswitchSq = BswitchSq
BH_A = A
BH_B = B
-- Kerr spin
local Jmag = SpinParam * (BH_GM * M) / C
BH_SpinCoeff = if Jmag > 0 then (4 * G * Jmag) / C3 else 0
local Axis = BHPart.CFrame.LookVector
local AxisMag = mathsqrt(Axis.X * Axis.X + Axis.Y * Axis.Y + Axis.Z * Axis.Z)
if AxisMag > EPSILON then
local InvAxisMag = 1 / AxisMag
BH_JaxisX = Axis.X * InvAxisMag
BH_JaxisY = Axis.Y * InvAxisMag
BH_JaxisZ = Axis.Z * InvAxisMag
else
BH_JaxisX, BH_JaxisY, BH_JaxisZ = 0, 1, 0
end
end
BuildBH()
ComputePixelDirs()
-- Raycast params
local RPMain, RPOcc
local function MakeRPMain()
local RP = RaycastParams.new()
RP.FilterType = Enum.RaycastFilterType.Blacklist
local Blacklist = {ScreenPart, OriginPart}
local SC = workspace:FindFirstChild("Scenario")
if SC then table.insert(Blacklist, SC) end
RP.FilterDescendantsInstances = Blacklist
return RP
end
local function MakeRPOcclusion()
local RP = RaycastParams.new()
RP.FilterType = Enum.RaycastFilterType.Blacklist
RP.FilterDescendantsInstances = {ScreenPart, OriginPart}
return RP
end
RPMain = MakeRPMain()
RPOcc = MakeRPOcclusion()
-- Optimized Rodrigues rotation (inlinede)
local function RotateVector(VX: number, VY: number, VZ: number,
AX: number, AY: number, AZ: number, Angle: number): (number, number, number)
local CosA = mathcos(Angle)
local SinA = mathsin(Angle)
local Dot = AX * VX + AY * VY + AZ * VZ
local CrossX = AY * VZ - AZ * VY
local CrossY = AZ * VX - AX * VZ
local CrossZ = AX * VY - AY * VX
local OneMinusCos = 1 - CosA
return VX * CosA + CrossX * SinA + AX * Dot * OneMinusCos, VY * CosA + CrossY * SinA + AY * Dot * OneMinusCos, VZ * CosA + CrossZ * SinA + AZ * Dot * OneMinusCos
end
-- Alpha calculation (inlined)
local function AlphaOfB(B: number): number
if B <= BH_Bswitch then
local Arg = mathmax(EPSILON, (B / BH_Bcrit) - 1)
return -BH_A * mathlog(Arg) + BH_B
else
local BSq = B * B
local Alpha1 = BH_Alpha1Coeff / (B + SOFTENING)
local Alpha2 = if USE_2PN then BH_Alpha2Coeff / (BSq + SOFTENING) else 0
local AlphaQ = if BH_HasCharge then BH_AlphaQCoeff / (BSq + SOFTENING) else 0
return Alpha1 + Alpha2 + AlphaQ
end
end
-- Extract color (inlined)
local function GetColor(Inst: Instance): (number, number, number)
if Inst:IsA("BasePart") then
local C = Inst.Color
return C.R, C.G, C.B
elseif Inst.Parent and Inst.Parent:IsA("BasePart") then
local C = Inst.Parent.Color
return C.R, C.G, C.B
else
return 1, 1, 1
end
end
-- Main ray tracing
local function SampleRay(KdirX: number, KdirY: number, KdirZ: number,
O: Vector3, OX: number, OY: number, OZ: number): (number, number, number)
-- Early setup
local RelX = BH_PosX - OX
local RelY = BH_PosY - OY
local RelZ = BH_PosZ - OZ
local Proj = RelX * KdirX + RelY * KdirY + RelZ * KdirZ
if Proj > 0 then
-- Closest approach point
local ClosestX = OX + KdirX * Proj
local ClosestY = OY + KdirY * Proj
local ClosestZ = OZ + KdirZ * Proj
local BvecX = BH_PosX - ClosestX
local BvecY = BH_PosY - ClosestY
local BvecZ = BH_PosZ - ClosestZ
local BSq = BvecX * BvecX + BvecY * BvecY + BvecZ * BvecZ
-- Shadow capture check
if BSq <= BH_BcritSq then
local Closest = Vector3new(ClosestX, ClosestY, ClosestZ)
local Seg = Closest - O
local RR = workspaceRaycast(workspace, O, Seg, RPOcc)
if RR and RR.Instance then
local HitPos = RR.Position
local HitDistSq = (HitPos.X - OX) * (HitPos.X - OX) +
(HitPos.Y - OY) * (HitPos.Y - OY) +
(HitPos.Z - OZ) * (HitPos.Z - OZ)
if HitDistSq < (Proj * Proj - 0.001) then
return GetColor(RR.Instance)
end
end
return 0, 0, 0
end
-- Deflection calculation
local B = mathsqrt(BSq)
if B < EPSILON then B = EPSILON end
local InvB = 1 / B
local BhatX = BvecX * InvB
local BhatY = BvecY * InvB
local BhatZ = BvecZ * InvB
local Alpha = AlphaOfB(B)
-- Axis = Kdir x Bhat
local AxisX = KdirY * BhatZ - KdirZ * BhatY
local AxisY = KdirZ * BhatX - KdirX * BhatZ
local AxisZ = KdirX * BhatY - KdirY * BhatX
local AxisMagSq = AxisX * AxisX + AxisY * AxisY + AxisZ * AxisZ
local DirX, DirY, DirZ
if AxisMagSq > EPSILON_SQ then
local AxisMag = mathsqrt(AxisMagSq)
local InvAxisMag = 1 / AxisMag
DirX, DirY, DirZ = RotateVector(KdirX, KdirY, KdirZ,
AxisX * InvAxisMag, AxisY * InvAxisMag, AxisZ * InvAxisMag, Alpha)
else
-- Fallback axis
local AltX, AltY, AltZ
if mathabs(KdirX) > 0.9 then
AltX, AltY, AltZ = 0, 1, 0
else
AltX, AltY, AltZ = 1, 0, 0
end
local FBX = KdirY * AltZ - KdirZ * AltY
local FBY = KdirZ * AltX - KdirX * AltZ
local FBZ = KdirX * AltY - KdirY * AltX
local FBMag = mathsqrt(FBX * FBX + FBY * FBY + FBZ * FBZ)
local InvFBMag = 1 / FBMag
DirX, DirY, DirZ = RotateVector(KdirX, KdirY, KdirZ,
FBX * InvFBMag, FBY * InvFBMag, FBZ * InvFBMag, Alpha)
end
-- Spin deflection
if BH_SpinCoeff > 0 then
local SpinAxisX = BH_JaxisY * KdirZ - BH_JaxisZ * KdirY
local SpinAxisY = BH_JaxisZ * KdirX - BH_JaxisX * KdirZ
local SpinAxisZ = BH_JaxisX * KdirY - BH_JaxisY * KdirX
local SpinMagSq = SpinAxisX * SpinAxisX + SpinAxisY * SpinAxisY + SpinAxisZ * SpinAxisZ
if SpinMagSq > EPSILON_SQ then
local SpinMag = mathsqrt(SpinMagSq)
local InvSpinMag = 1 / SpinMag
local SpinAngle = BH_SpinCoeff / (BSq + SOFTENING)
DirX, DirY, DirZ = RotateVector(DirX, DirY, DirZ,
SpinAxisX * InvSpinMag, SpinAxisY * InvSpinMag, SpinAxisZ * InvSpinMag, SpinAngle)
end
end
-- Final raycast
local Dir = Vector3new(DirX, DirY, DirZ)
local RR = workspaceRaycast(workspace, O, Dir * MAX_DISTANCE, RPMain)
if RR and RR.Instance then
return GetColor(RR.Instance)
else
return 0, 0, 0
end
else
-- No deflection
local Kdir = Vector3new(KdirX, KdirY, KdirZ)
local RR = workspaceRaycast(workspace, O, Kdir * MAX_DISTANCE, RPMain)
if RR and RR.Instance then
return GetColor(RR.Instance)
else
return 0, 0, 0
end
end
end
-- Render entire frame to buffer
local function RenderFrame(): buffer
local PixelBuffer = buffercreate(BUFFER_SIZE)
local O = OriginPart.Position
local OX, OY, OZ = O.X, O.Y, O.Z
local Offset = 0
local Pixels = PixelDirs
for I = 1, TOTAL_PIXELS do
local Kdir = Pixels[I]
local R, G, B = SampleRay(Kdir.X, Kdir.Y, Kdir.Z, O, OX, OY, OZ)
bufferwriteu8(PixelBuffer, Offset, mathfloor(R * 255))
bufferwriteu8(PixelBuffer, Offset + 1, mathfloor(G * 255))
bufferwriteu8(PixelBuffer, Offset + 2, mathfloor(B * 255))
bufferwriteu8(PixelBuffer, Offset + 3, 255)
Offset += 4
end
return PixelBuffer
end
while task.wait() do
local StartTime = os.clock()
BuildBH()
RPMain = MakeRPMain()
RPOcc = MakeRPOcclusion()
local PixelBuffer = RenderFrame()
EditableImage:WritePixelsBuffer(Vector2new(0, 0), Vector2new(SCREEN_WIDTH, SCREEN_HEIGHT), PixelBuffer)
local RenderTime = os.clock() - StartTime
end