Recreating the 2.5D effect from Pokémon Black & White
A deep-dive into the distinctive style of one of gaming's best RPGs.
Pokémon Black & White burst on to the Nintendo DS in 2010 with some of the most impressive and refined graphics seen on the console to date. Constrained by the Nintendo DS’s limited graphical power, low resolution, and relatively inexpensive hardware, the console created a generation of games with a very distinctive low-poly “3D pixel-art” style.
Hi, I’m Matthew. I’m working on a Windpunk RPG inspired by the glory days of the Nintendo DS. You’ll get to see more of that soon, but for now I wanted to document my research and understanding of the Pokemon Black and White game engine and its very distinctive (and dare I say, charming) aesthetic.
Before we try build it ourselves, let’s start with a quick history lesson.
Evolution of Pokémon Graphics
A Generational Leap
Pokemon’s fourth generation of titles - beginning with Pokémon Diamond & Pearl - debuted the now iconic 2.5D aesthetic the DS would become known for.

When the games were first shown, there was some criticism about the continued use of 2D sprites. In an interview with Spong, the Pokémon Company’s Tsunekazu Ishihara said (emphasis mine):
…if you look at the map [indicates DS screen] you will notice that they are all designed in 3D. […] It is, physically, in three dimensions but it is designed in such a way that it doesn’t look like it is in three dimensions. Intentionally. So that we maintain the original feel of the game.
You can see the use of 3D particularly in buildings such as the Pokémon Centre and Poké Mart. As you move throughout the overworld, you can see the building from a subtly different angle.
The 3D camera did not extend to most interiors however. In the player’s starting bedroom, the environment appears fully 2D. This is largely true across the entire game.
The Distortion World
In Pokémon Platinum, we saw an additional leap forward in 3D visuals. The distortion world, where the player meets the legendary Pokémon Giritina, introduces this very cool gravity shifting effect which fully leverages the 3D nature of the camera and sprites.
Skip to 1 minute in to see how the perspective changes:
Every game in Generation IV inched closer to a true 3D environment. It was not until Generation V however that this was delivered.
Enter Black & White
The fifth generation of Pokémon games - Black and White, along with their excellent sequels Black 2 and White 2 - are generally regarded to be the best, or certainly among the top of what the franchise has produced.
Black 2 has a particularly important place for me, as it was my very first Pokémon game. A few months later, I convinced my parents to pick up SoulSilver and proceeded to sink many hundred hours of my life into it.
More of everything please
Black and White doubled down on the 3D pixel art aesthetic immensely. Virtually everything in the game had added perspective, from the game world to Pokémon battles.
Remember the player’s bedroom from before? It’s fully 3D now.
All of the objects are rendered with proper perspective. You can walk around them and see the object from different angles.
The over-world saw a similar treatment. More and more of the environment was filled with real 3D objects. The best example is in how the games render trees - in Gen 4, they were sprites. Now they are actual 3D meshes.
In fact, the only game elements that really remained 2D were the character sprites themselves (and of course the Pokémon in battle). However if you look closely, you’ll find that even these are true 3D objects with sprites drawn on them (to use the technical term: ‘Billboards’).
Dynamic Camera
Black and White also brought us some impressive (for the time) set pieces. Sky Arrow Bridge and Castelia City were huge new environments which used dynamic camera angles and clever sprite trickery to make some truly stunning scenes.
Keep in mind, this all ran on the Nintendo DS - a device with a measly 4 Megabytes of RAM.
How do we build it?
The best way to understand something is to try it and get stuck. So, let’s build a simple graphics engine that mimics the above style.
In this case, I’m leveraging the game engine I am building for my own game. It uses OpenGL and Zig with SDL as a windowing library. There is no specific reason for this tech stack, I just happen to like Zig a lot.
The concepts here are universal. You can do them with any graphics API (e.g. OpenGL/DirectX/Metal/WebGPU/what have you) and programming language which supports them.
This article assumes some existing knowledge of 3D graphics. I encourage you to follow along and comment if you have questions.
Dissecting the scene
Let’s break down a screenshot from the game, to try understand how this effect was accomplished. While it may seem simple at first, there are quite a few techniques being layered here which complicates what we see at face value.
From the above screenshot, we can see:
Perspective Projection: The building is a real 3D object with real perspective. As you move from side-to-side, you can see the building from a subtly different angle.
Camera Angle: The camera is titled down at exactly 45 degrees.
Sprites: 2D sprites for characters exist inside a 3D game world.
Let’s look at another excerpt from the game.
At 3m50s in the following video, we can see some interesting perspective tricks used for the sprites at different angle.
Notice that the sprites are never viewed from “the side”. It is always displayed flat on the screen, a bit like a sticker.
From this, we can say that Black & White has:
Real 3D scenes with low-poly geometry
Low resolution ‘pixel-art’ textures
Sprites are billboards within the 3D scene
What is billboarding you ask? It’s creating a flat plane with a 2D texture “sticker” on it (like a real life billboard). This plane is a real 3D object which is rotated to face the camera so it appears to fit in the scene.
The use of billboards in Black & White is interesting, because I believe this is different to how Diamond & Pearl approached sprites. The reason for this is a specific consequence of the Nintendo DS’s hardware, which we’ll discuss later on. For a modern reimplementation however, this distinction doesn’t really matter.
Attempt 1: Low-Poly 3D
At first glance, Pokémon Black & White simply renders a 3D scene at the DS’s very low internal resolution.
In my first attempt to recreate this effect, I created a simple 3D “level” (if you can call it that). I then added a plane rotated at 45 degrees (note the camera is also 45 degrees) and put a character sprite on it. I borrowed a sprite of Crono from Square’s 1995 RPG Chrono Trigger (great game).
You can see me pan around the scene:
There are a few things wrong with this:
Simply rotating a plane at 45 degrees works when it is dead-centre, but fails the minute the character sprite goes off to the side (you can see it’s a rotated plane, rather than a “real” feeling object in 3D space).
The scene is not really a “pixel art” scene, as it’s rendered at a high resolution. This is not a problem per-se, and I quite like the look, but for the purpose of completeness I’d like this to work at a low resolution too.
The first pain point is easy to solve. The reason the above one looks bad is because of how I implemented Billboarding.
The solution was actually quite simple. The billboard needs to be rotated to face the camera at all times. I also added some random 3D models, made Crono’s sprite animate, and put debug lines everywhere.
You can see this in the next video:
Much better!
Now we could stop here - and for most modern games - it would make sense to. If for nothing else but the sake of completeness however, I would like to recreate the effect with the proper resolution.
Attempt 2: Running at Low-Res
To achieve the pixellation effect seen on Nintendo DS games, we’ll need to be rendering at a much lower resolution. To achieve this, I created an internal framebuffer texture of size 640x360 pixels. This is a fair bit higher than the resolution used by the DS, but is still pixellated and gives us a widescreen aspect ratio (rather than 4:3). I upscale using integer scaling to preserve the pixel grid.
As the Nintendo DS does not use/support anti-aliasing, I decided to forgo that here too.
I also added a few more gameplay features, like stairs and some basic height-map based collision.
The result is… very good actually.
The Good:
The low-poly 3D models look quite nice actually
Scaling isn’t too bad
The Bad:
Billboard sprites look terrible
The artefacts from scaling our sprites are the real problem here, and are what had me stumped for the longest time.
Sprite Rendering in DS Pokémon Titles
How is it that the Pokemon games achieved crisp sprites with relatively little distortion while ours look like a blurry mess?
The answer is very anti-climactic, but non-trivial to achieve. For Black & White, it effectively appears to be a combination of clever placement and sensible defaults to provide a “correct” scaling for sprites in the middle of the screen with the precision deteriorating towards the peripheries.
Disclaimer: While these are thoroughly educated guesses, there is also a good degree of speculation. Please comment if you have more specific details about these games as I’d love to hear it.
Sprite Rendering: Gen 4 vs Gen 5
Before we look into Black & White further, I should mention that I did uncover an interesting difference between how Generation IV (Diamond & Pearl) and Generation V (Black & White) approach sprite rendering.
Looking at a static image above, it is quite hard to tell. But when you see the sprites in motion, a difference becomes clear.
Take a look at Cheren (the character in the back). See how his sprite appears to “pop” in and out as we walk towards and away?
This is because characters are proper billboards in Generation V, in that they are rendered with perspective so characters further away appear smaller. However due to the low resolution of the DS, this means that their sprites become misaligned to the pixel grid the further away they are from the centre of the screen.
Compare against this snippet from Pokémon Platinum (Gen IV):
See how the sprites are always rendered at a fixed size regardless of position?
In Diamond & Pearl, it’s quite likely that billboards are not used at all. Whereas in Black & White, it is almost certain that billboards are used.
Put simply, in Diamond & Pearl, the player is 2D. In Black & White, the player is 3D.
Depth Buffer Tricks
For Diamond & Pearl, I believe it is using a clever trick with the depth buffer to partially mask 2D sprites and draw them directly at scale on the screen, with occlusion to make it look like they are in a true 3D environment.
As the sprites are rendered directly at scale (or at the very least, the player sprite is), the sprite is reproduced as a pixel-perfect image on the screen, unaffected by any 3D transformations.
This is also why the distortion world is so impressive. Notice how when the player changes gravity, the sprite is not actually stretched or deformed, but rather displayed at a perfect 90 degree rotation so the pixels are all intact - it is just sideways.
Let’s talk through it a bit more.
The Graphics Architecture of the Nintendo DS
We need to understand a bit about how the Nintendo DS works to analyse the mixed 2D + 3D effect.
I strongly recommend the following video, as it does a fairly technical deep dive into the Nintendo DS’ graphics architecture.
(I will summarise below so you can come back to it at the end)
The Nintendo DS’s internal graphics hardware allow for each screen to have a dedicated 2D Renderer. You, as a hypothetical DS developer, will assign one screen to have the ‘Main’ renderer and the other to have the ‘Sub’ renderer. Naturally, the screen with the main renderer is given greater resources and is thus more powerful.
The DS gives the developer a number of hardware layers. The exact number depends on the currently selected Video Mode. Each layer contains a different image, which themselves can be constructed out of a number of sprites, and these are composited together to make the final image you see on the screen.
3D Rendering
There is an additional reason for the Main/Sub distinction. The Main screen can additionally take advantage of the DS’s (at the time) brand-new 3D rendering technology.
The ‘Sub’ screen cannot render 3D graphics (at least not directly).
The 3D renderer will render to the first hardware layer, before any 2D drawing is done. This allows the developer to perform 2D transformations on the 3D image later if they so desire.
Consider the Distortion World in Pokémon Platinum. Here we have a 3D layer (the level geometry) and one or more 2D layers, which contain the player sprite. The sprite is drawn on top of the scene. If any 3D object is meant to be in front of the player, the player sprite can be partially occluded to appear like it is going behind. This means that the player can appear to clip behind 3D objects, when in actuality it is on a completely separate layer above the scene.
Full 3D vs Pixel Perfection
In Black & White, the player is a real 3D object in the 3D scene. This comes with a major cost and a major benefit.
Loser: Sprites are no longer rendered with pixel precision.
Winner: The camera can be fully dynamic.
The Pokémon team at Game Freak obviously decided that a fully dynamic camera was worth the trade-off in precision, and given how good the final presentation of Pokémon Black & White was, I think so too.
Sprite Rendering & Perspective Projection
While we have a better understanding of how Black & White works, we still do not know how to actually achieve the effect.
To progress, we need to understand how perspective projection works.
Before we get into any maths, watch this video:
We observe:
In the centre of the screen, the cube appears square
The further you move it to the periphery, the more it “distorts” (i.e. we see perspective)
The Black and White effect works on this principle. In order to maintain sharpness at low resolutions, the games render the player as close to the centre of the screen as possible. At the centre of the screen, the default camera follow distance is set as to achieve a perfect 1:1 mapping of the player’s size in world space (i.e. 1x1 coordinate) to the sprite dimensions (thus achieving a pixel-accurate look).
The further you move from the centre, the more distorted sprites become, but that’s a worthwhile tradeoff because the eye demands less detail too.
So to replicate this, we’ll need to think about three things:
Camera angle
Sprite dimensions
Camera follow distance
Field of view
We can ignore (1) because the sprite is rotated to always face the camera.
So what camera follow distance (i.e. distance from player) should we use for a given sprite size?
In the game, I am using this sprite of Crono from Chrono Trigger:
The sprite is 16x35 - 16 pixels wide and 35 pixels tall. As 35 is almost 36, I’m adding an extra row of padding at the top for simplicity. Our rendered sprite will thus be 16x36.
If we use a 16x16 pixel tile as our base size, this means Crono is 1 unit wide and 2.25 units tall (16 x 2.25 = 36).
How can we calculate the exact camera distance to make the sprite render perfectly 1:1 on screen?
Attempt 3: Perspective Magic
Let’s revisit our implementation and try again at the pixel-perfect renderer.
Recall the internal resolution of the game is 640x360 pixels. I chose this because 640x360 is a perfect multiple of 16:9 (widescreen).
Aspect ratio will be quite important when it comes to pixel scaling, so I chose this resolution to avoid any kind of scaling/cropping. 640x360 is a sensible option which is used by some popular games including Sea of Stars (according to this review by PC Mag anyway).
Alright. Let’s fix those sprites shall we?
An easy first step is to set the GL texture filtering to nearest neighbour.
This gets rid of some of the “fuzziness” around the tree sprites, and they actually look quite good.
Next I created a test sprite with a checkerboard pattern to debug pixel rendering:
What does it look like in game?
Computer, enhance.
We can see a half-pixel as a result of sampling.
It is important to note that the size of everything in the game world right now is completely arbitrary. I just thought this size looked nice.
We need to tighten this up to get proper accurate sprite rendering.
Centre Sprite
First, let’s properly centre the sprite in the frame.
Note, most of the mathematics are based on this fantastic overview of projection matrices in OpenGL: http://www.songho.ca/opengl/gl_projectionmatrix.html
This is a billboard when the camera is tilted 0 degrees vs 45 degrees:
Using the origin of the sprite (the red circle), we want to figure out where to place the camera such that the camera looks directly at the centre of the sprite.
We already know the height of the sprite (this is the actual pixel size of the image to show) and the camera pitch (“tilt down” amount). The distance of the camera from the billboard will be our zoom factor which we control. For now, let’s just choose ‘d = 1.0’.
The blue point ‘A’ is where the camera ray intersects with the billboard. It is exactly in the middle of the billboard (i.e. 1/2 w away from the origin).
To find the camera, we break the equation into two parts. We first find Point A, then we find the distance of the camera from Point A.
Here’s the first part. I hope you remember trigonometry :)
Next we want to find the position of the camera away from Point A:
This is just more SOH CAH TOA. As we have a right angle, we use (90 - θ) to work out the vertical and horizontal offsets.
Lastly, we need to combine our two measurements to get the total offset of the camera from the sprite origin.
I end up inverting the order of Cz, as I want it to be a negative value. This gives roughly this code in Zig:
const player_height = 2.25; // in world units, arbitrary
const angle_down: f32 = camera.pitch; // e.g. 45deg
const distance_away: f32 = 1.0; // temporary
// This is to look directly at the centre of the player billboard, assuming it is rendered at 'angle_down' from the horizontal
const y_camera_offset = (distance_away * std.math.cos(std.math.degreesToRadians(90.0 - angle_down))) + (0.5 * player_height * std.math.sin(std.math.degreesToRadians(angle_down)));
const z_camera_offset = (distance_away * std.math.sin(std.math.degreesToRadians(90.0 - angle_down))) - (0.5 * player_height * std.math.cos(std.math.degreesToRadians(angle_down)));
I also extended the multicolour band from the top to the middle of the sprite to show where the centre is.
Using a distance of n=1.0, we have the sprite perfectly centred:
Proof, when using distance n=10.0:
With this, it’s a lot easier to work on specific sizing and alignment.
Size Control
So we have a sprite exactly in the centre, but we still haven’t solved the sizing problem. What’s the next step?
We want to answer a simple question:
What distance d should the camera be from the sprite so the sprite is exactly 36 pixels tall and 16 pixels wide?
The answer is unfortunately that we need to do more maths.
Making the billboard take up the full screen height is fairly trivial.
If the billboard is 2.25 world units high (recall this is a completely arbitrary number. I just happen to want 1 world unit to be a 16x16 pixel tile), then we can form the following triangle:
NOTE: that the angle here (45 degrees) is NOT related to the angle from the horizontal. This is our Vertical FOV which you would have set up when you created your perspective projection camera.
Half of a 45deg FOV is 22.5, which we use above.
A perspective camera has a near plane and a far plan (shown in blue and red). We want the billboard to take up the entirety of the near plane, which will be the case when the camera is distance d away from the billboard.
We solve for d:
In our case we have:
d=1.125/tan(22.5)
Stick d in our camera distance algorithm from above and we get:
Nice!
The final step is to now make this take up exactly 16x36 pixels on screen.
We know the screen is 360 pixels tall. This means from the centreline, it is 180px tall. The texture is 18 pixels tall from its centre line.
Are you seeing it?
We redraw our triangle, but instead of 1.125, we have 1.125*(180/18)=11.25:
Stick this in the camera algorithm and we get a perfectly scaled sprite, inside a full real perspective projection:
Computer, enhance!
Amazing
Let’s swap Crono back in
Wow…
Before:
After:
Crystal clear.
Job done.
Sample Code
While I have not provided a full source code example (due to the impracticality of untangling it from the rest of my engine), here is an excerpt of the camera logic from my game. It combines the above algorithm with smooth follow and an ‘orbit’ value which allows the camera to rotate around the sprite, maintaining its follow distance. Hopefully there is sufficient detail here to help you implement your own:
const sprite_position = Vec3{ .x = player_center_x, .y = elevation, .z = player_center_z };
// You can rotate the camera around the billboard using this value
// e.g. 0.0 means default rotation, 360.0 would be the same view
const camera_orbit: f32 = 0.0;
const camera_v_fov: f32 = 45.0; // Vertical FOV
const sprite_height_pixels: f32 = 36.0; // Match stylesheet!!
const screen_scale_fac: f32 = internal_height / sprite_height_pixels;
const player_height: f32 = 2.25;
const angle_down: f32 = camera.pitch;
const distance_away: f32 = ((player_height * screen_scale_fac) / 2) / std.math.tan(std.math.degreesToRadians(camera_v_fov / 2.0));
// This is to look directly at the centre of the player billboard, assuming it is rendered at 'angle_down' from the horizontal
const y_camera_offset = (distance_away * std.math.cos(std.math.degreesToRadians(90.0 - angle_down))) + (0.5 * player_height * std.math.sin(std.math.degreesToRadians(angle_down)));
const z_camera_offset = (distance_away * std.math.sin(std.math.degreesToRadians(90.0 - angle_down))) - (0.5 * player_height * std.math.cos(std.math.degreesToRadians(angle_down)));
camera.yaw = camera_orbit;
// Circular motion - rotate around the billboard
const camera_orbit_offset_x = sprite_position.x - z_camera_offset * std.math.sin(std.math.degreesToRadians(camera_orbit));
const camera_orbit_offset_z = sprite_position.z + z_camera_offset * std.math.cos(std.math.degreesToRadians(camera_orbit));
const target_camera_pos = Vec3.create(
camera_orbit_offset_x,
elevation + y_camera_offset,
camera_orbit_offset_z,
);
// Camera Follow: Lerp towards target (at 60fps)
camera.x = animate.curves.EaseOutQuad(current_camera_pos.x, target_camera_pos.x, 0.125 * (delta_time / 16.66));
camera.y = animate.curves.EaseOutQuad(current_camera_pos.y, target_camera_pos.y, 0.125 * (delta_time / 16.66));
camera.z = animate.curves.EaseOutQuad(current_camera_pos.z, target_camera_pos.z, 0.125 * (delta_time / 16.66));
Conclusion & Final Thoughts
So there we are: a mostly clean room reimplementation of the Black & White 2.5D effect. It’s not perfect, but I think it’s pretty damn close.
This blog took ten times longer than it should have due to this sprite rendering question. What should have been simple perspective rendering at low resolution became a four-thousand-word-plus adventure into Nintendo DS hardware, graphics drivers, and niche rendering techniques passed through word of mouth over centuries at least a few years.
With the demo built, and all said and done, I did one final search on the topic.
…and stumbled across this…
“Difficulties with perspective rendering of pixel art sprites”
That sounds interesting…
…
What does it say?
How do I ensure that the player character sprite is always in the centre of the screen, and is exactly 16 screen pixels wide (while still making sense physically)? I know this is technically possible because the games Pokemon Black and Pokemon White use this style of rendering in the overworld, and the player character is always the correct size and at the centre of the screen relative to the DS screen resolution.
Incredible!
“The Trouble With Recreating Nostalgia”
In the process of building this, we’ve uncovered what I would call one of the main flaws of trying to “scale up” the Nintendo DS. The games were designed for a small screen with low pixel density, and very much depend on that for the effect. When scaled up, we deal with misaligned textures, wonky 3D models, and some very jarring texture filtering.
With my ongoing passion project to create a Nintendo DS inspired RPG, I’ve battled with a key question:
“Should we recreate retro art styles faithfully, or in the way we remember them?”
Here I would argue the latter. While there is certainly a market for creating games with original tools and techniques, player expectations are heavily skewed towards modern consoles and devices. This means widescreen, 60fps, crisp text rendering, and all the usual bells and whistles.
Indeed, this is the path followed by most Nintendo DS emulators. Rather than rendering games with 100% accuracy, they instead opt to rebuild the NDS’s rendering pipeline on top of modern technologies. This allows for native rendering at higher resolutions, much better performance, quality upscaling, texture filtering, antialiasing, and a lot more.
I’ve taken this same approach with my game (turn your brightness up!):
Show and Tell: A simple scene in the Zephyr engine. When using the pixel-perfect sprite placement technique discussed in this post, combined with some deferred lighting, the effect is quite stellar.
That brings us to the end of the article. I hope you’ve found this helpful or interesting, whether for your own game dev journey or just for curiosity.
Enjoy! -Matt
Spoiler: Yes, this engine is for my game. You have been tricked by my cunning plan into reading a 10 minute long advertisement. Subscribe to my Substack to see more (it’s free!)
Attribution
Osaka Castle Model: https://caspercroes.itch.io/osaka-castle-picocad
Crono Sprite (Chrono Trigger 1995 - Square)
Pokémon Tree Sprite (Pokémon Platinum 2008 - Game Freak / Nintendo)
Wood Texture (Minecraft 2011 - Mojang)
Reading List
http://problemkaputt.de/gbatek.htm#dstechnicaldata
http://www.songho.ca/opengl/gl_projectionmatrix.html
This one has nothing to do with the topic, but it is cool:
https://www.david-colson.com/2021/11/30/ps1-style-renderer.html
Hey, I've come across this Post but dont know if you're still active but anyway I'm still in school and would like to Use something like this in my Schoolproject. Tho im not sure if its possible for me to recreate all of this. Thats why i wanted to ask you if you would be interested in helping me out when theres a problem that occurs.
Kind Regards