May 10, 2017

Basic Raycasting Tutorial

  1. Goal
  2. Pre-requisites
  3. General Idea
  4. The Code
    1. Basic Setup
    2. Ray Casting Logic
    3. Getting the Image
  5. Result
  6. Epilogue

Goal

This tutorial's goal is to get you acquainted with the idea of ray casting. We will focus on the most basic method for ray casting: using a combination of unit vectors, vector addition, and lots and lots of iteration. Sounds fun? Let's dive in.

If you wanna see the finished product, scroll all the way to the bottom of the screen, or press the End key on your keyboard.

Pre-requisites

  • Some proficiency in C++
  • Some knowledge of the event-driven paradigm (if you know what the term is, you are golden)
  • Some knowledge of what a vector is, in mathematical terms (not just #include <vector>)

General Idea

In this tutorial, you will build a small program that projects a beam of light from the center of the canvas in the direction of your cursor. Any objects placed in the way of the beam will be blocked, just as light (in normal circumstances) is blocked when you place something in front of the source.

The objects will be placed by a simple click on the canvas.

The idea behind the ray casting is that we will first grab the vector going from the middle of the canvas towards the cursor, and change it into a unit vector. Then, we iteratively add the unit vectors onto the position vector of the middle of the canvas, until we come across an object. If we hit an object, we draw a line from the middle to the point it hit the object. If we don't, we just draw a line from the middle to the edge of the screen.

The Code

Let's start off with the basics.

Basic Setup

#include <SFML/Graphics.hpp>

using namespace sf;

int main() {
    RenderWindow w(VideoMode(500, 500), "Raycast Thingy", Style::Close);
    Event evt;

    while (w.isOpen()) {
        while (w.pollEvent(evt)) {
            switch (evt.type) {
            case Event::Closed:
                w.close();
                break;
            default:
                break;
            }
        }

        w.clear();

        w.display();
    }
}

This is just, you know, your standard main loop. We will be working with SFML.

If you are not sure how to compile this, please check out these tutorials.

Let's add a feature where circles will appear on click, so that we could add objects dynamically. Right after Event evt, initialize the following:

CircleShape c;
bool hasDrawn = false;
std::vector<decltype(Mouse::getPosition())> cpos;

We will be using the same CircleShape for drawing onto the window. The std::vector is for saving all the cursor positions, again, for drawing onto the window. Don't forget to include it!

Add another case in the switch.

case Event::MouseButtonReleased:
    cpos.push_back(Mouse::getPosition() - w.getPosition());
    break;

The reason why we have to subtract the window position from the mouse position is that the mouse position is relative to the monitor/screen, and not the canvas.

Now, in-between the w.clear() and w.display(), we are gonna start drawing the circles.

for (auto p : cpos) {
    c.setPosition(p.x, p.y);
    w.draw(c);
}

Remember to initialize the CircleShape, lest you won't be able to actually see it, even if you click a whole bunch.

c.setFillColor(Color::White);
c.setRadius(10);

For more documentation on what aspects of the circle you can change, consult the documentation.

At this stage, you should be able to click on the canvas, and a circle should appear on the screen.

Ray Casting Logic

Okay. You are done with all the basics. Now, how do we implement it? Let's just have an sf::VertexArray that handles the 2 points. Our first vertex is start, the center of the canvas. The second point will involve ray casting. Since we need to know where to start, and what direction we should go, it makes sense that the start and mouse position should be parameters. But what's img?

img is the entire canvas, as a buffered array of pixels. Since we will need to add unit vectors to our current starting position, we will need to know when we hit the circles. The most pixel-perfect way to do it would be to use an array look-up.

// Draw line with raycasting
VertexArray vs(Lines, 2);
vs[0].position = start;
vs[1].position = raycast(start,
                    (Vector2f) (Mouse::getPosition() - w.getPosition()),
                    img);
vs[0].color = Color::Red;
vs[1].color = Color::Red;
w.draw(vs);

Let's actually create the raycast function.

Vector2f raycast(Vector2f start, Vector2f mouse, Image img) {
}

First, we will get the vector that goes from the start to the mouse, and normalize it.

Vector2f u = mouse - start;
u /= v2fNorm(u);

The function float v2fNorm(Vector2f) grabs the norm of the vector, and will be left as an exercise to the reader (this is sounding like one of them textbooks already).

We will use sf::Rect<float> to check if the point (and the points after it) is within the bounds of the canvas.

Rect<float> r(Vector2f(0, 0), (Vector2f) img.getSize());
while (r.contains(start.x, start.y)) {
    if (img.getPixel(start.x, start.y) != Color::Black &&
        img.getPixel(start.x, start.y) != Color::Red) {
        // We've hit it!
        return start;
    }

    start += u;
}

The code above checks to see if the pixel is neither black nor red. In this example, the canvas has a black background, and the line is drawn in red, hence anything that isn't either of these colours should be an obstacle. We are using the vector start as a counter: every time we go through the loop, we add the unit vector to it again to update the counter.

The img.getPixel() grabs the colour of the pixel at the specified x and y coordinates.

We are almost done. What we need now, is to return the side if it doesn't find anything. This is easy, since the while-loop terminates if start goes out of bounds; start keeps it's value, and we can return that.

Getting the Image

Obtaining the entire image buffer of the canvas can be extremely time-consuming; it is proportionate to the size of the canvas. Let's add a few more variables to the start of int main().

Texture wt;
Image img;

And we initialize them.

wt.create(w.getSize().x, w.getSize().y);
wt.update(w);
img = wt.copyToImage();

This is just how they do things in SFML. First, you create an sf::Texture to hold the graphics displayed on the canvas. Then, you convert the texture to an sf::Image. From the docs, this is an extremely time-consuming operation, which is why the texture is updated if and only if a new obstacle is placed on the canvas.

Now, we just need code to update the texture and the image whenever an obstacle is placed onto the canvas. This task will be left as another exercise to the reader.

Result

And here are the results!

raycast

raycast

Epilogue

If you are still a bit stuck, here is my code. It's just a single source file, coupled with a Makefile for easy compilation.

Anyways, that's it! Think about all the other things you can do with it! If you can draw one line, why not draw tons of lines, and have the source start start moving?

Tags: tutorial graphics c++ ray casting