Skip to content

Concetti di Rendering di Base

WARNING

Although Minecraft is built using OpenGL, as of version 1.17+ you cannot use legacy OpenGL methods to render your own things. Instead, you must use the new BufferBuilder system, which formats rendering data and uploads it to OpenGL to draw.

Per riassumere, devi usare il sistema di rendering di Minecraft, o crearne uno tuo che utilizza GL.glDrawElements().

Questa pagina coprirà le basi del rendering usando il nuovo sistema, andando ad usare terminologia e concetti chiave.

Anche se molto del rendering in Minecraft è astratto attraverso i vari metodi DrawContext, e probabilmente non ti servirà nulla di quel che viene menzionato qui, è comunque importante capire le basi di come funziona il rendering.

Il Tessellator

Il Tessellator è la principale classe usata per renderizzare le cose in Minecraft. È un singleton, cioè solo un'istanza è presente in gioco. Puoi ottenere l'istanza usando Tessellator.getInstance().

Il BufferBuilder

Il BufferBuilder è la classe usata per formattare e caricare i dati di rendering su OpenGL. Viene usata per creare un buffer, che viene caricato su OpenGL per essere disegnato.

Il Tessellator viene usato per creare il BufferBuilder, che viene usato per formattare e caricare i dati di rendering su OpenGL. Puoi creare un BufferBuilder usando Tessellator.getBuffer().

Inizializzare il BufferBuilder

Prima di poter scrivere al BufferBuilder, devi inizializzarlo. Questo viene fatto usando BufferBuilder.begin(...), che prende un VertexFormat ed una modalità di disegno.

Formati Vertex

Il VertexFormat definisce gli elementi che includiamo nel nostro buffer di dati e precisa come questi elementi debbano essere trasmessi a OpenGL.

I seguenti elementi VertexFormat sono disponibili:

ElementoFormato
BLIT_SCREEN{ posizione (3 floats: x, y e z), uv (2 floats), colore (4 ubytes) }
POSITION_COLOR_TEXTURE_LIGHT_NORMAL{ posizione, color, texture uv, luce texture (2 shorts), texture normale (3 sbytes) }
POSITION_COLOR_TEXTURE_OVERLAY_LIGHT_NORMAL{ posizione, colore, texture uv, overlay (2 shorts), luce texture, normale (3 sbytes) }
POSITION_TEXTURE_COLOR_LIGHT{ posizione, texture uv, colore, luce texture }
POSITION{ posizione }
POSITION_COLOR{ posizione, colore }
LINES{ posizione, colore, normale }
POSITION_COLOR_LIGHT{ posizione, coloer, luce }
POSITION_TEXTURE{ posizione, uv }
POSITION_COLOR_TEXTURE{ posizione colore, uv }
POSITION_TEXTURE_COLOR{ posizione, uv, colore }
POSITION_COLOR_TEXTURE_LIGHT{ posizione, colore, uv, luce }
POSITION_TEXTURE_LIGHT_COLOR{ posizione, uv, luce, colore }
POSITION_TEXTURE_COLOR_NORMAL{ posizione, uv, colore, normale }

Modalità di Disegno

La modalità di disegno definisce come sono disegnati i dati. Sono disponibili le seguenti modalità di disegno:

Draw ModeDescrizione
DrawMode.LINESOgni elemento è fatto da 2 vertici ed è rappresentato da una singola linea.
DrawMode.LINE_STRIPIl primo elemento richiede 2 vertici. Elementi addizionali vengono disegnati con solo un nuovo vertice creando una linea continua.
DrawMode.DEBUG_LINESSimile a DrawMode.LINES, ma la linea è sempre esattamente larga un pixel sullo schermo.
DrawMode.DEBUG_LINE_STRIPCome DrawMode.LINE_STRIP, ma le linee sono sempre larghe un pixel.
DrawMode.TRIANGLESOgni elemento è farro da 3 vertici, formando un triangolo.
DrawMode.TRIANGLE_STRIPInizia con 3 vertici per il primo triangolo. Ogni vertex addizionale forma un nuovo triangolo con gli ultimi due vertici.
DrawMode.TRIANGLE_FANInizia con 3 vertici per il primo triangolo. Ogni vertex addizionale forma un triangolo con il primo e l'ultimo vertice.
DrawMode.QUADSOgni elemento è fatto da 4 vertice, formando un quadrilatero.

Scrivere al BufferBuilder

Una volta che il BufferBuilder è inizializzato, puoi scriverci dei dati.

Il BufferBuilder permette di costruire il nostro buffer, vertex per vertex. Per aggiungere un vertex, usiamo il metodo buffer.vertex(matrix, float, float, float). Il parametro matrix è la matrice di trasformazione, della quale discuteremo più dettagliatamente in seguito. I tre parametri float rappresentano le coordinate (x, y, z) della poszione del vertex.

Questo metodo restituisce un vertex builder, che possiamo usare per specificare informazioni addizionali per il vertex. È cruciale seguire l'ordine del nostro VertexFormat definito quando aggiungiamo questa informazione. Se non lo facciamo, OpenGL potrebbe non interpretare i nostri dati correttamente. Dopo aver fintio la costruzione del vertex, chiamiamo il metodo .next(). Questo finalizza il vertex corrente e prepare il builder per il prossimo.

Importante è anche capire il concetto di culling. Il culling è il processo con cui si rimuovono facce di una forma 3D che non sono visibili dalla prospettiva dell'osservatore. Se i vertici per una facca sono specificate nell'ordine sbagliato, la faccia potrebbe non essere renderizzata correttamente a causa del culling.

Cosìè una Matrice di Trasformazione?

Una matrice di trasformazione è una matrice 4x4 che viene usata per trasformare un vettore. In Minecraft, la matrice di trasformazione sta solo trasformando le coordinate che diamo nella chiamata del vertex. Le trasformazione possono scalare il nostro modello, muoverlo in giro e ruotarlo.

A volte viene chiamata anche matrici di posizione (position matrix), o matrice modelle (model matrix).

Soltiamente è ottenuta dalla classe MatrixStack, che può essere ottenuto attraverso l'oggetto DrawContext:

java
drawContext.getMatrices().peek().getPositionMatrix();

Un Esempio Pratico: Renderizzare una striscia di Triangoli

Spiegare come scrivere al BufferBuilder è più semplice con un esempio pratico. Diciamo che vogliamo renderizzare qualcosa usando la modalità di disegno DrawMode.TRIANGLE_STRIP e il formato vertex \`POSITION_COLOR.

Disegneremo vertici ai seguenti punti nella HUD (in ordine):

txt
(20, 20)
(5, 40)
(35, 40)
(20, 60)

Questo dovrebbe darci un diamante carino - siccome staimo usando la modalità di disegno TRIANGLE_STRIP, il renderizzatore farà i seguenti step:

Quattro step che mostrano il piazzamento dei vertici sullo schermo per formare due triangoli.

Siccome stiamo disegnando sulla HUD in questo esempio, useremo l'evento HudRenderCallback:

java
HudRenderCallback.EVENT.register((drawContext, tickDelta) -> {
	// Get the transformation matrix from the matrix stack, alongside the tessellator instance and a new buffer builder.
	Matrix4f transformationMatrix = drawContext.getMatrices().peek().getPositionMatrix();
	Tessellator tessellator = Tessellator.getInstance();
	BufferBuilder buffer = tessellator.getBuffer();

	// Initialize the buffer using the specified format and draw mode.
	buffer.begin(VertexFormat.DrawMode.TRIANGLE_STRIP, VertexFormats.POSITION_COLOR);

	// Write our vertices, Z doesn't really matter since it's on the HUD.
	buffer.vertex(transformationMatrix, 20, 20, 5).color(0xFF414141).next();
	buffer.vertex(transformationMatrix, 5, 40, 5).color(0xFF000000).next();
	buffer.vertex(transformationMatrix, 35, 40, 5).color(0xFF000000).next();
	buffer.vertex(transformationMatrix, 20, 60, 5).color(0xFF414141).next();

	// We'll get to this bit in the next section.
	RenderSystem.setShader(GameRenderer::getPositionColorProgram);
	RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);

	// Draw the buffer onto the screen.
	tessellator.draw();
});

Questo risulta nel disegno della cosa seguente nella HUD:

Risultato Finale

TIP

Prova a giocare coi colori e le posizione dei vertici per vedere che succede! Puoi anche provare ad usare modalità di disegno e formati vertex differenti.

La MatrixStack

Dopo aver imparato come scrivere al BufferBuilder, ti potresti chiedere come trasformare il tuo modello - anche animarlo magari. Qui è dove entra in gioco la classe MatrixStack.

La classe MatrixStack ha i seguenti metodi:

  • push() - Spinge una nuova matrice sullo stack.
  • pop() - Elimina la matrice in cima allo stack.
  • peek() - Restutuisce la matrice in cima allo stack.
  • translate(x, y, z) - Trasla la matrice in cima allo stack.
  • scale(x, y, z) - Scala la matrice in cima allo stack.

Puoi anche moltiplicare la matrice in cima allo stack usando i quaternioni, che copriremo nella prossima sezione.

Usando l'esempio di prima, possiamo scalare il nostro diamante su e giù usando la MatrixStack e il tickDelta - che è il tempo passato dall'ultimo frame.

WARNING

You must push and pop the matrix stack when you're done with it. If you don't, you'll end up with a broken matrix stack, which will cause rendering issues.

Assicurati di pushare la stack di matrci prima di prendere una matrice di trasformazione!

java
MatrixStack matrices = drawContext.getMatrices();

// Store the total tick delta in a field, so we can use it later.
totalTickDelta += tickDelta;

// Push a new matrix onto the stack.
matrices.push();
// Scale the matrix by 0.5 to make the triangle smaller and larger over time.
float scaleAmount = MathHelper.sin(totalTickDelta / 10F) / 2F + 1.5F;

// Apply the scaling amount to the matrix.
// We don't need to scale the Z axis since it's on the HUD and 2D.
matrices.scale(scaleAmount, scaleAmount, 1F);

// ... write to the buffer.

// Pop our matrix from the stack.
matrices.pop();

Un video che mostra il diamante scalato su e giù.

Quaternioni (Cose che ruotano)

I quaternioni sono un modo di rappresentare rotazioni in uno spazio 3D. Vengono usato per ruotare la matrice in cima al MatrixStack usando il metodo multiply(Quaternion, x, y, z).

Difficilmente dovrai usare una classe Quaternion direttamente, siccome Minecraft fornisce vaire istanze Quaternion pre-compilate nella sua classe di utility RotationAxis.

Diciamo che vogliamo ruotare il nostro diamante attorno all'asse z. Possiamo farlo usando il MatrixStack ed il metodo multiply(Quaternion, x, y, z).

java
// Lerp between 0 and 360 degrees over time.
float rotationAmount = (float) (totalTickDelta / 50F % 360);
matrices.multiply(RotationAxis.POSITIVE_Z.rotation(rotationAmount));
// Shift entire diamond so that it rotates in its center.
matrices.translate(-20f, -40f, 0f);

Il risultato è il seguente:

Un video che mostra il diamante ruotare attorno all'asse z.