Skip to content

Instantly share code, notes, and snippets.

@cjxgm
Last active August 29, 2015 14:24
Show Gist options
  • Save cjxgm/c6ccee4778d557084cb9 to your computer and use it in GitHub Desktop.
Save cjxgm/c6ccee4778d557084cb9 to your computer and use it in GitHub Desktop.
one file distance field raymarching (asciiart) renderer written in lua
---------------------------------------------------------------------------
-- vector
local make_vector = function()
local mt = {}
local vector
vector = function(x, y, z)
if type(x) == 'table' then
return setmetatable(x, mt)
end
x = x or 0
y = y or x
z = z or y
return vector{x, y, z}
end
local promote = function(x)
if x == nil then return nil end
local t = type(x)
if t == 'table' and getmetatable(x) == mt then return x end
if t == 'number' then return vector(x) end
error(("cannot promote %s %q to vector"):format(t, tostring(x)))
end
local map = function(f, a, b)
a = promote(a)
b = promote(b)
if b
then return vector{ f(a[1], b[1]), f(a[2], b[2]), f(a[3], b[3]) }
else return vector{ f(a[1]), f(a[2]), f(a[3]) }
end
end
mt.add = function(a, b) return map(function(x, y) return x+y end, a, b) end
mt.sub = function(a, b) return map(function(x, y) return x-y end, a, b) end
mt.mul = function(a, b) return map(function(x, y) return x*y end, a, b) end
mt.div = function(a, b) return map(function(x, y) return x/y end, a, b) end
mt.neg = function(a ) return map(function(x ) return -x end, a ) end
mt.dot = function(a, b) return a[1]*b[1] + a[2]*b[2] + a[3]*b[3] end
mt.sqr = function(a) return a .. a end
mt.len = function(a) return math.sqrt(a:sqr()) end
mt.norm = function(a) return a / #a end
mt.safe_norm = function(a, small)
small = small or 1e-6
local len = #a
if len < small then return vector() end
return a / len
end
mt.tostring = function(a)
return ("vector(%g, %g, %g)"):format(a[1], a[2], a[3])
end
mt.__index = mt
mt.__newindex = function(v, key, value)
local msg = "attempt to modify immutable vector(%g, %g, %g)[%q] = %q"
msg = msg:format(v[1], v[2], v[3], key, value)
error(msg)
end
mt.__add = mt.add
mt.__sub = mt.sub
mt.__mul = mt.mul
mt.__div = mt.div
mt.__unm = mt.unm
mt.__len = mt.len
mt.__concat = mt.dot
mt.__tostring = mt.tostring
return vector
end
local vector = make_vector()
---------------------------------------------------------------------------
-- distance estimator
local make_estimators = function()
local estimators = {}
estimators.sphere = function(p, radius)
return #p - radius
end
estimators.union = math.min
estimators.diff = function(a, b)
return math.max(a, -b)
end
estimators.move = function(p, x, y, z, est, ...)
return est(p - vector(x, y, z), ...)
end
estimators.plane = function(p, nx, ny, nz)
return p .. vector(nx or 0, ny or 0, nz or 0):norm()
end
return estimators
end
---------------------------------------------------------------------------
-- distance field renderer
local write = io.write
local make_distance_field_renderer = function(
output_method,
max_step,
render_radius)
local public = {}
local max_step = max_step or 1000
local render_radius = render_radius or 20
local lerp = function(x, xf, xt, df, dt)
return (x-xf) / (xt-xf) * (dt-df) + df
end
local clerp = function(x, xf, xt, df, dt)
if x < xf then return df end
if x > xt then return dt end
return lerp(x, xf, xt, df, dt)
end
local outputs = {}
outputs['256color'] = function(x)
local seq = "\x1b[48;5;%dm \x1b[0m"
x = math.floor(clerp(x, 0, 1, 232, 255))
return seq:format(x)
end
outputs['truecolor'] = function(x)
local seq = "\x1b[48;2;%d;%d;%dm \x1b[0m"
x = math.floor(clerp(x, 0, 1, 0, 255))
return seq:format(x, x, x)
end
outputs['ascii'] = function(x)
local grays = [==[ .'`^",:;Il!i><~+_-?][}{1)(|\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$]==]
x = math.floor(clerp(x, 0, 1, 1, grays:len()))
return grays:sub(x, x)
end
local output = outputs[output_method or ''] or outputs['ascii']
public.raymarch = function(estimator, origin, dir)
local dist = 0
for i=1,max_step do
local p = dir * dist + origin
local d = estimator(p)
if d < 1e-5 then
return {
hit = true,
step = i,
dist = dist,
ndist = lerp(dist, 0, render_radius, 0, 1),
nstep = lerp( i, 1, max_step, 0, 1),
}
end
dist = dist + d
if dist > render_radius then break end
end
return { hit = false }
end
local rad = math.rad
local cos = math.cos
local sin = math.sin
public.render = function(estimator, size, fov, eye)
for y=1,size.h do
for x=1,size.w do
local cx = rad( lerp(x, 1, size.w, -fov.x/2, fov.x/2))
local cy = rad(-lerp(y, 1, size.h, -fov.y/2, fov.y/2))
local origin = vector(eye.x or 0, eye.y or 0, eye.z or 0)
local dir = vector(cos(cy)*sin(cx), sin(cy), -cos(cy)*cos(cx))
local result = public.raymarch(estimator, origin, dir)
local x = 0
if result.hit then x = math.pow(1-result.nstep, 10) end
write(output(x))
end
write('\n')
end
end
return public
end
---------------------------------------------------------------------------
-- main
print "creating scene..."
local estimator = (function()
local estimators = make_estimators()
local union = estimators.union
local move = estimators.move
local sphere = estimators.sphere
local plane = estimators.plane
local diff = estimators.diff
return function(p)
return diff(union(
move(p, 2, 0, 0, sphere, 2),
move(p, 0, -1, 0, plane, 0, 1, 0)
), sphere(p, 1))
end
end)()
--[[ significantly faster
estimator = (function()
local s2p = vector(2, 0, 0)
return function(p)
local s1 = #p - 1
local s2 = #(p-s2p) - 2
local p = p[2] + 1
return math.max(math.min(s2, p), -s1)
end
end)()
--]]
print "rendering scene..."
local renderer = make_distance_field_renderer('256color', 100, 50)
local scale=1.4
renderer.render(estimator, {w=60*1.7*scale, h=30*scale}, {x=160, y=90}, {x=-0.5, y=0, z=2})
// c++ version for comparison
// CAUTION: THIS C++ CODE IS BADLY WRITTEN AND IS NOT INTENDED FOR LEARNING PURPOSE
// significantly faster, animation in realtime
#include <iostream>
#include <sstream>
#include <algorithm>
#include <utility> // for std::forward
#include <string>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <glm/geometric.hpp>
#include <unistd.h>
namespace ccray
{
using glm::ivec2;
using glm::vec2;
using glm::vec3;
using std::cerr;
using std::to_string;
namespace distance_estimators
{
inline auto sphere(vec3 const& p, float radius) { return length(p) - radius; }
inline auto plane(vec3 const& p, vec3 const& n) { return dot(p, normalize(n)); }
template <class ...TS>
inline constexpr auto combine(TS... xs) { return std::min({xs...}); }
inline constexpr auto diff(float a, float b) { return std::max(a, -b); }
template <class F, class ...ARGS>
inline constexpr auto move(vec3 const& p, vec3 const& offset,
F const& f, ARGS&&... args)
{
return f(p-offset, std::forward<ARGS>(args)...);
}
}
inline constexpr auto lerp(float x, float xf, float xt, float df, float dt)
{
return (x-xf) / (xt-xf) * (dt-df) + df;
}
inline constexpr auto clerp(float x, float xf, float xt, float df, float dt)
{
if (x < xf) return df;
if (x > xt) return dt;
return lerp(x, xf, xt, df, dt);
}
inline constexpr auto rad(float x) { return x*M_PI/180; }
namespace outputs
{
struct ascii
{
static auto convert(float x)
{
char gray[] = " .'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$)gray";
return int(clerp(x, 0, 1, 0, sizeof(gray)-2))[gray];
}
};
struct color256
{
static auto convert(float x)
{
return "\e[48;5;" + to_string(int(clerp(x, 0, 1, 232, 255))) + "m \e[0m";
}
};
struct truecolor
{
static auto convert(float x)
{
auto g = to_string(int(clerp(x, 0, 1, 0, 255)));
return "\e[48;2;" + g + ";" + g + ";" + g + "m \e[0m";
}
};
}
template <int MAX_STEP=1000, int RENDER_RADIUS=20, class OUTPUT=outputs::ascii>
struct renderer
{
float dist;
float step;
float ndist;
float nstep;
template <class ESTIMATOR>
bool raymarch(ESTIMATOR const& est, vec3 const& origin, vec3 const& dir)
{
dist = 0;
for (int i=0; i<MAX_STEP && dist<RENDER_RADIUS; i++) {
auto p = dir*dist + origin;
auto d = est(p);
if (d < 1e-5) {
step = i;
nstep = step / MAX_STEP;
ndist = dist / RENDER_RADIUS;
return true;
}
dist += d;
}
return false;
}
template <class ESTIMATOR>
void render(std::ostream& o, ESTIMATOR const& est, ivec2 const& size, vec2 const& fov, vec3 const& eye)
{
for (auto y=0; y<size.y; y++) {
for (auto x=0; x<size.x; x++) {
auto cx = rad( lerp(x, 0, size.x-1, -fov.x/2, fov.x/2));
auto cy = rad(-lerp(y, 0, size.y-1, -fov.y/2, fov.y/2));
auto dir = vec3{cos(cy)*sin(cx), sin(cy), -cos(cy)*cos(cx)};
auto hit = raymarch(est, eye, dir);
auto c = (hit ? glm::pow(1-nstep, 10.0f) : 0.0f);
o << OUTPUT::convert(c);
}
o << '\n';
}
}
};
}
auto scene(glm::vec3 const& p)
{
namespace de = ccray::distance_estimators;
return de::diff(de::combine(
de::move(p, {2, 0, 0}, de::sphere, 2),
de::move(p, {0, -1, 0}, de::plane, glm::vec3{0, 1, 0})
), de::sphere(p, 1));
}
int main()
{
using std::cerr;
ccray::renderer<100, 50, ccray::outputs::color256> r;
constexpr auto scale = 1.4f;
cerr << "\e[H\e[J\e[?25l";
for (int i=0; i<100; i++) {
cerr << "\e[H";
auto x = ccray::lerp(i, 0, 99, 1, -2);
auto y = ccray::lerp(i, 0, 99, 1, 0);
auto z = ccray::lerp(i, 0, 99, 2, 3);
std::ostringstream ss;
r.render(ss, scene, {60*1.7*scale, 30*scale}, {160, 90}, {x, y, z});
cerr << ss.str();
cerr << "# " << i << "\e[K\n";
usleep(1'000'000 / 30);
}
cerr << "\e[?25h\n";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment