0
This repository has been archived on 2025-04-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Minecraft-Overviewer/docs/design/designdoc.rst
2011-10-30 03:00:28 -04:00

487 lines
19 KiB
ReStructuredText

====================
Design Documentation
====================
So you'd like a technical overview of how The Overviewer works, huh? You've come
to the right place!
This document's scope does not cover the details of the code. The code is fairly
well commented and not difficult to understand. Instead, this document is
intended to give an explanation to how the Overviewer was designed, why certain
decisions were made, and how all the pieces fit together. Think of this document
as commenting on how all the high level pieces of the code work.
This document is probably a good read to anyone that wants to get involved in
Overviewer development.
So let's get started!
.. note::
This page is still under construction
.. contents::
Background Info
===============
The Overviewer's task is to take Minecraft worlds and render them into a set of tiles that can be displayed with a Google Maps interface.
A Minecraft world extends indefinitely along the two horizontal axes, and are
exactly 128 units high. Minecraft worlds are made of cubes, where each slot in
the world's grid has a type that determines what it is (grass, stone, ...).
This makes worlds relatively uncomplicated to render, the Overviewer simply
determines what cubes to draw and where. Since everything in Minecraft is
aligned to a strict grid, placement and rendering decisions are completely
deterministic and can be performed in an iterative fashon.
The coordinate system for Minecraft has three axes. The X and Z axes are the
horizontal axes. They extend indefinitely towards both positive and negative
infinity. (There are practical limits, but no theoretical limits). The Y axis
extends from 0 to 127, which corresponds with the world height limit. Each
block in Minecraft has a coordinate address, e.g. the block at 15,78,-35 refers
to 15 along the X axis, -35 along the Z axis, and 78 units up from bedrock.
The world is divided up into *chunks*. A chunk is a 16 by 16 area of the world
that extends from bedrock to sky. In other words, a 16,128,16 "chunk" of the
world. Chunks also have an address, but in only 2 dimensions. To find the which
chunk a block is in, simply divide its X and Z coordinates by 16 and take the
floor.
Minecraft worlds are generated on-the-fly by the chunk. This means not all
chunks will exist. There is no pattern to chunk generation, the game simply
generates them as needed.
Chunks are stored on-disk in region files. A Minecraft region is a "region" of
32 by 32 chunks. Regions have their own address, and for a particular chunk one
can find its region by dividing its coordinates by 32 and taking the floor. A
region may contain all 1024 of its chunks, or only a subset of them, since not
all chunks may exist. The absence of a region file indicates none of its chunks
exist.
About the Rendering
===================
Minecraft worlds are rendered in an approximated Isometric projection at an
oblique angle. In the original design, the projection acts as if your eye is
infinitely far away looking down at the world at a 45 degree angle in the
South-East direction (now, the world can be rendered at any of the 4 oblique
directions).
.. image:: screenshot.png
:alt: A screenshot of Overviewer output
In order to render a Minecraft world, there are a few steps that need to happen.
These steps are explained in detail in the next few sections.
1. Render each block
2. Render the chunks from the blocks
3. Render the tiles of the map from the chunks
4. Shrink and combine the tiles for the other zoom levels
Block Rendering
===============
.. This section shows how each block is pre-rendered
The first step is rendering the blocks from the textures. Each block is "built"
from its textures into an image of a cube and cached in global variables of the
:mod:`textures` module.
Textures come in the size 16 by 16 (higher resolution textures are resized and
the process remains the same). In order to render a cube out of this, an `affine
transformation`_ is applied to the texture in order to transform it to the top,
left, and right faces of the cube.
.. image:: blockrendering/texturecubing.png
:alt: A texture gets rendered into a cube
.. _affine transformation: http://en.wikipedia.org/wiki/Affine_transformation
The result is an image of a cube that is 24 by 24 pixels in size. This
particular size for the cubes was chosen for an important reason: 24 is
divisible by 2 and by 4. This makes placement much easier. E.g. in order to draw
two cubes that are next to each other in the world, one is drawn exactly 12
pixels over and 6 pixels down from the other. All placements of the cubes happen
on exact pixel boundaries and no further resolution is lost beyond the initial
transformations.
The transformation happens in two stages. First, the texture is transformed for
the top of the cube. Then the texture is transformed for the left side of the
cube, which is mirrored for the right side of the cube.
Top Transformation
------------------
The transformation for the top face of the cube is a simple `affine
transformation`_ from the original square texture. It is actually several affine
transformations: a re-size, a rotation, and a scaling; but since multiple affine
transformations can be chained together simply by multiplying the transformation
matrices together, only one transformation is actually done.
This can be seen in the function :func:`textures.transform_image`. It takes
these steps:
1. The texture is re-sized to 17 by 17 pixels. This is done because the diagonal
of a square with sides 17 is approximately 24, which is the target size for
the bounding box of the cube image. So when it's rotated, it will be the
correct width.
2. The image is rotated 45 degrees about its center.
3. The image is scaled on the vertical axis by a factor of 1/2.
This produces an image of size 24 by 12 as seen in the following sequence.
.. image:: blockrendering/texturetopsteps.png
:alt: The 4 steps for transforming a texture square into the top of the cube.
The final image, shown below, becomes the top of the cube.
.. image:: blockrendering/cube_top.png
:alt: Top of the cube
On the left is what will become the top of the block at actual size after the
transformation, the right is the same but blown up by a factor of 10 with no
interpolation to show the pixels.
Side Transformation
-------------------
The texture square is transformed for the sides of the cube in the
:func:`textures.transform_image_side` function. This is another `affine
transformation`_, but this time only two transformations are done: a re-size and
a shear.
1. First the texture is re-sized to 12 by 12 pixels. This is half the width of
24 so it will have the correct width after the shear.
2. The 12 by 12 square is sheared by a factor of 1.5 in the Y direction,
producing an image that is bounded by a 12 by 18 pixel square.
.. image:: blockrendering/texturesidesteps.png
:alt: Texture being sheared for the side of the cube.
This image is simply flipped along the horizontal axis for the other visible
side of the cube.
.. image:: blockrendering/cube_sides.png
:alt: The sides of the block
Again, shown on the left are the two sides of the block at actual size, the
right is scaled with no interpolation by a factor of 10 to show the pixels.
An Entire Cube
--------------
These three images, the top and two sides, are pasted into a single 24 by 24
pixel image to get the cube, as shown.
However, notice from the middle of the three images in the sequence below that
the images as transformed don't fit together exactly. There is some overlap when
put in the 24 by 24 box in which they must fit.
.. image:: blockrendering/cube_parts.png
:alt: How the cube parts fit together
There is one more complication. The cubes don't tessellate perfectly. This
diagram illustrates when a cube is positioned next to another. The lower cubes
are 18 pixels lower and 12 pixels to either side, which is half the width and
3/4 the height respectively.
.. image:: blockrendering/tessellation.png
:alt: Cubes don't tessellate perfectly
The solution is to manually touch up those 6 pixels. 3 pixels are added on the
upper left of each cube, 3 on the lower right. Therefore, they all line up
perfectly!
This is done at the end of :func:`textures._build_block`
.. image:: blockrendering/pixelfix.png
:alt: The 6 pixels manually added to each cube.
Other Cube Types
----------------
Many block types are not rendered as cubes. Fences, rails, doors, torches, and
many other types of blocks have custom rendering routines.
Chunk Rendering
===============
So now that each type of cube is rendered and cached in global variables within
the :mod:`textures` module, the next step is to use the data from a chunk of
the world to arrange these cubes on an image, rendering an entire chunk.
How big is a chunk going to be? A chunk is 16 by 16 blocks across, 128 blocks
high. The diagonal of a 16 by 16 grid is 16 squares. Observe.
This is the top-down view of a single chunk. It is essentially a 16 by 16 grid,
extending 128 units into the page.
.. image:: cuberenderimgs/chunk_topdown.png
:alt: A 16x16 square grid
Rendered at the appropriate perspective, we'll have something like this
(continued down for 128 layers).
.. image:: cuberenderimgs/chunk_perspective.png
:alt: Perspective rendering of the two top layers of a chunk.
Each of those cubes shown is where one of the pre-rendered cubes gets pasted.
This happens from back to front, bottom to top, so that the chunk gets drawn
correctly. Obviously if a cube in the back is pasted on the image after the
cubes in the front, it will be drawn on top of everything.
Cube Positioning
----------------
A single cube is drawn in a 24 by 24 square. Before we can construct a chunk out
of individual cubes, we must figure out how to position neighboring cubes.
First, to review, these are the measurements of a cube:
.. image:: cubepositionimgs/cube_measurements.png
:alt: The measurements of a cube
* The cube is bounded by a 24 by 24 pixel square.
* The side vertical edges are 12 pixels high.
* The top (and bottom) face of the cube takes 12 vertical pixels (and 24
horizontal pixels).
* The edges of the top and bottom of the cube take up 6 vertical pixels and 12
horizontal pixels each.
Two cubes that are neighbors after projection to the image (diagonally
neighboring in the world) have a horizontal offset of 24 pixels from each other,
as shown below on the left. This is mostly trivial, since the images don't end
up overlapping at all. Two cubes in the same configuration but rotated 90
degrees have some overlap in the image, and are only vertically offset by 12
pixels, as shown on the right.
.. image:: cubepositionimgs/cube_horizontal_offset.png
:alt: Two cubes horizontally positioned are offset by 24 pixels on the X axis.
Now for something slightly less trivial: two cubes that are stacked on top of
each other in the world. One is rendered lower on the vertical axis of the
image, but by how much?
.. image:: cubepositionimgs/cube_stacking.png
:alt: Two cubes stacked are offset in the image by 12 pixels.
Interestingly enough, due to the projection, this is exactly the same offset as
the situation above for diagonally neighboring cubes. The cube outlined in green
is drawn 12 pixels below the other one. Only the order that the cubes are drawn
is different.
And finally, what about cubes that are next to each other in the world ---
diagonally next to each other in the image?
.. image:: cubepositionimgs/cube_neighbors.png
:alt: Cubes that are neighbors are offset by 12 on the X and 6 on the Y
The cube outlined in green is offset on the horizontal axis by half the cube
width, or 12 pixels. It is offset on the vertical axis by half the width of the
cube's top, or 6 pixels. For the other 3 directions this could go, the
directions of the offsets are changed, but the amounts are the same.
The size of a chunk
-------------------
Now that we know how to place cubes relative to each other, we can begin to
construct a chunk.
Since the cube images are 24 by 24 pixels, and the diagonal of the 16 by 16 grid
is 16 squares, the width of one rendered chunk will be 384 pixels. Just
considering the top layer of the chunk:
.. image:: cuberenderimgs/chunk_width.png
:alt: Illustrating the width of a single chunk
Since cubes next to each other in the same "diagonal row" are offset by 24
pixels, this is trivially calculated.
The height is a bit more tricky to calculate. Let's start by calculating the
height of a single stack of 128 cubes.
If the top of a stack of cubes is at Y value 0, the 128th cube down must be
drawn (128-1)*12=1524 pixels below. However, that's not the end of the story.
The bottom cube has a height of 24 pixels, so the height of a rendered stack of
128 cube is 1548 pixels.
.. image:: cuberenderimgs/cube_stack128.png
:alt: A stack of 128 cubes takes 1560 vertical pixels to draw.
You can also calculate this by looking at the sides of the cubes, which don't
overlap at all. They are 12 pixels each, times 128 cubes in the stack, gives
1536 pixels. Add in the 6 pixels for the top cube and the 6 pixels for the
bottom cube to get the total height of 1548 pixels.
So what about the entire chunk? Let's take a look at the top and bottom few
layers of a chunk.
.. image:: cuberenderimgs/chunk_height.png
:alt: The highest and lowest positioned cubes in a chunk
Let's let the red cubes represent the stack from above. The one on the top we'll
define as position 0, with our vertical axis running positively downward (as is
the case in a lot of imaging library coordinate systems) Therefore, the bottom
red cube is at vertical offset 1524 below.
The green cube at the bottom most tip is the cube with the lowest vertical
placement on the image, so its offset plus 24 pixels for its height will be the
chunk height. Since the green cubes each have an offset of 12 pixels, add 15*12
pixels to get the offset of the lowest green cube: 1704.
So the total size of a chunk in pixels is 384 wide by 1728 tall. That's pretty
tall!
Tile Rendering
==============
.. Covers the placement of chunk images on a tile
So now that we know how to draw a single chunk, we can move on to placing them
on an image.
For the diagrams in this section, we are positioning an entire chunk, but
frequently, only the top face of the chunk is drawn (shown in green below).
.. image:: tilerendering/topofchunk.png
:alt: The top of a chunk is highlighted
This makes it easier and less cumbersome to describe chunk positionings. Just
remember that chunks extend down for 1536 more pixels.
Chunk Addressing
----------------
Chunks in Minecraft have an X,Z address, starting at 0,0 and extending to
positive and negative infinity on both axes. Since we're looking at things
diagonally, however, we need a way of addressing these chunks in the final
image. For that, we refer to them in rows and columns. Consider this grid
showing the tops of a five by five region of chunks, labeled with their in-game
addresses.
.. image:: tilerendering/chunkgrid.png
:alt: A grid of 5x5 chunks showing how chunks are addressed.
Now, we want to transform each chunk to a row/column address as shown here:
.. image:: tilerendering/chunkgridwithrowcol.png
:alt: A grid of 5x5 chunks showing how chunks are addressed.
So the chunk at address 0,0 would be at col 0, row 0; while the chunk at address
1,1 would be at col 2, row 0. The intersection of the red and green lines
addresses the chunk in col,row format.
Notice that as a consequence of this addressing scheme, there is no chunk at
e.g. column 1 row 0. There are some col,row addresses that lie between chunks
(as can be seen where the red/green lines intersect at a chunk boundary instead
of the middle of a chunk). Something to keep in mind.
So how does one translate between them? It turns out that a chunk's column
address is simply the sum of the X and the Z coordinate, while the row is the
difference. Try it!
::
col = X + Z
row = Z - X
X = (col - row) / 2
Z = (col + row) / 2
Chunk Positioning
-----------------
Again just looking at the top of a chunk, we can work out how to position them
relative to each other. This is similar to how to position blocks relative to
each other, but this time, for chunks.
A chunk's top face is 384 pixels wide by 192 pixels tall. Similar to the block,
neighboring chunks have these relationships:
.. image:: tilerendering/chunkpositioning.png
:alt: Chunk positioning diagram
But that's all pretty trivial. With this knowledge, we could draw the chunks at
the above offsets in one large image, but for large worlds, that would quickly
become too much to handle. (Early versions of the Overviewer did this, but the
large, unwieldy images quickly motivated the development of rendering to
individual tiles)
Tile Layout
-----------
Instead of rendering to one large image, chunks are rendered to small tiles.
Only a handful of chunks need to be rendered into each tile. The downside is
that chunks must be rendered multiple times for each tile they appear in, but
the upside is that arbitrarily sized maps can be viewed.
The Overviewer uses a tile size of 384 by 384 pixels. This is the same as a
width of a chunk and is no coincidence. Just considering the top face of a
chunk, 8 chunks get rendered into a tile in this configuration:
.. image:: tilerendering/chunksintile.png
:alt: The 8 chunks that get rendered into a tile
So the overall strategy is to convert all chunks into diagonal col,row
coordinates, then for each tile decide which chunks belong in it, then render
them in the appropriate place on the tile.
The rendering routines are actually passed a range of chunks to render, e.g.
rows 4-6, cols 20-24. The lower bound col,row chunk given in the range is
rendered at position 0,0 in the diagram above. That is, at offset -192,-96
pixels.
The rendering routines takes the given range of columns and rows, converts it
back into chunk coordinates, and renders the given 8 chunks plus all chunks from
the 16 rows above the given range (see the note below). The chunks are
positioned correctly with the above positioning rules, so any chunks that are
out of the bounds get rendered off the tile and don't affect the final image.
(There is therefore no penalty for rendering out-of-bounds chunks for a tile
except increased processing)
.. note::
Remember that chunks are actually very tall, so there are actually several
rows above 0 in the above diagram that are rendered into the tile. Since the
chunk outlines in the diagrams are only the top face of the chunk, they most
likely don't contribute to the image since chunks usually don't have
anything to render way up at the top near the sky.
Since every other column of chunks is half-way in two tiles, they must be
rendered twice. Each neighboring tile is therefore only 2 columns over, not 3 as
one may suspect at first. Same goes for the rows: The next tile down is 4 rows
down, not 5.
Quadtrees
=========
.. About the tile output
get_range_by_path
-----------------
.. Explain the quadtree.QuadtreeGen._get_range_by_path method
Reading the Data Files
======================
..
Covers how to extract the blocks of each chunk from the region files. Also
covers the nbt file stuff.
Image Composition
=================
..
Covers the issues I had with PIL's image composition and why we needed
something fancier.
Multiprocessing
===============
..
Covers how the Overviewer utilizes multiple processors to render faster
Caching
=======
.. How the overviewer determines what needs to be rendered and what doesn't
Lighting
========
Cave Mode
=========