..
#gamedev

How to Render 3D World Pixel by Pixel

This post I'll describe how to render a 3D world pixel by pixel. This steps is a little bit complicate to explain but I'll do my best.

The fist step is to render some weird gradient pixels on the screen.

Render 3D Perspective

To render a 3D world only using pixels (software render) we need to interate through y-axis, x-axis and compute each axis from -0.5 until 0.5.

With this approach, we can create a space world (-0.5 <=> 0.5) for entire 2D perspective.

Let's see the code:

for (int y = 0; y < height; y++) {
        // from -0.5 to 0.5
        double yd = (y - height / 2.0) / height;

        double zoom = height;
        double z = (zoom) / yd;
        if (yd < 0) {
                z = (zoom) / -yd;
        }

        for (int x = 0; x < width; x++) {
                double xx = x;
                double yy = z;

                int xPix = (int) xx;
                int yPix = (int) yy;

                pixels[y * width + x] = yPix << 8 | xPix;
        }
 }

The above code makes this changes:

  1. 0 - height: Change from -100 to 0 (y - height).
  2. After that, -50 to 50 (y - height / 2.0).
  3. And finally, normalize with division by height itself. -0.5 to 0.5 (y - height / 2.0) / height.

This code creates the Y perspective like image below:

1733957253.png

It works because when we divide zoom / yd, we are through middle negative zoom value to infinity to middle positive zoom value. Like, -100, infinity (zoom / 0.0000001), +100.

This calculation makes the center pixels so far.

We need to do the same for x-axis, but, after compute -0.5 to 0.5 we need to multiply by Z, to make perspective at the same situation (-width, infinity, width).

for (int y = 0; y < height; y++) {
        double yd = (y - height / 2.0) / height;

        double zoom = height;
        double zd = (zoom) / yd;
        if (yd < 0) {
                zd = (zoom) / -yd;
        }

        for (int x = 0; x < width; x++) {
                double xd = (x - width / 2.0) / height;
                xd *= zd;

                double xx = xd;
                double yy = zd;

                int xPix = (int) xx & 255;
                int yPix = (int) yy & 255;

                pixels[y * width + x] = yPix << 8 | xPix;
        }
 }

1733959110.png

Now, we can load the bitmap's pixel into our pixel buffer and adjusts the zoom. Let's see:

for (int y = 0; y < height; y++) {
        double yd = ((y) - height / 2.0) / height;

        double zoom = 4;
        double zd = (zoom + yCam) / yd;
        if (yd < 0) {
                zd = (zoom - yCam) / -yd;
        }

        for (int x = 0; x < width; x++) {
                double xd = (x - width / 2.0) / height;
                xd *= zd;

                double xx = xd + xCam;
                double yy = zd + zCam;

                int xPix = (int) xx;
                int yPix = (int) yy;

                int tileWidth = 8;
                pixels[y * width + x] = Assets.FLOORS.pixels[(yPix & 7) * tileWidth + (xPix & 7)];
        }
 }

This code results into this:

1733959483.png

But, in the x-axis has one more pixel that we need to avoid. So, change the code to this:

if (xx < 0) xPix -= 1;
if (yy < 0) yPix -= 1;

Rotate the World

To rotate the world we need a formula the uses cosine and sine.

First, let's get the cosine and sine value of specific position (angle). For this tests purpose, I'm going to use the current time to make some new value every frame.

double rotation = game.time * 0.3;
double rCos = Math.cos(rotation);
double rSin = Math.sin(rotation);

Now, inside the for loop X, let's compute the rotation X and Y by formula:

double rotationX = xd * rCos - zd * rSin;
double rotationY = zd * rCos + xd * rSin;

This part applies the rotation matrix to the original coordinates (xd, zd). The rotation matrix for 2D rotations is:

[ rCos -rSin ] [ rSin rCos ]

Multiplying the rotation matrix by the original coordinate vector [xd, zd] gives us the rotated coordinates [rotationX, rotationY]:

[rotationX] = [ rCos -rSin ] [xd]

[rotationY] = [ rSin rCos ] * [zd]

Now, add the new value into xx and yy.

double xx = rotationX;
double yy = rotationY;

1733960730.png

Post process Z-buffer

To make post process like shadows, we need store the current z-position into zBuffer.

zBuffer[y * width + x] = zd;

After that, we can process each pixel to a new value like more or less bright.

void postProcess() {
        for (int i = 0; i < width * height; i++) {
                int col = pixels[i];

                int brightness = (int) (20000 / (zBuffer[i] * zBuffer[i]));
                // normalize the bright
                if (brightness < 0)   brightness = 0;
                if (brightness > 255) brightness = 255;

                // convert pixel to RGB
                int r = (col >> 16) & 0xff;
                int g = (col >> 8) & 0xff;
                int b = (col) & 0xff;

                // process the channels
                r = r * brightness / 255;
                g = g * brightness / 255;
                b = b * brightness / 255;

                // convert RGB to pixel
                int pixel = (r << 16) | (g << 8) | b;
                pixels[i] = pixel;
        }
}

1733960979.png