Friday, 26 December 2014

The Tessellator and WorldRenderer [1.8]

Minecraft uses OpenGL to draw what you see on the screen. But interacting directly with OpenGL is quite messy, so it uses the Tessellator and WorldRenderer classes to make things easier.

What you see on the screen is created by a long list of rendering instructions that is executed by OpenGL and your graphics card. These instructions might include "draw a pixel at [3,4,5]", or "draw a line from [1,2,3] to [4,5,6]", or "draw a quadrilateral with vertices [1,2,3], [4,5,6], [7,8,9] and [10,11,12], filling it with bitmap texture abc."  Depending on your version of OpenGL, this list of rendering instructions might be either a Display list (glRenderList in forge) or a VertexBufferObject.  Minecraft has been designed to use them interchangeably.

Some advice before you start

The Tessellator is good for drawing simple things like lines or cubes.  But if you're planning to use the Tessellator for anything other than simple shapes, I recommend that you don't!  Use Techne instead, it's much easier.

Techne home website
Techne Tutorial - Part 1 - Basics

How the Tessellator and WorldRenderer work


The WorldRenderer class is used to add rendering instructions to a Display list.    A typical way to use it is:
  1. WorldRenderer worldRenderer = Tessellator.getInstance().getWorldRenderer().
  2. GLStateManager.pushAttrib() to save the rendering flags (eg lighting, alpha functions)
  3. GLStateManager.pushMatrix() to save the current rendering position.
  4. worldRenderer.setTranslation() to set the origin to an appropriate location.
  5. worldRenderer.startDrawingQuads(), which tells the renderer that you will be drawing Quads.
  6. Set up the various flags (brightness, normals, color, alpha), for example
    worldRenderer.setColorRGBA(...); or worldRenderer.setColourOpaque_I(...);
  7. Each face of each block is drawn by calling addVertexWithUV(x, y, z, u, v) four times, once for each corner of the face.  [x,y,z] are the coordinates of the vertex, and [u,v] is the coordinates of the texture pixel (texel) corresponding to that vertex.  See the picture and examples below.
    * The order of the vertices is important!  If you are looking at a face, the coordinates must be given in an anticlockwise order.  Otherwise, the face will be pointing in the wrong direction.
  8. Minecraft uses a single large texture for all block face textures.  As the icons are registered during startup, they are stitched together into a single large texture - (see glTexImage2D in TextureUtil).  When you render a face, you use the small portion of this large texture that corresponds to the appropriate icon.  The usual way to retrieve the [u,v] coordinates for your icon is
    icon.getMinU(), icon.getMaxU(), icon.getMinV(), icon.getMaxV().  For points that don't lie on the edge of the texture, use icon.getInterpolatedU(0 .. 16), icon.getInterpolatedV(0..16).  (For example: icon.getInterpolatedU(8), icon.getInterpolatedV(8) corresponds to the middle point of the icon's texture.)
  9. Tessellator.getInstance().draw() to complete the drawing.
  10. GLStateManager.popAttrib() and GLStateManager.popMatrix() to restore the previous rendering settings (avoids interfering with subsequent vanilla rendering code).

 Note that the direction of the v axis in the texture map is opposite to the y axis in world coordinates!   So for example - to draw the grass texture onto the ABCD face:

Some further examples:

Our test block: rendering an "up arrow" on the Zpos (south) face, a "5" on the east face, a "2" on the north face.
Red block points north, blue block points east.

Flipped left-right

Rotated

Weirdness, probably not very useful!

Added the world coordinates in clockwise order instead of anti-clockwise... where did the face go?

Looking south: clockwise world coordinates make the face point in the opposite direction.  (Pass 0 faces are not visible from their back).
The points don't have to be on the outer face, they can lie anywhere within the block.
Diagonal

Rotated on outer face

There are some restrictions on what you can do with quads:
  • Quads can be sheared - eg the top is pushed right relative to the bottom, or the left is pushed up relative to the right.  So long as the top edge has the same length as the bottom edge, and the left edge has the same length as the right edge, it will still render OK.
Left half of the texture, top is sheared 0.25 to the right

Right half of the texture, top is sheared 0.25 to the left

Top half of the texture, left is sheared down by 0.25

  • However, if you try to "pinch" the quad (eg the top edge is shorter than the bottom edge) it will render looking rather strange.
Top edge is not the same length as the bottom edge --> strange appearance

  • If your points aren't coplanar (eg wouldn't "lie flat" on a tabletop) then the Quad "folds" into two triangles (ABC, CDA) and may not render how you intended.
Non-coplanar points.  (Black line added to emphasise how the face has folded into two triangles.)

Using the Tessellator for other shapes

Although mostly used for Quads, the Tessellator is also suitable for drawing other shapes as well - see the list of suitable drawMode settings below, taken from GL11.class.  The vanilla code uses several of these including GL_LINES, GL_LINE_STRIP, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN.  They are used in the same way as for Quads - i.e. .startDrawing(drawMode), setting appropriate flags, adding the appropriate number of vertices, then calling draw().

public static final int GL_POINTS = 0;
public static final int GL_LINES = 1;
public static final int GL_LINE_LOOP = 2;
public static final int GL_LINE_STRIP = 3;
public static final int GL_TRIANGLES = 4;
public static final int GL_TRIANGLE_STRIP = 5;
public static final int GL_TRIANGLE_FAN = 6;
public static final int GL_QUADS = 7;
public static final int GL_QUAD_STRIP = 8;
public static final int GL_POLYGON = 9;

Changing Rendering Modes

Depending on what you're rendering, you may need to change a few of the rendering settings before starting rendering.  Generally these might be
  1. Turning alpha testing on or off (GLStateManager.disableAlpha() or .enableAlpha()) - used for transparent "cutouts" when rendering.
  2. Turning alpha blending on or off and changing the blend function (used for partially transparent textures like ice) - GLStateManager.enableBlend(), GLStateManager.blendFunc(..)
  3. Changing depth testing GLStateManager.disableDepth() 
  4. Changing back face culling GLStateManager.disableCull()
  5. Turning texturing on or off (GLStateManager.enableTexture2D())
  6. Changing the texture sheet (eg blocks sheet, or another texture of your own):
    Minecraft.getMinecraft().getTextureManager().bindTexture(TextureMap.locationBlocksTexture); or
    Minecraft.getMinecraft().getTextureManager().bindTexture(your texture sheet); 
  7. Changing the lighting mode between item lighting (with OpenGL light sources) and landscape lighting (with blocklight / sky light multitexturing) - use RenderHelper.enableStandardItemLighting() and .disableStandardItemLighting()
    Minecraft.getMinecraft().entityRenderer.enableLightMap() and .disableLightMap(), or
    OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, skyLight * 16, blockLight * 16); where skyLight = 0 - 15, blockLight = 0 - 15

A useful guide to all things OpenGL 1.1 is here.  It is superceded by a few more versions of OpenGL by now but it covers nearly all of the aspects that Minecraft uses.

Some further background information.

VertexFormat (with VertexFormatElement) is used in WorldRenderer to specify the binary format of vertices used for rendering.  Each vertex will have one or more of the following bits of information:
  • The position of that vertex
  • The colour of that vertex
  • The texture UV coordinate (floats)
  • The lightmap texture UV coordinate (shorts) – the “brightness” index (block light, skylight)
  • The normal vector at that vertex (used for items, i.e. with scene lighting enabled)
Blocks default to 1 – 4 but not 5.  Items default to 1 -3 and 5, but not 4.





6 comments:

  1. Hello there,
    I followed your instructions, but ingame the blockface does not appear.
    Here is my code:

    private static void testDraw()
    {
    Tessellator tess = Tessellator.getInstance();
    WorldRenderer worldRenderer = tess.getWorldRenderer();
    GlStateManager.pushAttrib();
    GlStateManager.pushMatrix();
    worldRenderer.startDrawingQuads();
    worldRenderer.addVertexWithUV(0, 5, 0, 0.3, 0.1);
    worldRenderer.addVertexWithUV(0, 6, 0, 0.3, 0.0);
    worldRenderer.addVertexWithUV(1, 6, 0, 0.2, 0.0);
    worldRenderer.addVertexWithUV(1, 5, 0, 0.2, 0.1);
    tess.draw();
    GlStateManager.popAttrib();
    GlStateManager.popMatrix();
    }



    I already debugged and I noticed that the code is executed, so guess ive done smth wrong here.
    But what is it? Pls help !

    sry for my bad english im german

    ReplyDelete
  2. Hi Could you post this question on the forum? http://www.minecraftforge.net/forum/index.php/board,73.0.html Lots of helpful folks there.
    At a rough guess - you are rendering to the wrong [x,y,z] location, so it's there, but you can't see it. The code looks ok.

    Dein English ist ganz gut, sicher besser als mein Deutsch :)

    -TGG

    ReplyDelete
    Replies
    1. Hey, Ive already solved my problem, it actually was at the wrong position. Your guide really helped me and i learned how minecraft works and stuff.. so thanks :D

      Delete
  3. As far as I can tell, Icons don't exist anymore in 1.8. What is the process to get texture coordinates now?

    ReplyDelete
  4. Hi
    TextureAtlasSprite might be what you're looking for, eg
    final ResourceLocation fireballRL = new ResourceLocation("dragonmounts:entities/breath_fire");
    TextureAtlasSprite sprite = Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(fireballRL.toString());
    -TGG

    ReplyDelete
  5. I really like how you formatted your notes.
    หนังตลก

    ReplyDelete