Raytracing on Pokitto

End result:

Source code will be pasted here. Not my work, just copy pasted from an example and slightly modified to work on Pokitto

Pokitto binary
raytrace.bin (49.9 KB)

Source code on mbed community page
https://os.mbed.com/teams/Pokitto-Community-Team/code/Raytracing/

6 Likes

Sweet! It looks like the original source may have come from here. In the code posted here I see a series of include statements that seem to be mangled at the top of the program, so I went looking.

That scratchapixel site comes complete with chunky amounts of theory! I love it when folks put up information rich sites.

Just as a point of reference, how much of the Pokitto memory capacity does this end up consuming?

Thanks for digging that out and making a link.

181kB of Flash. As for RAM I haven’t checked. There’s some dynamic allocation so I would not hazard a guess.

Original code rendered the picture into a buffer, I cut that out due to RAM.

Wow, this worked a lot quicker than I thought it would. This is really cool :slight_smile:

I guess that’s the power of drawing something directly to a screen :stuck_out_tongue:

No, this is one of those things where the simulator performance can not be trusted. This is because any modern computer has a very powerful floating point math unit, whereas Pokitto does things 100% in software. The actual rendering time of that picture (until the very end) is actually 8 minutes.

Be mindful of this!

1 Like

I was talking about the video you made :stuck_out_tongue:

So, was that video time lapsed or something?

Still, this is really cool :slight_smile:

Only 8 minutes?
Most of my Blender renders in college took longer than that :P.
(High quality ones admitedly.)

Maybe we need a good written fixed point arithmetic library :slight_smile:

* looks shifty *

I want to say something, but spoilers…

2 Likes

The code is all written in C++ with use of classes, standard libraries and all the goodies a high level language offer.
Couple of question:

  • 181 Kb of flash is a huge amount of memory used, due to use off std?
  • Is the best approach use C++ for microcontroller? Or is better to keep using plain C?

These are open question I know, but it will be very instructive have the opinion of experienced people.
Can’t wait have some well structured game to study (@adekto platformer, @epicdude312 minicraft, @jonne pixonia)

I think this is actually one of the big advantages of mbed vs. arduino. On mbed/Pokitto you can take standard C++ code and compile it. Then you can optimize to make it smaller. On the Arduino however, you are dealing with the crap called “sketches”. Sorry, its just a thing that really peeves me about Arduino in general. The fact that you don’t need function declarations and can get started in 5 minutes on the Arduino is not worth the 3 months you spend later bugging other people on forums and trying to understand why your first attempts at C++ code doesn’t work. Its ease of use through obfuscation and its just plain wrong.

As for the memory usage, browsing through the memory .map file given by the compiler shows there are hundreds of std:: functions of questionable use (fore example std::moneypunct). C++ streams related stuff.

So code size probably could be cut down alot.

2 Likes

Personally, I love vectors for example. I see absolutely nothing wrong with C++. Mbed is C++ and if your code runs, why worry?

C++ all the way buddy!

2 Likes

I couldn’t agree more.

I get that they’re trying to be inclusive to people who don’t necesarily know how to program, but in the process they’ve ended up elbowing experienced programmers in the stomach.

Worse yet, everyone who is trying to use Arduino to learn C++ ends up learning bad habits and end up getting even more confused when their C++ code won’t compile because they haven’t predeclared a function. ‘Lies to children’ are only helpful if you’re around to tell them the truth when they need to know it.

This is also one of the reasons I tend to create a Game class and just make the setup and loop in the .ino forward to Game::setup and Game::loop. By keeping the .ino minimal I can pretend it’s not even there.

If it were easier to get the IDE to use regular error/warning levels instead of minimum warning levels I’d do that too. The number of ‘warnings’ that I’ve seen that should really be errors (because they indicate severe bugs) is astounding.

* end rant *


I concur.

Really it would be nice if C++ had a special “stdlib for embedded systems” that was a trimmed down version of the actual stdlib, but to have a standardised one would mean having another comittee of experts (especially ones from the world of embedded systems), more time and resources being consumed et cetera, so it’s understandable why there isn’t one.

2 Likes

71 kB version:

// [header]
// A very basic raytracer example.
// [/header]
// [compile]
// c++ -o raytracer -O3 -Wall raytracer.cpp
// [/compile]
// [ignore]
// Copyright (C) 2012  www.scratchapixel.com
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
// [/ignore]
#include "Pokitto.h"
#include <cstdlib>
#include <cstdio>
#include <cmath>
//#include <fstream>
#include <vector>
//#include <iostream>
//#include <cassert>

Pokitto::Core g;

#if defined __linux__ || defined __APPLE__
// "Compiled for Linux
#else
// Windows doesn't define these values by default, Linux does
#define M_PI 3.141592653589793
#define INFINITY 1e8
#endif

template<typename T>
class Vec3
{
public:
    T x, y, z;
    Vec3() : x(T(0)), y(T(0)), z(T(0)) {}
    Vec3(T xx) : x(xx), y(xx), z(xx) {}
    Vec3(T xx, T yy, T zz) : x(xx), y(yy), z(zz) {}
    Vec3& normalize()
    {
        T nor2 = length2();
        if (nor2 > 0) {
            T invNor = 1 / sqrt(nor2);
            x *= invNor, y *= invNor, z *= invNor;
        }
        return *this;
    }
    Vec3<T> operator * (const T &f) const { return Vec3<T>(x * f, y * f, z * f); }
    Vec3<T> operator * (const Vec3<T> &v) const { return Vec3<T>(x * v.x, y * v.y, z * v.z); }
    T dot(const Vec3<T> &v) const { return x * v.x + y * v.y + z * v.z; }
    Vec3<T> operator - (const Vec3<T> &v) const { return Vec3<T>(x - v.x, y - v.y, z - v.z); }
    Vec3<T> operator + (const Vec3<T> &v) const { return Vec3<T>(x + v.x, y + v.y, z + v.z); }
    Vec3<T>& operator += (const Vec3<T> &v) { x += v.x, y += v.y, z += v.z; return *this; }
    Vec3<T>& operator *= (const Vec3<T> &v) { x *= v.x, y *= v.y, z *= v.z; return *this; }
    Vec3<T> operator - () const { return Vec3<T>(-x, -y, -z); }
    T length2() const { return x * x + y * y + z * z; }
    T length() const { return sqrt(length2()); }

};

typedef Vec3<float> Vec3f;

class Sphere
{
public:
    Vec3f center;                           /// position of the sphere
    float radius, radius2;                  /// sphere radius and radius^2
    Vec3f surfaceColor, emissionColor;      /// surface color and emission (light)
    float transparency, reflection;         /// surface transparency and reflectivity
    Sphere(
        const Vec3f &c,
        const float &r,
        const Vec3f &sc,
        const float &refl = 0,
        const float &transp = 0,
        const Vec3f &ec = 0) :
        center(c), radius(r), radius2(r * r), surfaceColor(sc), emissionColor(ec),
        transparency(transp), reflection(refl)
    { /* empty */ }
    //[comment]
    // Compute a ray-sphere intersection using the geometric solution
    //[/comment]
    bool intersect(const Vec3f &rayorig, const Vec3f &raydir, float &t0, float &t1) const
    {
        Vec3f l = center - rayorig;
        float tca = l.dot(raydir);
        if (tca < 0) return false;
        float d2 = l.dot(l) - tca * tca;
        if (d2 > radius2) return false;
        float thc = sqrt(radius2 - d2);
        t0 = tca - thc;
        t1 = tca + thc;

        return true;
    }
};

//[comment]
// This variable controls the maximum recursion depth
//[/comment]
#define MAX_RAY_DEPTH 5

float mix(const float &a, const float &b, const float &mix)
{
    return b * mix + a * (1 - mix);
}

//[comment]
// This is the main trace function. It takes a ray as argument (defined by its origin
// and direction). We test if this ray intersects any of the geometry in the scene.
// If the ray intersects an object, we compute the intersection point, the normal
// at the intersection point, and shade this point using this information.
// Shading depends on the surface property (is it transparent, reflective, diffuse).
// The function returns a color for the ray. If the ray intersects an object that
// is the color of the object at the intersection point, otherwise it returns
// the background color.
//[/comment]
Vec3f trace(
    const Vec3f &rayorig,
    const Vec3f &raydir,
    const std::vector<Sphere> &spheres,
    const int &depth)
{
    //if (raydir.length() != 1) std::cerr << "Error " << raydir << std::endl;
    float tnear = INFINITY;
    const Sphere* sphere = NULL;
    // find intersection of this ray with the sphere in the scene
    for (unsigned i = 0; i < spheres.size(); ++i) {
        float t0 = INFINITY, t1 = INFINITY;
        if (spheres[i].intersect(rayorig, raydir, t0, t1)) {
            if (t0 < 0) t0 = t1;
            if (t0 < tnear) {
                tnear = t0;
                sphere = &spheres[i];
            }
        }
    }
    // if there's no intersection return black or background color
    if (!sphere) return Vec3f(2);
    Vec3f surfaceColor = 0; // color of the ray/surfaceof the object intersected by the ray
    Vec3f phit = rayorig + raydir * tnear; // point of intersection
    Vec3f nhit = phit - sphere->center; // normal at the intersection point
    nhit.normalize(); // normalize normal direction
    // If the normal and the view direction are not opposite to each other
    // reverse the normal direction. That also means we are inside the sphere so set
    // the inside bool to true. Finally reverse the sign of IdotN which we want
    // positive.
    float bias = 1e-4; // add some bias to the point from which we will be tracing
    bool inside = false;
    if (raydir.dot(nhit) > 0) nhit = -nhit, inside = true;
    if ((sphere->transparency > 0 || sphere->reflection > 0) && depth < MAX_RAY_DEPTH) {
        float facingratio = -raydir.dot(nhit);
        // change the mix value to tweak the effect
        float fresneleffect = mix(pow(1 - facingratio, 3), 1, 0.1);
        // compute reflection direction (not need to normalize because all vectors
        // are already normalized)
        Vec3f refldir = raydir - nhit * 2 * raydir.dot(nhit);
        refldir.normalize();
        Vec3f reflection = trace(phit + nhit * bias, refldir, spheres, depth + 1);
        Vec3f refraction = 0;
        // if the sphere is also transparent compute refraction ray (transmission)
        if (sphere->transparency) {
            float ior = 1.1, eta = (inside) ? ior : 1 / ior; // are we inside or outside the surface?
            float cosi = -nhit.dot(raydir);
            float k = 1 - eta * eta * (1 - cosi * cosi);
            Vec3f refrdir = raydir * eta + nhit * (eta *  cosi - sqrt(k));
            refrdir.normalize();
            refraction = trace(phit - nhit * bias, refrdir, spheres, depth + 1);
        }
        // the result is a mix of reflection and refraction (if the sphere is transparent)
        surfaceColor = (
            reflection * fresneleffect +
            refraction * (1 - fresneleffect) * sphere->transparency) * sphere->surfaceColor;
    }
    else {
        // it's a diffuse object, no need to raytrace any further
        for (unsigned i = 0; i < spheres.size(); ++i) {
            if (spheres[i].emissionColor.x > 0) {
                // this is a light
                Vec3f transmission = 1;
                Vec3f lightDirection = spheres[i].center - phit;
                lightDirection.normalize();
                for (unsigned j = 0; j < spheres.size(); ++j) {
                    if (i != j) {
                        float t0, t1;
                        if (spheres[j].intersect(phit + nhit * bias, lightDirection, t0, t1)) {
                            transmission = 0;
                            break;
                        }
                    }
                }
                surfaceColor += sphere->surfaceColor * transmission *
                std::max(float(0), nhit.dot(lightDirection)) * spheres[i].emissionColor;
            }
        }
    }

    return surfaceColor + sphere->emissionColor;
}

//[comment]
// Main rendering function. We compute a camera ray for each pixel of the image
// trace it and return a color. If the ray hits a sphere, we return the color of the
// sphere at the intersection point, else we return the background color.
//[/comment]
void render(const std::vector<Sphere> &spheres, int rscale)
{
    unsigned width = 220, height = 176;
    //Vec3f *image = new Vec3f[width * height], *pixel = image;
    Vec3f pixel; //jonne
    float invWidth = 1 / float(width), invHeight = 1 / float(height);
    float fov = 30, aspectratio = width / float(height);
    float angle = tan(M_PI * 0.5 * fov / 180.);
    // Trace rays
    for (unsigned y = 1; y < height+1; y+=rscale) {
        for (unsigned x = 1; x < width+1; x+=rscale) { //, ++pixel) {
            float xx = (2 * ((x + 0.5*rscale) * invWidth) - 1) * angle * aspectratio;
            float yy = (1 - 2 * ((y + 0.5*rscale) * invHeight)) * angle;
            Vec3f raydir(xx, yy, -1);
            raydir.normalize();
            //*pixel = trace(Vec3f(0), raydir, spheres, 0);
            pixel = trace(Vec3f(0), raydir, spheres, 0);
            //g.display.directPixel(x,y,g.display.RGBto565(pixel.x*255,pixel.y*255,pixel.z*255));
            g.display.directRectangle(x,y,x+rscale,y+rscale,g.display.RGBto565(pixel.x*255,pixel.y*255,pixel.z*255));
            //g.wait(100);
        }
    }
    // Save result to a PPM image (keep these flags if you compile under Windows)
    /*std::ofstream ofs("./untitled.ppm", std::ios::out | std::ios::binary);
    ofs << "P6\n" << width << " " << height << "\n255\n";
    for (unsigned i = 0; i < width * height; ++i) {
        ofs << (unsigned char)(std::min(float(1), image[i].x) * 255) <<
               (unsigned char)(std::min(float(1), image[i].y) * 255) <<
               (unsigned char)(std::min(float(1), image[i].z) * 255);
    }
    ofs.close();*/
    //delete [] image;
}

//[comment]
// In the main function, we will create the scene which is composed of 5 spheres
// and 1 light (which is also a sphere). Then, once the scene description is complete
// we render that scene, by calling the render() function.
//[/comment]
int main()
{
    g.begin();
    //srand48(13);
    std::vector<Sphere> spheres;
    // position, radius, surface color, reflectivity, transparency, emission color
    spheres.push_back(Sphere(Vec3f( 0.0, -10004, -20), 10000, Vec3f(0.20, 0.20, 0.20), 0, 0.0));
    spheres.push_back(Sphere(Vec3f( 0.0,      0, -20),     4, Vec3f(0.32, 1.00, 0.16), 1, 0.5)); //green middle sphere
    spheres.push_back(Sphere(Vec3f( 5.0,     -1, -15),     2, Vec3f(1.00, 0.16, 1.00), 1, 0.5)); //magenta
    spheres.push_back(Sphere(Vec3f( 5.0,      0, -25),     3, Vec3f(0.36, 0.16, 0.97), 1, 0.5)); //blue
    spheres.push_back(Sphere(Vec3f(-5.5,      0, -15),     3, Vec3f(1.00, 0.65, 0.30), 1, 0.5)); //orange
    // light
    spheres.push_back(Sphere(Vec3f( 0.0,     20, -30),     3, Vec3f(0.00, 0.00, 0.00), 0, 0.0, Vec3f(3)));
    render(spheres,32);
    render(spheres,16);
    render(spheres,8);
    render(spheres,4);
    render(spheres,2);
    render(spheres,1);
    while (g.isRunning()) {
        if(g.update(true)) {

        }
    }
    return 0;
}

1 Like

tl;dr:
Use C if you really, really want to, but here’s a compelling list of reasons why you should aim to use C++ instead or at the very least give C++ a chance.


First thing’s first, disclaimer:
I am horribly biased because I’ve never even bothered with C.

That said, C++11 all the way.

Despite popular misconception, C++ does not produce significantly larger code than C.

If you know what you’re doing, it produces code that is either just as small, or in some rare cases (thanks to modern optimisations being given language level support) smaller.

There are some features (like virtual functions and exceptions) that by their very nature end up being heavy and producing more code, but those are completely optional and don’t have an easy equivalent in C. (Which is to say it’s theoretically possible to emulate them, but it would be a lot of work.)

Code size aside, here are the reasons why I think people should stick to C++:

  • Much more type-safe than C (e.g. C++ doesn’t allow implict conversion of void* to other kinds of pointers)
  • No need to typedef structs
  • Classes help keep related code together
  • Classes tend to be easier to read and understand
  • constructors and destructors, permitting RAII (C relies on you remember to allocate/deallocate things)
  • operator overloading (e.g. myType1 == myType2 instead of areEqual(myType1, myType2))
  • references (e.g. void add5(int & i) { i += 5; } // Look, no pointers! (this is a contrived example for the sake of brevity))
  • namespaces
  • scoped enums (a.k.a enum class)
  • templates (e.g. generic collections like std::vector<T>)
  • virtual functions (if needed)
  • constexpr
  • move-constructors
  • copy elision

(I could keep going on, but I won’t - I think that’s long enough.)

It’s a big language with a lot of features to learn, but every single one has its use and almost all of them make the programmer’s life easier.

Frankly the only reason I can see to use C is “I don’t have the time to learn C++”, because C certainly isn’t more readable or more feature rich.

If the supposed complexity of C++ scares you, remember that you don’t have to learn it all at once - I’ve been using C++ for 3-4 years and I still find things I don’t know about it.
If you don’t understand a feature, don’t use it, and more importantly don’t let the advanced features put you off the whole language because there are plenty of other features that will make your life easier. It’s easy enough to just stick to classes and to never have to touch templates or overload an operator if you really don’t want to.

(Though I will say, as someone who has jumped down the rabbit hole, it’s a lot more fun when you can template your way out of having to write a ton of boilerplate.)

1 Like

Game, set and match almost on this one alone. Especially when mixing libraries.

IMHO the idea “C for microcontrollers” was relevant for the 8-bit AVRs, partly due to the limited memory & partly non-standard C++ implementation of the AVR version of GCC. The situation is a lot better with the ARM 32-bit Cortexes.

2 Likes

Truth be told, beyond the weird ino file behaviour and the “errors are warnings” stuff, I’ve found C++ still works perfectly fine on AVR chips, and using classes with member functions is no more expensive than the equivalent C code. I’ve certainly yet to see a case where equivalent C++ code is measurably worse than C.

But I’m also sure that ARM cores are better suited to the task.
Being 32-bit alone gives it a major advantage in terms of optimisations.

1 Like

Thank you very much both @jonne and @pharap
This is a really instructive talk for me.

I’ve just start digging in c/c++ but I’ve already had some background on object oriented stuff and programming in general.
Even seeing the clear advantages of c++ my main concern was about its application to microcontroller world.
Now I’ve a stronger confidence on use all the good stuff I wished to, but I was scared of.

I’ll finish actual project in C and start open to C++ advantages with next one.

1 Like