This script uses multiple advanced techniques to achieve a basic raycast renderer featuring shading, volumetrics, and more.
local Players = game:GetService("Players")
local AssetService = game:GetService("AssetService")
local RunService = game:GetService("RunService")
local Lighting = game:GetService("Lighting")
local localPlayer = Players.LocalPlayer
local camera = workspace.CurrentCamera
-- Feature toggles
local ENABLE_LIGHTING = true -- Toggle dynamic lighting calculations
local ENABLE_VOLUMETRIC_LIGHT = true -- Toggle volumetric lighting effects
local ENABLE_SHADING = true -- Toggle shading based on normal direction
local ENABLE_COLOR_QUANTIZATION = true -- Toggle retro color quantization
local ENABLE_CROSSHAIR = true -- Toggle crosshair overlay
local ENABLE_FOG = false
-- Rendering constants
local RENDER_CONFIG = {
CANVAS_SIZE = Vector2.new(320, 240),
IMAGE_DEPTH = 300,
RAY_COUNT_HORIZONTAL = 92,
RAY_COUNT_VERTICAL = 92,
FOG_DENSITY = 0.01,
SKY_COLOR = Color3.new(0.1, 0.1, 0.2),
FLOOR_COLOR = Color3.new(0.2, 0.2, 0.2),
TARGET_COLOR = Color3.new(1, 0, 0),
SHADING_MIN = 0.2,
COLOR_QUANTIZATION_DEPTH = 40,
COLOR_QUANTIZATION_MIN = 4,
DISTANCE_PIXEL_RATIO = 50,
FLOOR_Y_POSITION = 0,
LIGHT_FALLOFF = 1.5,
AMBIENT_LIGHT = 0.15,
FULLBRIGHT_MIN = 0.6, -- Minimum brightness for unlit objects
}
-- Performance optimization
local floor, min, max, exp, clamp = math.floor, math.min, math.max, math.exp, math.clamp
local Vector3new, Color3new, Vector2new = Vector3.new, Color3.new, Vector2.new
-- Create the EditableImage once
local editableImage = AssetService:CreateEditableImage({Size = RENDER_CONFIG.CANVAS_SIZE})
-- Create GUI elements
local screenGui = Instance.new("ScreenGui")
screenGui.IgnoreGuiInset = true
screenGui.Parent = localPlayer:WaitForChild("PlayerGui")
local imageLabel = Instance.new("ImageLabel")
imageLabel.Size = UDim2.new(1, 0, 1, 0)
imageLabel.Position = UDim2.new(0.5, 0, 0.5, 0)
imageLabel.AnchorPoint = Vector2.new(0.5, 0.5)
imageLabel.BackgroundTransparency = 1
imageLabel.Parent = screenGui
-- Create raycast params once
local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
-- Light source cache
local lightSources = {}
local lastLightUpdateTime = 0
local LIGHT_UPDATE_INTERVAL = 1
-- Volumetric lighting constants
local VOLUME_SAMPLES = 3 -- Reduced for performance
local SCATTERING_COEFFICIENT = 0.02
local VOLUME_STEP_MULTIPLIER = 1/VOLUME_SAMPLES
-- Precalculate ray directions
local rayDirections = {}
for i = 0, RENDER_CONFIG.RAY_COUNT_HORIZONTAL - 1 do
rayDirections[i] = {}
for j = 0, RENDER_CONFIG.RAY_COUNT_VERTICAL - 1 do
local rayX = (i / RENDER_CONFIG.RAY_COUNT_HORIZONTAL - 0.5) * 2
local rayY = ((RENDER_CONFIG.RAY_COUNT_VERTICAL - j) / RENDER_CONFIG.RAY_COUNT_VERTICAL - 0.5) * 2
rayDirections[i][j] = Vector3new(rayX, rayY, -1).Unit
end
end
-- Create pixel buffer
local pixelBufferSize = RENDER_CONFIG.CANVAS_SIZE.X * RENDER_CONFIG.CANVAS_SIZE.Y * 4
local pixelBuffer = buffer.create(pixelBufferSize)
-- Utility functions
local function calculateLightAttenuation(distance, range)
if distance >= range then return 0 end
local normalizedDist = distance / range
return (1 - normalizedDist^RENDER_CONFIG.LIGHT_FALLOFF)
end
local function updateLightSources()
lightSources = {}
for _, instance in ipairs(workspace:GetDescendants()) do
if (instance:IsA("PointLight") or instance:IsA("SpotLight") or instance:IsA("SurfaceLight")) and instance.Enabled then
local parent = instance.Parent
if parent and parent:IsA("BasePart") then
table.insert(lightSources, {
position = parent.Position,
color = instance.Color,
range = instance.Range,
brightness = instance.Brightness,
type = instance.ClassName
})
end
elseif instance:IsA("Part") and (instance.Material == Enum.Material.Neon or instance.Material == Enum.Material.CorrodedMetal) then
table.insert(lightSources, {
position = instance.Position,
color = instance.Color,
range = instance.Size.Magnitude * 2,
brightness = 2,
type = "NeonPart"
})
end
end
table.insert(lightSources, {
direction = Lighting:GetSunDirection(),
color = Lighting:GetMinutesAfterMidnight() / 60 > 6 and Lighting:GetMinutesAfterMidnight() / 60 < 18 and Lighting.OutdoorAmbient or Lighting.Ambient,
brightness = Lighting:GetMinutesAfterMidnight() / 60 > 6 and Lighting:GetMinutesAfterMidnight() / 60 < 18 and 1 or 0.5,
type = "DirectionalLight"
})
lastLightUpdateTime = tick()
end
-- Optimized volumetric lighting calculation
local function calculateVolumetricLight(rayOrigin, rayDir, rayLength)
if not ENABLE_VOLUMETRIC_LIGHT then return 0, 0, 0 end
local vR, vG, vB = 0, 0, 0
local stepSize = rayLength * VOLUME_STEP_MULTIPLIER
for _, light in ipairs(lightSources) do
if light.type ~= "DirectionalLight" then
local lightPos = light.position
local lightRange = light.range
local lightR = light.color.R * light.brightness
local lightG = light.color.G * light.brightness
local lightB = light.color.B * light.brightness
for i = 1, VOLUME_SAMPLES do
local sampleDistance = i * stepSize
local sampleX = rayOrigin.X + rayDir.X * sampleDistance
local sampleY = rayOrigin.Y + rayDir.Y * sampleDistance
local sampleZ = rayOrigin.Z + rayDir.Z * sampleDistance
local dirX = lightPos.X - sampleX
local dirY = lightPos.Y - sampleY
local dirZ = lightPos.Z - sampleZ
local distToLight = (dirX*dirX + dirY*dirY + dirZ*dirZ)^0.5
if distToLight < lightRange then
local attenuation = calculateLightAttenuation(distToLight, lightRange)
local scatteringFactor = exp(-sampleDistance * 0.01) * attenuation * SCATTERING_COEFFICIENT
vR = vR + lightR * scatteringFactor
vG = vG + lightG * scatteringFactor
vB = vB + lightB * scatteringFactor
end
end
end
end
return vR, vG, vB
end
-- Optimized lighting calculation
local function calculateLighting(position, normal, baseColor)
if not ENABLE_LIGHTING then
return baseColor -- Return flat color if lighting is disabled
end
local totalR, totalG, totalB = RENDER_CONFIG.AMBIENT_LIGHT, RENDER_CONFIG.AMBIENT_LIGHT, RENDER_CONFIG.AMBIENT_LIGHT
for _, light in ipairs(lightSources) do
if light.type == "DirectionalLight" then
local nDotL = max(0, normal:Dot(-light.direction))
totalR = totalR + light.color.R * nDotL * light.brightness
totalG = totalG + light.color.G * nDotL * light.brightness
totalB = totalB + light.color.B * nDotL * light.brightness
else
local lightDir = light.position - position
local distToLight = lightDir.Magnitude
if distToLight < light.range then
local nDotL = max(0, normal:Dot(lightDir.Unit))
local attenuation = calculateLightAttenuation(distToLight, light.range)
local factor = nDotL * light.brightness * attenuation
totalR = totalR + light.color.R * factor
totalG = totalG + light.color.G * factor
totalB = totalB + light.color.B * factor
end
end
end
-- Ensure minimum brightness for unlit objects
totalR = max(totalR, RENDER_CONFIG.FULLBRIGHT_MIN)
totalG = max(totalG, RENDER_CONFIG.FULLBRIGHT_MIN)
totalB = max(totalB, RENDER_CONFIG.FULLBRIGHT_MIN)
return Color3new(
clamp(baseColor.R * totalR, 0, 1),
clamp(baseColor.G * totalG, 0, 1),
clamp(baseColor.B * totalB, 0, 1)
)
end
-- Simplified color operations
local function applyShading(color, shadeFactor)
if not ENABLE_SHADING then return color end
shadeFactor = max(shadeFactor, RENDER_CONFIG.FULLBRIGHT_MIN) -- Ensure minimum brightness
return Color3new(color.R * shadeFactor, color.G * shadeFactor, color.B * shadeFactor)
end
local function quantizeColor(color, depth)
if not ENABLE_COLOR_QUANTIZATION then return color end
local factor = max(RENDER_CONFIG.COLOR_QUANTIZATION_MIN, floor(depth / RENDER_CONFIG.COLOR_QUANTIZATION_DEPTH))
local invFactor = 1/factor
return Color3new(
floor(color.R * factor) * invFactor,
floor(color.G * factor) * invFactor,
floor(color.B * factor) * invFactor
)
end
local function applyFog(color, distance)
if not ENABLE_FOG then return color end
local fogAmount = 1 - exp(-distance * RENDER_CONFIG.FOG_DENSITY)
return color:Lerp(RENDER_CONFIG.SKY_COLOR, fogAmount)
end
-- Optimized fill rectangle function
local function fillRect(startX, startY, endX, endY, r, g, b)
r, g, b = floor(r * 255), floor(g * 255), floor(b * 255)
for y = startY, endY do
local rowStart = y * RENDER_CONFIG.CANVAS_SIZE.X * 4
for x = startX, endX do
local index = rowStart + x * 4
buffer.writeu8(pixelBuffer, index, r)
buffer.writeu8(pixelBuffer, index + 1, g)
buffer.writeu8(pixelBuffer, index + 2, b)
buffer.writeu8(pixelBuffer, index + 3, 255) -- Alpha
end
end
end
local pixelsPerRayX = RENDER_CONFIG.CANVAS_SIZE.X / RENDER_CONFIG.RAY_COUNT_HORIZONTAL
local pixelsPerRayY = RENDER_CONFIG.CANVAS_SIZE.Y / RENDER_CONFIG.RAY_COUNT_VERTICAL
local function castRayAndRender()
if tick() - lastLightUpdateTime > LIGHT_UPDATE_INTERVAL then
updateLightSources()
end
raycastParams.FilterDescendantsInstances = {localPlayer.Character}
local cameraPos = camera.CFrame.Position
local cameraMatrix = camera.CFrame
for i = 0, RENDER_CONFIG.RAY_COUNT_HORIZONTAL - 1 do
for j = 0, RENDER_CONFIG.RAY_COUNT_VERTICAL - 1 do
local direction = cameraMatrix:VectorToWorldSpace(rayDirections[i][j])
local raycastResult = workspace:Raycast(cameraPos, direction * RENDER_CONFIG.IMAGE_DEPTH, raycastParams)
local r, g, b, distance
if raycastResult then
local hitPart = raycastResult.Instance
local hitPos = raycastResult.Position
local hitNormal = raycastResult.Normal
distance = (hitPos - cameraPos).Magnitude
local baseColor = hitPart.Color
local litColor = ENABLE_LIGHTING and calculateLighting(hitPos, hitNormal, baseColor) or baseColor
local shadeFactor = ENABLE_SHADING and max(RENDER_CONFIG.SHADING_MIN, hitNormal:Dot(-direction)) or 1
local shadedColor = applyShading(litColor, shadeFactor)
local quantizedColor = quantizeColor(shadedColor, distance)
-- Add volumetric lighting if enabled, else use base color
local vR, vG, vB = calculateVolumetricLight(cameraPos, direction, distance)
r = ENABLE_VOLUMETRIC_LIGHT and clamp(quantizedColor.R + vR, 0, 1) or quantizedColor.R
g = ENABLE_VOLUMETRIC_LIGHT and clamp(quantizedColor.G + vG, 0, 1) or quantizedColor.G
b = ENABLE_VOLUMETRIC_LIGHT and clamp(quantizedColor.B + vB, 0, 1) or quantizedColor.B
-- Apply fog if enabled
r, g, b = ENABLE_FOG and applyFog(Color3.fromRGB(r,g,b), distance) or r, g, b
else
local floorHitDistance = (RENDER_CONFIG.FLOOR_Y_POSITION - cameraPos.Y) / direction.Y
if floorHitDistance > 0 and floorHitDistance < RENDER_CONFIG.IMAGE_DEPTH then
distance = floorHitDistance
local floorHitPos = cameraPos + direction * floorHitDistance
local floorNormal = Vector3new(0, 1, 0)
local baseColor = RENDER_CONFIG.FLOOR_COLOR
local litFloorColor = ENABLE_LIGHTING and calculateLighting(floorHitPos, floorNormal, baseColor) or baseColor
local shadeFactor = ENABLE_SHADING and max(RENDER_CONFIG.SHADING_MIN, floorNormal:Dot(-direction)) or 1
local shadedFloorColor = applyShading(litFloorColor, shadeFactor)
local quantizedColor = quantizeColor(shadedFloorColor, distance)
-- Add volumetric lighting for floor if enabled
local vR, vG, vB = calculateVolumetricLight(cameraPos, direction, distance)
r = ENABLE_VOLUMETRIC_LIGHT and clamp(quantizedColor.R + vR, 0, 1) or quantizedColor.R
g = ENABLE_VOLUMETRIC_LIGHT and clamp(quantizedColor.G + vG, 0, 1) or quantizedColor.G
b = ENABLE_VOLUMETRIC_LIGHT and clamp(quantizedColor.B + vB, 0, 1) or quantizedColor.B
-- Apply fog if enabled
r, g, b = ENABLE_FOG and applyFog(Color3new(r, g, b), distance):ToRGB() or r, g, b
else
-- For sky, use base sky color with optional volumetric lighting
local vR, vG, vB = calculateVolumetricLight(cameraPos, direction, RENDER_CONFIG.IMAGE_DEPTH)
r = ENABLE_VOLUMETRIC_LIGHT and clamp(RENDER_CONFIG.SKY_COLOR.R + vR, 0, 1) or RENDER_CONFIG.SKY_COLOR.R
g = ENABLE_VOLUMETRIC_LIGHT and clamp(RENDER_CONFIG.SKY_COLOR.G + vG, 0, 1) or RENDER_CONFIG.SKY_COLOR.G
b = ENABLE_VOLUMETRIC_LIGHT and clamp(RENDER_CONFIG.SKY_COLOR.B + vB, 0, 1) or RENDER_CONFIG.SKY_COLOR.B
distance = RENDER_CONFIG.IMAGE_DEPTH
end
end
local startX = floor(i * pixelsPerRayX)
local endX = floor((i + 1) * pixelsPerRayX) - 1
local startY = floor(j * pixelsPerRayY)
local endY = floor((j + 1) * pixelsPerRayY) - 1
fillRect(startX, startY, endX, endY, r, g, b)
end
end
editableImage:WritePixelsBuffer(Vector2new(0, 0), RENDER_CONFIG.CANVAS_SIZE, pixelBuffer)
-- Draw crosshair if enabled
if ENABLE_CROSSHAIR then
local center = Vector2new(RENDER_CONFIG.CANVAS_SIZE.X/2, RENDER_CONFIG.CANVAS_SIZE.Y/2)
editableImage:DrawLine(Vector2new(center.X - 10, center.Y), Vector2new(center.X + 10, center.Y), RENDER_CONFIG.TARGET_COLOR, 0, Enum.ImageCombineType.Overwrite)
editableImage:DrawLine(Vector2new(center.X, center.Y - 10), Vector2new(center.X, center.Y + 10), RENDER_CONFIG.TARGET_COLOR, 0, Enum.ImageCombineType.Overwrite)
end
imageLabel.ImageContent = Content.fromObject(editableImage)
end
updateLightSources()
RunService.RenderStepped:Connect(castRayAndRender)
Author: StarVSK
Posted: March 25, 2025