Skip to content

Instantly share code, notes, and snippets.

@ZakBlystone
Last active March 23, 2025 03:37
Show Gist options
  • Save ZakBlystone/9a139c51015a9870c02eddc958926021 to your computer and use it in GitHub Desktop.
Save ZakBlystone/9a139c51015a9870c02eddc958926021 to your computer and use it in GitHub Desktop.
-- FastToScreen (CC0 Public Domain, free to use in whatever you want)
-- Created by: Zachary Blystone ( [email protected] )
-- Localized functions
local __vunpack = FindMetaTable("Vector").Unpack
local __cos = math.cos
local __sin = math.sin
local __min = math.min
local __tan = math.tan
local __abs = math.abs
local __atan2 = math.atan2
local mt = {}
mt.__index = mt
-- FastToScreen:SetBorder
-- Controls how projected points are clamped to viewport edges.
-- Takes one of the following (numbers specified in pixels):
-- 1. 'true': sets a 0px border on the projection
-- 2. 'false': clears any previously set border
-- 3. A float indicating how far inset the border should be
-- 4. A table containing the left,right,top,bottom insets for the border
-- {<left>, <right>, <top>, <bottom>} (e.g. {16,16,16,32})
function mt:SetBorder(border)
if border == false then border = nil end
if border == true then border = 0 end
if type(border) == "number" then border = {border,border,border,border} end
self.t_border = border
return self
end
-- FastToScreen:Setup
-- Initializes the internal state needed to project points to the screen.
-- All arguments are optional:
-- origin: A Vector that defines camera position in world-space
-- angles: An Angle that defines the camera rotation in world-space
-- fov: The camera's field-of-view in degrees
-- width: The width of the screen or rendertarget
-- height: The height of the screen or rendertarget
function mt:Setup(origin, angles, fov, width, height)
-- Defaults if arguments are not provided
origin = origin or EyePos()
angles = angles or EyeAngles()
fov = fov or LocalPlayer():GetFOV()
width = width or ScrW()
height = height or ScrH()
-- Unpack basis and translation vectors
local ox,oy,oz = __vunpack(origin)
local fx,fy,fz = __vunpack(angles:Forward())
local rx,ry,rz = __vunpack(angles:Right())
local ux,uy,uz = __vunpack(angles:Up())
-- Calculate 3x4 world-to-camera matrix
local m = self.m_wtoc
m[ 1], m[ 2], m[ 3], m[ 4] = -rx, -ry, -rz, (rx * ox + ry * oy + rz * oz)
m[ 5], m[ 6], m[ 7], m[ 8] = ux, uy, uz, -(ux * ox + uy * oy + uz * oz)
m[ 9], m[10], m[11], m[12] = fx, fy, fz, -(fx * ox + fy * oy + fz * oz)
-- Calculate width, height, and aspect ratio for Source
-- (Based on 4:3 'standard' ratio)
local aspect = width / height
self.f_wscale = 1 / ( __tan( fov * math.pi / 360 ) * aspect * 0.75 )
self.f_hscale = aspect * self.f_wscale
self.f_wsize = width
self.f_hsize = height
return self
end
-- FastToScreen:ToScreenRaw
-- Projectes a given point in world-space to screen-space
-- (Point is not visible if z < 0)
-- Takes: raw x,y,z coordinates as numbers representing a point in world-space
-- Returns:
-- Projected x,y,z coordinates as numbers
-- Whether or not that point was clamped to an edge
-- If clamped; the angle of the point from the center of the screen
function mt:ToScreenRaw(x,y,z)
local m = self.m_wtoc
local b = self.t_border
local wsize, hsize = self.f_wsize, self.f_hsize
local on_edge, angle = false, 0
-- Transform point into camera-space
x, y, z =
m[ 1] * x + m[ 2] * y + m[ 3] * z + m[ 4],
m[ 5] * x + m[ 6] * y + m[ 7] * z + m[ 8],
m[ 9] * x + m[10] * y + m[11] * z + m[12]
-- Transform point into NDC-space
local w = 1 / -__abs(z)
x = self.f_wscale * x * w
y = self.f_hscale * y * w
-- Clamp point to border if one is set up
if b then
-- Calculate border edges in NDC-space
local cl,cr,ct,cb =
-1 + 2 * ( b[1] / wsize ),
-1 + 2 * ( (wsize - b[2]) / wsize ),
-1 + 2 * ( b[3] / hsize ),
-1 + 2 * ( (hsize - b[4]) / hsize )
-- If point lies outside border or is behind the camera, clamp it
if z < 0 or x - cl < 0 or x - cr > 0 or y - ct < 0 or y - cb > 0 then
-- Compute angle of point from center of view
on_edge = true
angle = __atan2(y,x)
local cos, sin = __cos(angle), __sin(angle)
local d = 2
-- Project point onto edges, 'd' will be nearest edge distance
if sin > 0 then d = __min(d, (y - cb) / -sin) end
if sin < 0 then d = __min(d, (y - ct) / -sin) end
if cos > 0 then d = __min(d, (x - cr) / -cos) end
if cos < 0 then d = __min(d, (x - cl) / -cos) end
-- Move point along angle towards projected distance
x = x + cos * d
y = y + sin * d
end
end
-- Transform point into screen-space
x = (1 + x) * wsize * 0.5
y = (1 + y) * hsize * 0.5
return x, y, z, on_edge, angle
end
-- FastToScreen:ToScreen
-- Projectes a given point in world-space to screen-space
-- (Point is not visible if z < 0)
-- Takes: A Vector representing a point in world-space
-- Returns:
-- Projected x,y,z coordinates as numbers
-- Whether or not that point was clamped to an edge
-- If clamped; the angle of the point from the center of the screen
function mt:ToScreen(v)
return self:ToScreenRaw( __vunpack(v) )
end
-- FastToScreen
-- Returns a new 'FastToScreen' object.
-- Must be initialized with :Setup before using.
function FastToScreen()
return setmetatable({
m_wtoc = {
1,0,0,0,
0,1,0,0,
0,0,1,0},
f_wscale = 1,
f_hscale = 1,
f_aspect = 1,
f_width = 0,
f_height = 0,
t_border = nil,
}, mt)
end
-- Example: Project origin to screen, clamped to a 16-px wide border
--[[
local ts = FastToScreen():SetBorder(16)
hook.Add("HUDPaint", "marker_test", function()
ts:Setup()
local x,y,z = ts:ToScreenRaw( 0,0,0 )
surface.SetDrawColor(255,255,255)
surface.DrawRect(x-5,y-5,10,10)
end)
]]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment