Last active
April 9, 2019 03:23
-
-
Save Polkm/beb43111f96dd8d96387ee12500cec05 to your computer and use it in GitHub Desktop.
A minimal implementation of a distance field ray marching in pico 8. Has color dithering and soft shadows.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--math | |
local function length(x, y, z) return sqrt(x*x + y*y + z*z) end | |
local function norm(x, y, z) local l = length(x,y,z) return x/l, y/l, z/l end | |
local function dot(xa, ya, za, xb, yb, zb) return xa*xb + ya*yb + za*zb end | |
--globals | |
local ex, ey, ez = 0, 1, -1.5 --camera position | |
local fov = 90 --camera FOV | |
local tmin, tmax = .1, 100 --minimum and maximum distance from camera | |
local maxSteps = 45 --maximum number of steps to take | |
local lx, ly, lz = norm(.2, .5, -.6) --light direction | |
--distance field functions | |
local function plane(x, y, z) return y end | |
local function sphere(x, y, z, radius) | |
local manhattanDistance = x + y + z | |
--this if is to avoid errors due to pico's limited number range | |
if manhattanDistance > radius * 2 then return manhattanDistance end | |
return length(x, y, z) - radius | |
end | |
--union combines two distance functions | |
local function union(a, am, b, bm) if a < b then return a, am else return b, bm end end | |
--scene defines the total distance function | |
local function scene(x, y, z) | |
local d, m = tmax, 0 --max distance is sky | |
d, m = union(d, m, plane(x, y, z), "green") | |
d, m = union(d, m, sphere(x, y-1, z, .5), "red") | |
return d,m | |
end | |
--calculates the normal at xyz | |
local function sceneNormal(x, y, z) | |
local eps = 0.1 | |
local xa, xb = scene(x+eps, y, z), scene(x-eps ,y ,z) | |
local ya, yb = scene(x, y+eps, z), scene(x, y-eps ,z) | |
local za ,zb = scene(x, y, z+eps), scene(x, y, z-eps) | |
return norm(xa-xb, ya-yb, za-zb) | |
end | |
--rendering | |
local colorGradients = { | |
red = {1, 2, 8, 9, 10, 7}, | |
green = {0, 1, 5, 3, 11}, | |
sky = {15, 12, 12, 1}, | |
} | |
--returns smooth color based on position and gradient | |
local function dither(x, y, gradient, value) | |
local whole = flr(value * #gradient) | |
local fraction = value * #gradient - whole | |
local low = gradient[min(whole + 1, #gradient)] | |
local high = gradient[min(min(whole + 1, #gradient) + 1, #gradient)] | |
if fraction < 1/7 then return low end | |
if fraction < 2/7 then if (x+1)%3==0 and y%3==0 then return high else return low end end | |
if fraction < 3/7 then if x%2==0 and y%2==0 then return high else return low end end | |
if fraction < 4/7 then if (x%2==0 and y%2==0) or (x%2~=0 and y%2~=0) then return high else return low end end | |
if fraction < 5/7 then if x%2==0 and (y+1)%2==0 then return low else return high end end | |
if fraction < 6/7 then if (x+1)%3==0 and y%3==0 then return low else return high end end | |
return high | |
end | |
local function getShadowPoint(t, x, y, z) return x + lx * t, y + ly * t, z + lz * t end | |
--computes the shoft shadow value | |
local function shadow(x,y,z) | |
--t starts at 0.2 so the shadow ray doesn't intersect | |
--the object it's trying to shadow | |
local res, t, distance, sx, sy, sz = 1, 0.2, 0, 0, 0, 0 | |
for i = 1, 6 do | |
sx, sy, sz = getShadowPoint(t, x, y, z) | |
distance, _ = scene(sx, sy, sz) --we don't care about the color | |
res = min(res, 2 * distance / t) --increase 2 to get sharper shadows | |
t += min(max(distance, .02), .2) | |
if distance < .05 or t > 10.0 then break end | |
end | |
return min(max(res, 0), 1) | |
end | |
--calculates the final lighting and color | |
local function render(x, y, t, tx, ty, tz, rx, ry, rz, color) | |
local nx, ny, nz = sceneNormal(tx, ty, tz) | |
local light = 0 | |
light += min(max(dot(nx, ny, nz, lx, ly, lz), 0), 1) --sun light | |
light *= shadow(tx,ty,tz) --shadow color | |
light = min(max(light, 0), 1) --clamp final light value | |
return dither(x, y, colorGradients[color], light) | |
end | |
--calculates the sky color | |
local function sky(x, y, rx, ry, rz) | |
local altitude = (min(max(ry, 0), 1) ^ 1.5) | |
return dither(x, y, colorGradients.sky, altitude) | |
end | |
--tracing | |
--this is the heart of a ray tracer | |
--the for loop pushes the test point forward until | |
--it finds a surface that is close enough to render | |
--the forward direction is based off the xy of the screen and fov | |
local function getRayDirection(x, y) return norm(x / 64 - 1, (128 - y) / 64 - 1, 90 / fov) end | |
local function getTestPoint(t, rx, ry, rz) return ex + rx * t, ey + ry * t, ez + rz * t end | |
function trace(x,y) | |
local rx, ry, rz = getRayDirection(x, y) | |
local tx, ty, tz = 0, 0, 0 | |
local t, distance, color = 0, 0, 0 | |
for i = 1, maxSteps do | |
tx, ty, tz = getTestPoint(t, rx, ry, rz) | |
distance, color = scene(tx, ty, tz) | |
--the test point is close enough, render | |
if distance < .05 then return render(x, y, t, tx, ty, tz, rx, ry, rz, color) end | |
--the test point is too far, give up, draw the sky | |
if distance >= tmax then break end | |
--move forward by some fraction | |
t += distance * .7 | |
end | |
return sky(x, y, rx, ry, rz) | |
end | |
--just here to get pico to work the way it should be defaut tbh | |
function _init() cls() end | |
function _update() end | |
--pick random points to trace, but only if they | |
--havent been traced before | |
--cache the expensive trace function, and set pixel | |
local traced = {} | |
function _draw() | |
for s = 1, 400 do | |
local x, y = flr(rnd(128)), flr(rnd(128)) | |
local i = x + y * 128 | |
if not traced[i] then | |
pset(x, y, trace(x,y)) | |
traced[i] = true | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment