Jump to content
Search In
  • More options...
Find results that contain...
Find results in...

[Solved][1.12.2] Most Efficient Way To Render


SerpentDagger
 Share

Recommended Posts

In my 3D graphing calculator mod, the complexity of the graph being rendered can be extremely large. As such, I'm looking for the most efficient way possible with which to render the graph.

I store all the vertices of the graph within a Vec3d[][], and iterate over that array during FastTESR#renderTileEntityFast(), the code for which is shown below.

Is there a more efficient method of achieving this?

Spoiler

	@Override
	public void renderTileEntityFast(TileGCBase te, double x, double y, double z, float partialTicks, int destroyStage, float partial,
			BufferBuilder buffer)
	{
		if (te.isErrored())
			return;
		if (!te.renderReady)
			return;
		vArray = te.getVertexArray();
		disconnects = te.disconnects;
		rgba = te.rgba;
		int r = rgba[0];
		int g = rgba[1];
		int b = rgba[2];
		int a = rgba[3];
		tex = new ResourceLocation(te.tex);
		double tileCount = te.tileCount;
		double lowestF = te.lowestF;
		double highestF = te.highestF;
		boolean doLight = highestF - lowestF > 0.01;
		int lightmap = 200;
		double difY = highestF - lowestF;
		double ratio = 1;
		
		sprite = mc.getTextureMapBlocks().getAtlasSprite(tex.toString());
		
		double uMin = sprite.getMinU();
		double uMax = sprite.getMaxU();
		double vMin = sprite.getMinV();
		double vMax = sprite.getMaxV();
		
		double stepU = (uMax - uMin) / tileCount;
		double stepV = (vMax - vMin) / tileCount;
		
		double u = uMin;
		double v = vMin;
		double u2 = uMin;
		double v2 = vMin;
		
		//Part that needs optimization vvvvvv
		for (int j = 0; j + 1 < vArray.length; j++)
		{
			u = (u + stepU >= uMax - 0.000000001) ? uMin : u + stepU;
			u2 = u + stepU;
			for (int k = 0; k + 1 < vArray[j].length; k++)
			{
				v = (v + stepV >= vMax - 0.000000001) ? vMin : v + stepV;
				v2 = v + stepV;
				
				if (disconnects[j][k])
					continue;
				
				vec = vArray[j][k];
				if (doLight)
				{
					ratio = (vec.y - lowestF) / difY;
					lightmap = (int) (200 * ratio);
				}
				buffer.pos(x + vec.x, y + vec.y, z + vec.z).color(r, g, b, a).tex(u, v).lightmap(80, lightmap).endVertex();

				vec = vArray[j + 1][k];
				if (doLight)
				{
					ratio = (vec.y - lowestF) / difY;
					lightmap = (int) (200 * ratio);
				}
				buffer.pos(x + vec.x, y + vec.y, z + vec.z).color(r, g, b, a).tex(u2, v).lightmap(80, lightmap).endVertex();
				
				vec = vArray[j + 1][k + 1];
				if (doLight)
				{
					ratio = (vec.y - lowestF) / difY;
					lightmap = (int) (200 * ratio);
				}
				buffer.pos(x + vec.x, y + vec.y, z + vec.z).color(r, g, b, a).tex(u2, v2).lightmap(80, lightmap).endVertex();
				
				vec = vArray[j][k + 1];
				if (doLight)
				{
					ratio = (vec.y - lowestF) / difY;
					lightmap = (int) (200 * ratio);
				}
				buffer.pos(x + vec.x, y + vec.y, z + vec.z).color(r, g, b, a).tex(u, v2).lightmap(80, lightmap).endVertex();
			}
			v = vMin;
		}
		//End part that needs optimization
	}

 

 

Edited by SerpentDagger

Fancy 3D Graphing Calculator mod, with many different coordinate systems.

Lightweight 3D/2D position/vector transformations library, also with support for different coordinate systems.

Link to comment
Share on other sites

I realized that the FastTESR method of rendering (add vertices to a communal buffer, render buffer, throw away buffer and ask for more vertices) is, at least for my purposes, extremely wasteful. In my current situation, nothing changes location, so there's no need to discard the data after every frame, and reload it before the next.

As a result of this concept, I've mostly circumvented the system: no longer contributing to the buffer, and simply using the method as one that is called for each frame, for each tile entity.

Instead, when the graph is first loaded, I use the GlStateManager to generate a new call list (and delete the old one if it existed), and store the reference to this list within the TileEntity that I was given during FastTESR#renderTileEntityFast().

During subsequent frames, I then simply use GlStateManager#callList() to render the call list whose reference index is stored in the TileEntity.

It isn't a standard use of the FastTESR system, but it does produce a 7-fold performance improvement, or much more, depending on the circumstances.

 

The only problem this presents is that I'm no longer able to take advantage of the depth-sorting that's run on the vertices of the batched FastTESR buffer. This means that graphs are rendered "out of order," which is especially a problem for translucent ones.

I can fix the problem of individual graphs rendering out of order (e.g: a graph behind is rendered on top of one in front) by moving the actual rendering over to one of the rendering events, and then drawing the furthest graph first, etc, but I'm not sure if there's a way of fixing the order within a graph being off, while keeping to call lists (BufferBulder#sortVertexData() isn't applicable to call lists).

I'll keep looking, though.

  • Like 1

Fancy 3D Graphing Calculator mod, with many different coordinate systems.

Lightweight 3D/2D position/vector transformations library, also with support for different coordinate systems.

Link to comment
Share on other sites

On 11/28/2019 at 11:11 PM, SerpentDagger said:

but I'm not sure if there's a way of fixing the order within a graph being off, while keeping to call lists

I decided to switch away from call lists for translucent graphs, trying instead to use the Tessellator / BufferBuilder combo in order to have access to BufferBuilder#sortVertexData().

I wanted to be efficient, and store the BufferBuilder for later once I generated it, swapping it into a Tessellator during rendering. As it turns out, the Tessellator system is rigged such that there's no way to do that. The BufferBuilder is reset after every draw, and the BufferBuilder field of the Tessellator is private and final. In order to get around that, I created a new Tessellator with a public non-final BufferBuilder, and an extension of BufferBuilder who's reset() and finishDrawing() methods are altered to not reset and not finish.

This allows me to store and reuse my BufferBuilder objects, instead of throwing them away and rebuilding them constantly (a very expensive endeavor). I can now also use BufferBuilder#sortVertexData() on the buffer being swapped into the Tessellator, and my transparent graphs are properly rendered.

This all maintaining a similar 7-fold performance improvement over the standard system.

Fancy 3D Graphing Calculator mod, with many different coordinate systems.

Lightweight 3D/2D position/vector transformations library, also with support for different coordinate systems.

Link to comment
Share on other sites

On 11/28/2019 at 11:11 PM, SerpentDagger said:

by moving the actual rendering over to one of the rendering events, and then drawing the furthest graph first

Having implemented this, the transparency is proper in that respect too. I keep an ordered array of graphs, and render according to that order.

I decided to only sort vertices and graphs every 10 frames, to mitigate the performance impact. This works fairly well, but results in regular lag spikes when the graph is complicated enough to cause them. That seems better, however, than constant lag of the same magnitude.

 

I've noticed that the method of swapping BufferBuilders into the Tessellator and rendering with Tessellator#draw() is actually ~25% faster than using call lists, but that the call list only impacts performance significantly when you're actually looking at the graph, while the Tessellator method is constant, no matter where you look.

 

This is a bit of a dilemma, as I'm not sure which is less intrusive-- constant but lesser lag, or higher lag only when you're looking at the graph.

I'm sort of leaning towards the latter. After all, there's no point in impacting performance if you're not looking at the thing, right?

Thoughts on the above would be appreciated.

Fancy 3D Graphing Calculator mod, with many different coordinate systems.

Lightweight 3D/2D position/vector transformations library, also with support for different coordinate systems.

Link to comment
Share on other sites

  • 2 weeks later...

Having finished cleaning up the concepts above, I though't I'd take a moment

To summarize and clarify the answer to the question for future readers, while the memory is fresh.

 

If you have a TESR or FastTESR that doesn't change its vertices every frame, or that only translates the vertices every frame, then you have at least two options for improving performance. As I stated in previous posts, I actually managed a 7-10 times performance improvement.

 

The first option is to use call lists, while the second is to adapt the Tessellator slightly (from now on, the adapted Tessellator will be referenced as ATess). In both cases, you can reuse the Tessellator rendering code you've probably already got.

I should note that you can also take advantage of VBOs with the call list system, and gain a bit of extra performance, but only when the graphics card supports them. Because of that, you can't rely on VBOs alone. I never got them working well, and so I won't be discussing them further, but there's "example" code in RenderGlobal.

 

The benefit of using call lists over ATess is that you won't impact performance (or at least, you will impact it much less) when the player isn't looking at the object being rendered.

The benefit of using ATess is that it is about 25% faster than the call lists (in my experience) when you are looking at the object, and it allows you to sort vertices to weed out transparency issues.

 

Call lists are stored by OpenGL, and accessed through integer IDs. To create and use call lists, you can do the following:

- Check to see if the list has already been created, and destroy it if so, using GlAllocation#deleteDisplayLists().

- Get an instance of the Tessellator and its BufferBuilder.

- Allocate a rendering ID through GlAllocation#generateDisplayLists().

- Create a new list with the generated ID by using GlStateManager#glNewList().

- Begin drawing with your BufferBuilder.

- Add vertices to the buffer in the same manner as with the Tessellator.

- Call Tessellator#draw().

- Finish the list with GlStateManager#glEndList().

- You can now render this call list by using GlStateManager#callList(), and passing in the ID you stored earlier.

 

The adapted Tessellator (ATess) and adapted BufferBuilder (ABuff from now on) are only different in that you can swap the ABuff into and out from the ATess, and in that the ABuff doesn't reset when drawn. This allows you to not constantly reload all the data into the buffer. The code for these adaptations is shown below. To use them:

- Create a new ATess instance, and get its ABuff.

- I ended up storing the ABuffs within an array, which allowed me to mostly reuse the render ID system stated above in the call list section. This isn't necessary, though.

- Begin drawing with the ABuff.

- Add vertices as you would with the standard Tessellator and BufferBuilder.

- Call ATess#draw().

- Save the ABuff somewhere for later use. It now contains your rendering data.

- You can now render this ABuff by swapping it into an ATess and calling ATess#draw(). You can also sort the vertex data, since ABuff extends BufferBuilder.

 

ATess and ABuff classes:

Spoiler

package graphingcalculator3d.client;

import net.minecraft.client.renderer.WorldVertexBufferUploader;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;

/**
 * Same as a normal Tessellator, except it uses a ABuff, which is public and non-final.
 */
@SideOnly(Side.CLIENT)
public class ATess
{
	public ABuff buffer;
	private final WorldVertexBufferUploader vboUploader = new WorldVertexBufferUploader();
	/** The static instance of the Tessellator. */
	private static final ATess INSTANCE = new ATess(2097152);
	
	public static ATess getInstance()
	{
		return INSTANCE;
	}
	
	public ATess(int bufferSize)
	{
        this.buffer = new ABuff(bufferSize);
	}
	
	/**
	 * Draws the data set up in this tessellator and resets the state to prepare for new drawing.
	 */
	public void draw()
	{
		this.buffer.finishDrawing();
		this.vboUploader.draw(this.buffer);
	}
	
	public ABuff getBuffer()
	{
		return this.buffer;
	}
}

 

Spoiler

package graphingcalculator3d.client;

import net.minecraft.client.renderer.BufferBuilder;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;

/**
 * Same as a normal BufferBuilder, but doesn't reset, and is used in the ATess. 
 * @author SerpentDagger
 */
@SideOnly(Side.CLIENT)
public class ABuff extends BufferBuilder
{
	public ABuff(int bufferSizeIn)
	{
		super(bufferSizeIn);
	}
	
	@Override
	public void finishDrawing()
	{
		this.getByteBuffer().position(0);
		this.getByteBuffer().limit(this.getVertexCount() * this.getVertexFormat().getIntegerSize() * 4);
	}
	
	@Override
	public void reset()
	{
		//Your vile, resetting ways are no match for me! Bwahahaha! Suffer in your new-found efficiency!
	}
}

 

 

  • Thanks 1

Fancy 3D Graphing Calculator mod, with many different coordinate systems.

Lightweight 3D/2D position/vector transformations library, also with support for different coordinate systems.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share



×
×
  • Create New...

Important Information

By using this site, you agree to our Privacy Policy.