Jump to content

Recommended Posts

Posted (edited)

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.

Posted

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.

Posted
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.

Posted
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.

  • 2 weeks later...
Posted

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.

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
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
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.

Announcements



  • Recently Browsing

    • No registered users viewing this page.
  • Posts

    • Version 1.19 - Forge 41.0.63 I want to create a wolf entity that I can ride, so far it seems to be working, but the problem is that when I get on the wolf, I can’t control it. I then discovered that the issue is that the server doesn’t detect that I’m riding the wolf, so I’m struggling with synchronization. However, it seems to not be working properly. As I understand it, the server receives the packet but doesn’t register it correctly. I’m a bit new to Java, and I’ll try to provide all the relevant code and prints Thank you very much in advance No player is mounted, or the passenger is not a player. No player is mounted, or the passenger is not a player. No player is mounted, or the passenger is not a player. No player is mounted, or the passenger is not a player. No player is mounted, or the passenger is not a player. MountableWolfEntity package com.vals.valscraft.entity; import com.vals.valscraft.network.MountSyncPacket; import com.vals.valscraft.network.NetworkHandler; import net.minecraft.client.Minecraft; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.animal.Wolf; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.Entity; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.network.PacketDistributor; public class MountableWolfEntity extends Wolf { private boolean hasSaddle; private static final EntityDataAccessor<Byte> DATA_ID_FLAGS = SynchedEntityData.defineId(MountableWolfEntity.class, EntityDataSerializers.BYTE); public MountableWolfEntity(EntityType<? extends Wolf> type, Level level) { super(type, level); this.hasSaddle = false; } @Override protected void defineSynchedData() { super.defineSynchedData(); this.entityData.define(DATA_ID_FLAGS, (byte)0); } public static AttributeSupplier.Builder createAttributes() { return Wolf.createAttributes() .add(Attributes.MAX_HEALTH, 20.0) .add(Attributes.MOVEMENT_SPEED, 0.3); } @Override public InteractionResult mobInteract(Player player, InteractionHand hand) { ItemStack itemstack = player.getItemInHand(hand); if (itemstack.getItem() == Items.SADDLE && !this.hasSaddle()) { if (!player.isCreative()) { itemstack.shrink(1); } this.setSaddle(true); return InteractionResult.SUCCESS; } else if (!level.isClientSide && this.hasSaddle()) { player.startRiding(this); MountSyncPacket packet = new MountSyncPacket(true); // 'true' means the player is mounted NetworkHandler.CHANNEL.sendToServer(packet); // Ensure the server handles the packet return InteractionResult.SUCCESS; } return InteractionResult.PASS; } @Override public void travel(Vec3 travelVector) { if (this.isVehicle() && this.getControllingPassenger() instanceof Player) { System.out.println("The wolf has a passenger."); System.out.println("The passenger is a player."); Player player = (Player) this.getControllingPassenger(); // Ensure the player is the controller this.setYRot(player.getYRot()); this.yRotO = this.getYRot(); this.setXRot(player.getXRot() * 0.5F); this.setRot(this.getYRot(), this.getXRot()); this.yBodyRot = this.getYRot(); this.yHeadRot = this.yBodyRot; float forward = player.zza; float strafe = player.xxa; if (forward <= 0.0F) { forward *= 0.25F; } this.flyingSpeed = this.getSpeed() * 0.1F; this.setSpeed((float) this.getAttributeValue(Attributes.MOVEMENT_SPEED) * 1.5F); this.setDeltaMovement(new Vec3(strafe, travelVector.y, forward).scale(this.getSpeed())); this.calculateEntityAnimation(this, false); } else { // The wolf does not have a passenger or the passenger is not a player System.out.println("No player is mounted, or the passenger is not a player."); super.travel(travelVector); } } public boolean hasSaddle() { return this.hasSaddle; } public void setSaddle(boolean hasSaddle) { this.hasSaddle = hasSaddle; } @Override protected void dropEquipment() { super.dropEquipment(); if (this.hasSaddle()) { this.spawnAtLocation(Items.SADDLE); this.setSaddle(false); } } @SubscribeEvent public static void onServerTick(TickEvent.ServerTickEvent event) { if (event.phase == TickEvent.Phase.START) { MinecraftServer server = net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer(); if (server != null) { for (ServerPlayer player : server.getPlayerList().getPlayers()) { if (player.isPassenger() && player.getVehicle() instanceof MountableWolfEntity) { MountableWolfEntity wolf = (MountableWolfEntity) player.getVehicle(); System.out.println("Tick: " + player.getName().getString() + " is correctly mounted on " + wolf); } } } } } private boolean lastMountedState = false; @Override public void tick() { super.tick(); if (!this.level.isClientSide) { // Only on the server boolean isMounted = this.isVehicle() && this.getControllingPassenger() instanceof Player; // Only print if the state changed if (isMounted != lastMountedState) { if (isMounted) { Player player = (Player) this.getControllingPassenger(); // Verify the passenger is a player System.out.println("Server: Player " + player.getName().getString() + " is now mounted."); } else { System.out.println("Server: The wolf no longer has a passenger."); } lastMountedState = isMounted; } } } @Override public void addPassenger(Entity passenger) { super.addPassenger(passenger); if (passenger instanceof Player) { Player player = (Player) passenger; if (!this.level.isClientSide && player instanceof ServerPlayer) { // Send the packet to the server to indicate the player is mounted NetworkHandler.CHANNEL.send(PacketDistributor.PLAYER.with(() -> (ServerPlayer) player), new MountSyncPacket(true)); } } } @Override public void removePassenger(Entity passenger) { super.removePassenger(passenger); if (passenger instanceof Player) { Player player = (Player) passenger; if (!this.level.isClientSide && player instanceof ServerPlayer) { // Send the packet to the server to indicate the player is no longer mounted NetworkHandler.CHANNEL.send(PacketDistributor.PLAYER.with(() -> (ServerPlayer) player), new MountSyncPacket(false)); } } } @Override public boolean isControlledByLocalInstance() { Entity entity = this.getControllingPassenger(); return entity instanceof Player; } @Override public void positionRider(Entity passenger) { if (this.hasPassenger(passenger)) { double xOffset = Math.cos(Math.toRadians(this.getYRot() + 90)) * 0.4; double zOffset = Math.sin(Math.toRadians(this.getYRot() + 90)) * 0.4; passenger.setPos(this.getX() + xOffset, this.getY() + this.getPassengersRidingOffset() + passenger.getMyRidingOffset(), this.getZ() + zOffset); } } } MountSyncPacket package com.vals.valscraft.network; import com.vals.valscraft.entity.MountableWolfEntity; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.player.Player; import net.minecraftforge.network.NetworkEvent; import java.util.function.Supplier; public class MountSyncPacket { private final boolean isMounted; public MountSyncPacket(boolean isMounted) { this.isMounted = isMounted; } public void encode(FriendlyByteBuf buffer) { buffer.writeBoolean(isMounted); } public static MountSyncPacket decode(FriendlyByteBuf buffer) { return new MountSyncPacket(buffer.readBoolean()); } public void handle(NetworkEvent.Context context) { context.enqueueWork(() -> { ServerPlayer player = context.getSender(); // Get the player from the context if (player != null) { // Verifies if the player has dismounted if (!isMounted) { Entity vehicle = player.getVehicle(); if (vehicle instanceof MountableWolfEntity wolf) { // Logic to remove the player as a passenger wolf.removePassenger(player); System.out.println("Server: Player " + player.getName().getString() + " is no longer mounted."); } } } }); context.setPacketHandled(true); // Marks the packet as handled } } networkHandler package com.vals.valscraft.network; import com.vals.valscraft.valscraft; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.network.NetworkRegistry; import net.minecraftforge.network.simple.SimpleChannel; import net.minecraftforge.network.NetworkEvent; import java.util.function.Supplier; public class NetworkHandler { private static final String PROTOCOL_VERSION = "1"; public static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel( new ResourceLocation(valscraft.MODID, "main"), () -> PROTOCOL_VERSION, PROTOCOL_VERSION::equals, PROTOCOL_VERSION::equals ); public static void init() { int packetId = 0; // Register the mount synchronization packet CHANNEL.registerMessage( packetId++, MountSyncPacket.class, MountSyncPacket::encode, MountSyncPacket::decode, (msg, context) -> msg.handle(context.get()) // Get the context with context.get() ); } }  
    • Do you use features of inventory profiles next (ipnext) or is there a change without it?
    • Remove rubidium - you are already using embeddium, which is a fork of rubidium
  • Topics

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.