Writing a Pathtracer in Lua, Week 1

Hey guys! This week, it’s gonna be a little bit uncommon. I plan to start out a multi-week collection to implement a Pathtracer in Lua. Properly not every thing is to be applied in Lua, however a lot of the stuffs. Why Lua you ask? As a result of 1. Lua is a really versatile language and I can simply hot-reload my Pathtracing code every time I really feel like, and a pair of. the consequence might be proven instantaneously. No extra tedious ready for the monstrously sluggish C++ codebase to compile. I do know there’s Ninja and every thing however Visible Studio actually did a quantity on me. I hope you’ll be able to go on this lovely journey with me as nicely, random web stranger!
Mission Construction
The undertaking itself will likely be known as LuaPT
. The heavy-lifting and core code will likely be executed in C++, whereas the precise pathtracing code will likely be written in Lua. On this case, we are able to hot-reload the pathtracer in actual time when adjustments are made, making the entire course of extraordinarily fast.
However it’s type of exhausting to tell apart between heavy-lifting and precise pathtracing. Mannequin loading, picture I/O and the like will clearly be a C++ job, but it surely’s not like pathtracing is a trivial job. Subsequently, listed below are my plans to this point:
- Mannequin loading, picture I/O, or simply about each I/O associated factor will likely be written in C++.
- Thread employees and administration will likely be written in C++, however will probably be Lua which supplies them duties.
- The pathtracer will likely be visualized utilizing GLFW & ImGui (C++)
- C++ will maintain hot-reloading Lua.
- BVH building is someplace within the center and so each can maintain it (preferrably C++?).
- Ray sampling, Monte Carlo integration, and many others. is completed fully in Lua.
Flowchart
Right here’s an excellent easy flowchart demonstrating the workflow.
The blue components will likely be executed in C++, and the black half in Lua. As we are able to see, Lua is politely asking C++ to do issues more often than not. Nonetheless, the precise pathtracing half is in Lua. Which means we are able to both move the entire scene into Lua, and implement the intersect
perform, or having Lua invoke the intersect
perform applied in C++. You recognize, it appears to be like virtually like an engine. I assume I’ll know extra because the design progresses.
Picture I/O
And now it’s time to get to work! We’ll be implementing the Picture
class first; in any case, what’s a pathtracer’s price if it can not output pictures?
utilizing CComp = unsigned char; // CComp for coloration part
struct RGB
{
CComp r;
CComp g;
CComp b;
};
class Picture
{
public:
/**
* Default constructor
*/
Picture();
unsigned int at(int x, int y) const;
void set_rgb(int x, int y, CComp r, CComp g, CComp b);
void set_rgb(int x, int y, const RGB &rgb);
RGB get_rgb(int x, int y) const;
bool save(const std::string &dest) const;
bool load(const std::string &path);
/**
* Copy constructor
*
* @param different The opposite picture, to be copied from.
*/
Picture(const Picture& different);
/**
* Destructor
*/
~Picture();
int id() const;
int w, h;
personal:
std::unique_ptr<CComp[]> picture;
int id_;
bool initialized;
};
The Picture
class holds a unique_ptr
which will likely be freed upon going out of scope. On this approach, we are able to handle the reminiscence utilization effectively. The implementation entails utilizing stb_image.h
and stb_image_write.h
of the stb library. We’ll solely showcase some key capabilities under as a result of dumping the entire supply file in a weblog publish is an enormous waste of area.
The picture is initialized by setting the values of all of its channels to 0 (together with alpha). Proper now we solely help RGBA picture – though we gained’t be utilizing the alpha channel anytime quickly.
Picture::Picture(int w, int h, int ch) : w(w), h(h), picture(nullptr), id_(::id++), initialized(false)
{
assert(ch == num_ch && "Unsupported: channels != 4");
picture.reset(new CComp[w * h * num_ch]);
initialized = true;
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
picture[at(x, y) + 0] = 0;
picture[at(x, y) + 1] = 0;
picture[at(x, y) + 2] = 0;
picture[at(x, y) + 3] = 0;
}
}
}
The at
perform asserts that the sampling place is right and calculates the correct offset. Solely initialized
pictures can be utilized.
unsigned int Picture::at(int x, int y) const
{
assert(initialized && "Picture is just not initialized");
if (x < 0) { x = 0; }
if (y < 0) { y = 0; }
if (x >= w) { x = w; }
if (y >= h) { y = h; }
return (y * w + x) * num_ch;
}
The Lua Surroundings
Now to arrange the Lua surroundings. Aside from the traditional lua_State
pointer, we are going to maintain all Lua-related variables within the Lua
class as nicely.
/**
* The Lua interface. Liable for loading in scripts and executing them.
* The entire thing is designed to be a large state machine, and subsequently non-copyable.
*/
class Lua
{
public:
/**
* No copy constructor
*/
Lua(const Lua &one other) = delete;
/**
* Destructor
*/
~Lua();
int execute(const std::string &buffer);
int execute_file(const std::string &file);
void report_error(const std::string &msg);
void register_funcs();
std::vector<std::string> err_log;
static std::shared_ptr<Lua> inst();
personal:
/**
* Default constructor
* Thou shalt not name me straight, for I'm a singleton
*/
Lua();
bool lua_ready;
lua_State *l;
std::vector<std::shared_ptr<Picture> > pictures;
};
The Lua
class is a singleton and may solely be accessed utilizing inst()
, through which will probably be lazy-initialized (Lua()
is personal). It’s able to executing each traces of code and recordsdata. An error log is stored in order that errors are straightforward to hint. Proper now, when LuaPT
launches, a CLI reveals and accepts file names as enter. Lua::inst()->execute_file()
is then executed with errors logged into err_log
. Then the CLI asks for one more file title and executes that. Then it loops till EOF is reached.
Within the Lua
constructor, it is going to invoke register_funcs
through which C++-Lua interoperability capabilities are registered, for instance, creating/saving a picture:
void Lua::register_funcs()
{
lua_register(l, "make_image", make_image);
lua_register(l, "set_pixel", set_pixel);
lua_register(l, "save_image", save_image);
lua_register(l, "free_image", free_image);
}
Lua Wrappers & GC
As Lua can’t simply invoke constructors in C++, we have to implement further capabilities in order that pictures might be managed in Lua. We obtain this by having an Picture
vector within the Lua
class, in order that make_image
can simply be inserting a brand new picture into stated vector and returning the index. We are able to then manipulate the picture in Lua utilizing the deal with we bought. When deleting the picture, we name free_image
in Lua in order that it’s faraway from the vector. The good pointer will now have 0 reference and free itself.
However why not embrace Lua’s intrinsic GC as nicely? Lua can rubbish gather issues when they’re left alone after some time. That is fairly helpful when, for instance, the codebase in Lua turns into sophisticated down the road and useful resource administration turns into troublesome. To make issues simpler for us, we are able to wrap the capabilities up in order that they develop into pseudo courses (once more), however this time, it’s in Lua.
Picture = {
deal with = 0,
w = 0,
h = 0,
__gc = perform()
if w ~= 0 and h ~= 0 then
free_image(self.deal with)
finish
finish
}
perform Picture:new(w, h)
native ret = {}
setmetatable(ret, self)
self.__index = self
ret.deal with = make_image(w, h)
ret.w = w
ret.h = h
return ret
finish
perform Picture:save(title)
save_image(self.deal with, title)
finish
perform Picture:pixel(x, y, r, g, b)
set_pixel(self.deal with, x, y, clamp(r, 0, 1), clamp(g, 0, 1), clamp(b, 0, 1))
finish
perform Picture:pixel_vec4(x, y, vec4)
set_pixel(self.deal with, x, y, clamp(vec4.x, 0, 1), clamp(vec4.y, 0, 1), clamp(vec4.z, 0, 1))
finish
So to recap, we applied the picture I/O in C++, as courses, then wrapped them up as capabilities. Then in Lua, we wrapped the capabilities up (once more) they usually develop into courses.
Present Consequence
That’s all we’ve applied to this point. A approach for Lua to save lots of/write the picture. Concurrency, mannequin loading, and many others. remains to be additional down the road.
However we are able to nonetheless have some spectacular consequence. For instance, feast your eye on THIS! After implementing a small math library in Lua later, we have now raymarched a ball utilizing the very factor we simply made.
require "lib/picture"
require "math"
native pprint = require "lib/pprint"
native im = Picture:new(100, 100)
perform shade(u, v, x, y)
ro = Vec4:new(0, 0, -2, 1)
middle = Vec4:new(0, 0, 0, 1)
entrance = middle:subtr(ro):nor3() -- more often than not we cannot be needing the 4th part
proper = entrance:cross(Vec4:new(0, 1, 0, 1)):nor3()
up = entrance:cross(proper):nor3()
rd = proper:scl(u):add(up:scl(v)):add(entrance:scl(1)):nor3()
t = 0.01
for i = 1, 200 do
p = ro:add(rd:scl(t))
-- attempt to pattern a sphere situated at middle with r=0.5
r = 1.0
d = p:len3() - r
if d < 0.01 then
-- a sphere on the middle has a particular case regular the place the place being precisely the alternative of the conventional path
nor = p:nor3():scl(-1)
nor.y = -nor.y
brightness = math.max(0.0, nor:dot3(Vec4:new(0, 1, 1, 1):nor3()))
return Vec4:new(1.0, 0.5, 0.0, 1.0):scl(brightness)
finish
t = t + d
if t > 20 then
break
finish
finish
-- coloration background
uu = math.flooring(u * 5)
vv = math.flooring(v * 5)
if (uu + vv) % 2 == 0 then
return Vec4:new(0.1, 0.1, 0.1, 1)
else
return Vec4:new(0.9, 0.9, 0.9, 1)
finish
finish
for y = 1, 100 do
for x = 1, 100 do
u = (x - 1) / 100.0 * 2.0 - 1.0
v = (y - 1) / 100.0 * 2.0 - 1.0
res = shade(u, v, x - 1, y - 1)
im:pixel_vec4(x - 1, y - 1, res)
finish
finish
im:save("rm.png")
I’ll clean the undertaking across the edges and implement it little by little within the coming weeks. By posting it onto my weblog, it now has robotically develop into an enormous sunken price and now I can’t abandon it anymore (?). So keep tuned for extra!
Copyleft 2023 42yeah.