How to Render 3D World Pixel by Pixel
Table of Contents
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:
- 0 - height: Change from -100 to 0
(y - height)
. - After that, -50 to 50
(y - height / 2.0)
. - 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:
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; } }
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:
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;
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; } }
References
2D rotation: https://www.youtube.com/watch?v=a59YQ4qe7mE