Last active
August 29, 2015 14:24
-
-
Save cjxgm/c6ccee4778d557084cb9 to your computer and use it in GitHub Desktop.
one file distance field raymarching (asciiart) renderer written in lua
This file contains hidden or 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
--------------------------------------------------------------------------- | |
-- 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}) |
This file contains hidden or 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
// 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