How to make a raytracer. PART 2

An indepth tutorial on how one could make a simple raytracer

by jake_4543

Author Avatar

IF YOU HAVEN'T DONE PART 1, GO TO PART 1


The actual raytracer

Good job! you have reached the final step.

All the code shown here needs to go in the "Raytracer" module script that we made.

The comments in the code should explain it, if you have more questions ask in the replies.

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local Utils = require(ReplicatedStorage:WaitForChild("Utils"))
local Settings = require(script:WaitForChild("Settings"))

local Raycast = Utils.Raycast
local Reflect = Utils.Reflect

local RequestEvent = ReplicatedStorage:WaitForChild("requestEvent")

local Player = Players.LocalPlayer
local Camera = workspace.CurrentCamera

-- Gets the direction between a and b
local function getDir(a,b)
	return (b-a).Unit
end

-- We use this to convert a Color3 to a table to be sent to the python server.
local function colToTable(col)
	return {col.R*255,col.G*255,col.B*255}
end

-- Calculates the shadow of a pixel
local function calcShadow(Color,Position,Normal,Direction,ShouldShadow)
	-- Raycast to the sun
	local P = Position + (Normal/100)
	local D = getDir(P,workspace.Sun.Position)
	local Shadow = Raycast(P,D,Settings.MaxDistance,{workspace.Trace,workspace.Sun})
	
	if Shadow.Instance ~= workspace.Sun and not ShouldShadow then
		local h,s,v = Color:toHSV()

		local NormalShadow = .25

		-- Darken the pixel by NormalShadow
		return Color3.fromHSV(h,s,v-NormalShadow)

	elseif Shadow.Instance == workspace.Sun then
		-- specular highlighting
		local h,s,v = Color:toHSV()

		local NormalBright = .25

		local Reflection = Reflect(Direction,Normal)

		local DirectionToSun = getDir(Position,workspace.Sun.Position)
		
		-- Multiple .25 by the dot product of our reflection vector and the direction of the sun from our position
		NormalBright *= (Reflection:Dot(DirectionToSun))

		NormalBright = math.clamp(NormalBright,0,1)

		return Color3.fromHSV(h,s,v+NormalBright)
	end
	
	return Color
end

-- This function is a bit confusing im not sure how to describe it that well.
local function calculateReflection(Dir,Norm,Reflectivity,Position,Color,bounces)
	if Reflectivity == 0 then
		return Color
	end
	
	local Reflection = Reflect(Dir,Norm)
	
	local Result = Raycast(Position,Reflection,Settings.MaxDistance,{workspace.Trace})
	
	if Result then
		-- Lerp the color from our default color to the reflected color depending on how reflective the surface is
		Color = Color:lerp(Result.Instance.Color,Reflectivity)
		
		if Result.Instance.Reflectance > 0 and bounces < Settings.Samples then
			-- Does all the reflection bounces then calculates the shadow
			
			local temp = calculateReflection(getDir(Position,Result.Position),Result.Normal,Result.Instance.Reflectance,Result.Position,Color,bounces)
			Color = Color:lerp(temp,Reflectivity)
			
			Color = calcShadow(Color,Result.Position,Result.Normal,Reflection)
		else
			-- Just calculates the shadow
			
			Color = calcShadow(Color,Result.Position,Result.Normal,Reflection)
		end
		
		bounces += 1
	else
		-- If our reflection hit the sky then just lerp to the sky color
		
		Color = Color:lerp(Settings.SkyColor,Reflectivity)
	end
	
	return Color
end

local Raytracer = {}
Raytracer.__index = Raytracer

	function Raytracer.init(SizeX,SizeY,StartPos)
		-- tells the server to init the image on the python server
		RequestEvent:InvokeServer{
			request_type = 1,
			image_size_x = SizeX,
			image_size_y = SizeY
		}
		
		local self = setmetatable({},Raytracer)
		
		self.sizex = SizeX
		self.sizey = SizeY
		self.startpos = StartPos
		
		return self
	end
	
	-- This doesnt need much info on it pretty straight forward
	function Raytracer:Raytrace()	
		local pixelCache = {}
		
		local finished = 0
		
		for y = 1,self.sizey do
			pixelCache[y] = {}
			print(y)
			task.spawn(function()
				for x = 1,self.sizex do

					local pixelPosition = Vector2.new(x,y)
					pixelPosition += self.startpos

					local pixelColor = nil

					local ray = Camera:ScreenPointToRay(pixelPosition.X,pixelPosition.Y)

					local MaxDistance = Settings.MaxDistance

					local Result = Raycast(ray.Origin,ray.Direction,Settings.MaxDistance,{workspace.Trace})

					if not Result then
						pixelColor = Settings.SkyColor
						pixelCache[y][x] = colToTable(pixelColor)

						RunService.RenderStepped:Wait()

						continue
					else
						pixelColor = Result.Instance.Color
					end

					local HasReflected = false

					if Result.Instance.Reflectance > 0 then
						pixelColor = calculateReflection(ray.Direction,Result.Normal,Result.Instance.Reflectance,Result.Position,pixelColor,0)
						HasReflected = true

						pixelColor = calcShadow(pixelColor,Result.Position,Result.Normal,ray.Direction,true)
					else
						pixelColor = calcShadow(pixelColor,Result.Position,Result.Normal,ray.Direction)
					end
					
					-- This was a really bad attempt at anti aliasing (it doesnt work but does create some cool outlines)
					--if Result then
					--	local ray1 = Camera:ScreenPointToRay(pixelPosition.X+1,pixelPosition.Y)
					--	local ray2 = Camera:ScreenPointToRay(pixelPosition.X,pixelPosition.Y+1)
					--	local ray3 = Camera:ScreenPointToRay(pixelPosition.X-1,pixelPosition.Y)
					--	local ray4 = Camera:ScreenPointToRay(pixelPosition.X,pixelPosition.Y-1)

					--	local Result1 = Raycast(ray1.Origin,ray1.Direction,Settings.MaxDistance,{workspace.Trace})
					--	local Result2 = Raycast(ray2.Origin,ray2.Direction,Settings.MaxDistance,{workspace.Trace})
					--	local Result3 = Raycast(ray3.Origin,ray3.Direction,Settings.MaxDistance,{workspace.Trace})
					--	local Result4 = Raycast(ray4.Origin,ray4.Direction,Settings.MaxDistance,{workspace.Trace})

					--	local blendTable = {}

					--	if Result1 then
					--		if Result1.Instance ~= Result.Instance then
					--			table.insert(blendTable,Result1)
					--		end
					--	end
					--	if Result2 then
					--		if Result2.Instance ~= Result.Instance then
					--			table.insert(blendTable,Result2)
					--		end
					--	end
					--	if Result3 then
					--		if Result3.Instance ~= Result.Instance then
					--			table.insert(blendTable,Result3)
					--		end
					--	end
					--	if Result4 then
					--		if Result4.Instance ~= Result.Instance then
					--			table.insert(blendTable,Result4)
					--		end
					--	end

					--	local finalColor = pixelColor

					--	for _,v in blendTable do
					--		finalColor = Color3.new(
					--			(finalColor.R + v.Instance.Color.R)/2,
					--			(finalColor.G + v.Instance.Color.G)/2,
					--			(finalColor.B + v.Instance.Color.B)/2
					--		)
					--	end

					--	pixelColor = finalColor
					--end

					pixelCache[y][x] = colToTable(pixelColor)
				end
				
				finished += 1
			end)
			
			RunService.RenderStepped:Wait()
		end
		
		repeat
			warn("waiting for all pixels to finish...")
			warn(("%s/%s"):format(finished,self.sizey))
			task.wait(.25)
		until finished == self.sizey
		
		-- tell the server to save our pixels
		RequestEvent:InvokeServer{
			request_type = 2,
			pixel_data = pixelCache,
			imageSize = Vector2.new(0,self.sizey)
		}
		
		-- tell the server to save the image
		RequestEvent:InvokeServer{
			request_type = 3,
		}
	end

return Raytracer

Important Information

Remember when you use the raytracer you have the python server running, for details on the python server check Part 1.

Never move your camera while Raytracing or you are likely to get weird results.

Don't worry about it Raytracing your character as it only Raytraces what is in the "Trace" folder in workspace.

Thats about it. If i've missed anything out or there are any bugs be sure to tell me!

Oh and most importantly, have fun!

Psst.. Heres an image that I made with the raytracer:

image|100x100

View in-game to comment, award, and more!