LOW POLY GRAPHICS: A TECHNICAL LOOK
I got into low poly because it looked cool. I stayed in it because the tech is genuinely elegant. There's a specific kind of satisfaction in opening a profiler and seeing a whole scene render in under two milliseconds because you made smart choices about polygons, draw calls, and shading. A well-built low poly game is basically a lesson in GPU efficiency wearing a stylish hat.
Most articles about low poly graphics stop at "it looks nice and runs fast," which is true but useless if you're actually building one. So let's talk about what's happening in the pipeline when a low poly scene hits your screen. Polygon budgets, vertex shaders, shading models, batching, lightmaps, shadows, LODs, and why mobile GPUs love this art style more than any other. The full technical picture, from my actual experience shipping this stuff.
What counts as "low" poly anyway
Before getting into the rendering math, it helps to have a number in mind. Low poly isn't a fixed polygon count, it's a relative thing, but here's roughly where I work.
A hero character might be 2,000 to 5,000 triangles. A simple NPC, 500 to 1,500. A tree, usually under 200. A rock, 20 to 60. A small prop like a mug or a book, often under 30. A typical building exterior, a few hundred. A full environment scene that renders in one frame might total 150,000 to 400,000 triangles, including every background object.
Compare that to a modern AAA game where a single hero character can be 100,000 triangles before hair and accessories, and a full scene routinely pushes 10 million. We're running on roughly one-thirtieth the geometry. That's not a minor optimization, that's a fundamentally different performance profile, and everything else flows from it.
If you want more on the modeling side of this, I covered it in my low poly 3D models guide. For the rest of this post I'm focused on what happens after the model exists.
Vertex shaders and why low poly is almost free
The GPU processes geometry in two stages. First, the vertex shader runs once per vertex and figures out where that vertex lives in screen space, what color or lighting data it carries, and any deformations from animation or procedural effects. Then the rasterizer fills in the pixels between those vertices and the fragment shader runs per pixel to figure out the final color.
Low poly crushes the first stage. If your whole scene has 200,000 vertices across all objects, your vertex shader runs 200,000 times. A modern GPU can run vertex shaders in the billions per second. So the vertex stage on a low poly scene is basically noise, it costs you nothing.
This has a cascading benefit. You can put more work into your vertex shader without tanking performance. Procedural vertex animation, like grass swaying or water waves, becomes cheap when you only have a few thousand vertices to process. Custom per-vertex lighting calculations, the kind that would be prohibitive on high poly meshes, work fine. I've shipped shaders that do four texture samples and a full noise function per vertex and still run at 500+ FPS, because the vertex count was tiny.
The pixel stage is a different story. Pixels still need to get shaded no matter how few polygons you have. If your scene covers the screen, you're still shading 2 million pixels at 1080p. So the trick isn't "ignore fragment cost," it's "keep the fragment shader simple because your geometry already did the heavy lifting."
Flat vs smooth shading
This is where low poly diverges hardest from standard 3D rendering. In a high poly workflow, every surface gets smooth shading so the polygon edges disappear and the model looks curvy. In low poly, you want the opposite. You want every polygon to look like a distinct flat plane with its own visible light value.
There are two ways to get flat shading. The first is to split the mesh so each face has its own unique vertices, not shared with neighbors. Then each face has independent normals, and normal-based lighting produces per-face flat shading automatically. The downside is your vertex count roughly triples because shared vertices become unique ones. For a 500-triangle model that's a jump from roughly 250 vertices to 1,500. Still tiny, but worth knowing.
The second way is a flat shading shader. You use the same shared-vertex mesh but tell the shader to compute the face normal from the triangle itself, using the derivative functions ddx and ddy on the world position. This gives you flat shading without splitting the mesh, so vertex count stays low. The downside is you can't use vertex colors for per-face tinting because the vertices are shared between faces.
I use both techniques depending on the asset. For hand-painted vertex-color models I split the edges. For large environment meshes where vertex count matters, I use the derivative-based flat shader. Either way, the result is that each polygon catches light as its own little flat panel, and the whole low poly aesthetic clicks into place.
Smooth shading still has a role. Rounded organic things like smooth stones, water surfaces, or characters with curved body parts benefit from smooth normals even in a low poly style. The trick is using smooth and flat shading selectively within the same scene, sometimes within the same model. Chunky faceted rocks with a smooth river running through them looks way better than forcing one shading mode on everything.
If you care about how lighting interacts with low poly surfaces in more depth, I wrote a whole thing on smooth lighting for low poly in Unity that covers the shader setup.
Draw calls, the actual bottleneck
Here's the thing people miss about low poly performance. The polygon count doesn't matter much on modern hardware. What matters is draw calls. Every time the CPU tells the GPU "render this object with this material," that's a draw call, and it costs real CPU time to set up. On mobile hardware you want to stay under maybe 100 draw calls per frame. On PC you can push a few thousand, but fewer is always faster.
A naive low poly scene can have thousands of draw calls if you're not careful. One for each tree, each rock, each grass patch, each building. Even if each object is only 50 triangles, the draw call overhead piles up and eventually you're CPU-bound while your GPU sits idle.
The fix is batching. You combine objects that share a material into a single draw call. Unity has static batching, dynamic batching, and the SRP batcher. Unreal has instanced static meshes and nanite batching. Godot has multimesh instances. Every engine has some version of this, and for low poly games it's essential.
The practical approach is to have a small number of shared materials, ideally one or two. If every object in your game uses the same low poly shader with the same texture atlas, the engine can batch aggressively. I've shipped scenes with 5,000 visible objects running on 40 draw calls because everything shared one material. That's the whole secret to low poly performance on weak hardware.
Mesh instancing for repeated assets
Instancing is batching's more precise cousin. Instead of combining many different meshes into one draw, instancing tells the GPU "render this one mesh a thousand times at these thousand positions." The GPU stores the mesh once and renders it repeatedly with different transforms pulled from an array.
For low poly scenes this is gold. Grass, leaves, rocks, fence posts, streetlights, repeating architectural elements, anything that appears many times. All of it should be instanced. One tree mesh rendered 500 times across a forest is way cheaper than 500 separate tree meshes, even if each mesh is the same geometry.
In Unity you can use Graphics.DrawMeshInstanced or the GPU Resident Drawer. In Unreal it's HISM components. In Godot, MultiMeshInstance3D. The API names vary, the concept is the same. Get your repeated assets onto the instancing path and watch your draw calls collapse.
I also recommend mixing a small number of variant meshes instead of using one. Three tree meshes instanced 300 times each looks way less repetitive than one tree mesh instanced 900 times, and still costs basically nothing. The human eye spots identical assets fast, so invest in a handful of variations and let instancing do the heavy lifting.
Lightmap baking on low poly worlds
Real-time lighting is expensive. Every light source adds shader cost, every shadow caster adds another render pass. For a low poly scene you usually don't need real-time lighting at all. You bake it.
Lightmap baking precomputes how light bounces around a static scene and stores the result in a texture. At runtime, the GPU samples the lightmap and adds it to the surface color. No real-time light calculations, no shadow passes, just a cheap texture lookup. The scene looks beautifully lit, and it costs almost nothing per frame.
The catch with low poly is that lightmaps need UVs, and low poly models often don't have proper lightmap UVs because the flat color workflow skips UV unwrapping. You have to add a second UV channel specifically for lightmapping. Most engines can auto-generate this, which usually works fine for low poly because the geometry is simple enough that automatic unwrap doesn't mangle it.
I bake lightmaps for every static environment in my games. Terrain, buildings, trees that don't move, props that won't be picked up. The dynamic stuff like characters and physics objects uses light probes, which is a cheaper real-time approximation that samples pre-baked lighting at the object's position. The combined result looks like fully dynamic lighting but runs at a fraction of the cost.
Lightmap resolution matters too. Low poly scenes can usually get away with much lower lightmap resolution than realistic scenes because the flat-shaded surfaces already hide a lot of detail. I commonly use 16 or 32 texels per unit instead of the 128+ that realistic games use. The lightmap textures end up tiny, which means they fit in GPU memory even on mobile.
Shadow casting choices
Shadows are where I see low poly games waste performance most often. Real-time shadows are one of the most expensive things in modern rendering. Every shadow-casting light renders the scene again from the light's point of view to generate a shadow map. A scene with three shadow-casting lights renders three extra times per frame, plus the main view.
For low poly you can usually ditch most real-time shadows. Bake them into the lightmap instead. Static objects cast shadows on static objects via the lightmap, which is free at runtime. You only need real-time shadows for dynamic objects casting onto static ones, or dynamic onto dynamic.
Even then, I limit real-time shadows to one directional light, usually the sun, and a small shadow distance. On mobile I sometimes disable real-time shadows entirely and use blob shadows instead. A blob shadow is just a dark circle decal projected under a character. It's a quad with a soft round texture, costing basically nothing, and it sells "this character is grounded" way better than no shadow at all. Low poly games can get away with blob shadows because the whole style is stylized anyway.
When I do use shadow maps, I crank down the resolution. 512x512 is often plenty for a low poly scene. You don't need crisp shadow detail because the whole look is chunky. Soft blocky shadows match the aesthetic and save a lot of memory and fill rate.
The LOD system
Level of Detail is the technique of swapping in simpler meshes as objects get farther from the camera. For realistic games, LODs are mandatory because high poly assets would murder performance at distance. For low poly, LODs are optional but often still worth it.
Here's my take. If a tree is 200 triangles and the player never sees it up close, rendering it at full quality at any distance is fine. You're not saving much by swapping in a 40-triangle version. But if you have thousands of trees filling a landscape, those triangle savings multiply. Thousand trees at 200 tris is 200,000 triangles. Switch to 40-tri versions at distance and you save 160,000 triangles. That might matter on mobile.
The bigger LOD win for low poly is usually reducing draw calls, not triangles. You can swap a full tree for a single-quad billboard at long distance. The billboard is two triangles and can be batched with thousands of other billboards into one draw call. That's a massive performance win for the forest.
My rule: use LODs for anything that appears at huge distances in large quantities. Trees, distant buildings, terrain chunks. Skip LODs for hero props, interactive objects, or anything the player always sees up close. The complexity cost of maintaining LODs isn't worth it for low-count assets.
Performance on mobile
Low poly was basically born for mobile. The combination of low vertex counts, simple shaders, aggressive batching, and baked lighting hits mobile GPUs in exactly the sweet spot they were designed for.
Mobile GPUs have limited memory bandwidth and limited fill rate, but they handle simple geometry and simple shaders well. A low poly game with one material, flat shading, baked lightmaps, and blob shadows can run at 60 FPS on a five-year-old phone. I've shipped stuff that pulls less than 3W on a Snapdragon, which means the battery doesn't drain and the phone doesn't thermal throttle, which means the game stays playable for hours.
The specific mobile tricks I use. Keep texture sizes small, 512 or 1024 at most. Use compressed texture formats, ETC2 on Android, ASTC on iOS. Disable anti-aliasing or use FXAA, MSAA is expensive on mobile tile-based GPUs. Use half-precision floats in shaders when you can. Cap the frame rate at 60 to save battery. Use occlusion culling so you're not shading objects behind walls.
Low poly makes all of this easier because the art style is already simple. You're not fighting to fit detailed characters into mobile constraints, you're starting from a place that already fits. The Polylusion art style we settled on was partly driven by wanting our games to run on phones without compromise, and it turned out to look great on PC too.
The tradeoffs that add up
Every technique in this post is a tradeoff. Flat shading trades smooth curves for polygon clarity. Baked lighting trades dynamism for speed. Shared materials trade per-object customization for batching. Instancing trades variety for count. LODs trade authoring complexity for runtime perf.
The reason low poly games run on anything isn't one of these tradeoffs, it's all of them stacked. Each saves a little. Combined, they turn a 20-millisecond frame into a 2-millisecond frame. That difference is what lets a small studio ship a game that runs smoothly on a Steam Deck, a phone, an old laptop, and a current-gen console without having to build three different pipelines.
I think that's the actual magic of this aesthetic. It's not just that low poly looks cool, it's that every part of the pipeline cooperates. The art style, the rendering techniques, the performance profile, the hardware targets, they all reinforce each other. You make the art simple, and everything downstream gets easier. The game ships faster, runs better, looks distinctive, and works on devices the AAA pipeline can't touch. That's a pretty good deal.
LIKED THIS? STAY IN THE LOOP
New posts, game updates, and things you won't find anywhere else.