From 74dde9690516558e19519a5c8da06283f703b384 Mon Sep 17 00:00:00 2001 From: Glease <4586901+Glease@users.noreply.github.com> Date: Thu, 9 Sep 2021 03:38:46 +0800 Subject: [PATCH] redo holograms render to decouple it from particle system --- .../structurelib/ClientProxy.java | 321 ++++++++++++++---- .../entity/fx/EntityFXBlockHint.java | 146 -------- 2 files changed, 255 insertions(+), 212 deletions(-) delete mode 100644 src/main/java/com/gtnewhorizon/structurelib/entity/fx/EntityFXBlockHint.java diff --git a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java index e253cb9a..6597ca9a 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java +++ b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java @@ -1,51 +1,88 @@ package com.gtnewhorizon.structurelib; -import com.google.common.collect.BiMap; -import com.google.common.collect.HashBiMap; -import com.gtnewhorizon.structurelib.entity.fx.EntityFXBlockHint; +import java.util.*; + +import com.gtnewhorizon.structurelib.entity.fx.WeightlessParticleFX; import com.gtnewhorizon.structurelib.net.SetChannelDataMessage; +import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; +import cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent; +import cpw.mods.fml.common.gameevent.TickEvent.Phase; import net.minecraft.block.Block; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiNewChat; +import net.minecraft.client.particle.EntityFX; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.client.renderer.texture.TextureMap; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.util.ChatComponentText; import net.minecraft.util.IIcon; +import net.minecraft.util.Vec3; import net.minecraft.world.World; +import net.minecraftforge.client.event.RenderWorldLastEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.world.WorldEvent; +import org.lwjgl.opengl.GL11; + + +import static com.gtnewhorizon.structurelib.StructureLib.RANDOM; public class ClientProxy extends CommonProxy { - private static final short[] NO_TINT = {255, 255, 255, 0}; + private static final short[] RGBA_NO_TINT = {255, 255, 255, 255}; + private static final Map allHints = new HashMap<>(); + /** + * All batches of hints. + */ + private static final List allGroups = new ArrayList<>(); + public static int HOLOGRAM_LIFETIME = 200; + /** + * Current batch of hints. Belongs to the same logical multiblock. + */ + private static HintGroup currentHints; + /** + * Collection of all hints. Sorted from farthest (index 0) to nearest (index size - 1) + *

+ * 10k should be more than enough, if not a bit wasteful + */ + private static final List allHintsForRender = new ArrayList<>(10000); + /** + * If the diff to current player position is too great sort the allHints. + * Initial value is very far off below y=0 but not huge enough to make a NaN. + *

+ * We are using this supposedly immutable object as a mutable object here. + * So be aware. + */ + private static final Vec3 lastPlayerPos = Vec3.createVectorHelper(0, -1e30, 0); + /** + * if true rebuild the allHints list from all hintOwners (and sort it) + */ + private static boolean allHintsDirty = false; @Override public void hintParticleTinted(World w, int x, int y, int z, IIcon[] icons, short[] RGBa) { - EntityFXBlockHint hint = new EntityFXBlockHint(w, x, y, z, icons).withColorTint(RGBa); - Minecraft.getMinecraft().effectRenderer.addEffect(hint); ensureHinting(); + HintParticleInfo info = new HintParticleInfo(x, y, z, icons, RGBa); + + // check and remove colliding holograms if (ConfigurationHandler.INSTANCE.isRemoveCollidingHologram()) { - HintParticleInfo info = new HintParticleInfo(x, y, z, currentHints); - EntityFXBlockHint dupe = allHints.inverse().get(info); - if (dupe != null) { - List owner = allHints.get(dupe).owner; - // in case someone forget to call endHinting - if (owner != currentHints) { - hintOwners.remove(owner); - owner.forEach(EntityFXBlockHint::setDead); - owner.clear(); - } + HintGroup dupe = allHints.get(info); + if (dupe != null && dupe != currentHints) { + allGroups.remove(dupe); + removeGroup(dupe); } - allHints.forcePut(hint, info); } - currentHints.add(hint); + allHints.put(info, currentHints); + currentHints.getHints().add(info); + allHintsDirty = true; + + EntityFX particle = new WeightlessParticleFX(w, x + RANDOM.nextFloat() * 0.5F, y + RANDOM.nextFloat() * 0.5F, z + RANDOM.nextFloat() * 0.5F, 0, 0, 0); + particle.setRBGColorF(0, 0.6F * RANDOM.nextFloat(), 0.8f); + Minecraft.getMinecraft().effectRenderer.addEffect(particle); } @Override @@ -55,27 +92,35 @@ public class ClientProxy extends CommonProxy { @Override public void hintParticle(World w, int x, int y, int z, IIcon[] icons) { - hintParticleTinted(w, x, y, z, icons, NO_TINT); + hintParticleTinted(w, x, y, z, icons, RGBA_NO_TINT); } @Override public void hintParticle(World w, int x, int y, int z, Block block, int meta) { - hintParticleTinted(w, x, y, z, createIIconFromBlock(block, meta), NO_TINT); + hintParticleTinted(w, x, y, z, createIIconFromBlock(block, meta), RGBA_NO_TINT); } @Override public boolean updateHintParticleTint(EntityPlayer player, World w, int x, int y, int z, short[] rgBa) { if (player instanceof EntityPlayerMP) return super.updateHintParticleTint(player, w, x, y, z, rgBa); if (player != getCurrentPlayer()) return false; // how? - HintParticleInfo info = new HintParticleInfo(x, y, z, null); - EntityFXBlockHint existing = allHints.inverse().get(info); + HintParticleInfo info = new HintParticleInfo(x, y, z, null, null); + HintGroup existing = allHints.get(info); if (existing != null) { - existing.withColorTint(rgBa); + for (HintParticleInfo hint : existing.getHints()) { + if (hint.equals(info)) + hint.tint = rgBa; + } return true; } return false; } + static void removeGroup(HintGroup group) { + for (HintParticleInfo hintParticleInfo : group.getHints()) allHints.remove(hintParticleInfo); + allHintsDirty = true; + } + private static IIcon[] createIIconFromBlock(Block block, int meta) { IIcon[] ret = new IIcon[6]; for (int i = 0; i < 6; i++) { @@ -102,49 +147,34 @@ public class ClientProxy extends CommonProxy { return player == Minecraft.getMinecraft().thePlayer; } - private static final BiMap allHints = HashBiMap.create(); - private static final List> hintOwners = new ArrayList<>(); - private static List currentHints; - - public static void onHintDead(EntityFXBlockHint fx) { - if (ConfigurationHandler.INSTANCE.isRemoveCollidingHologram()) allHints.remove(fx); - for (Iterator> iterator = hintOwners.iterator(); iterator.hasNext(); ) { - List list = iterator.next(); - if (list.remove(fx)) { - if (list.isEmpty()) iterator.remove(); - break; - } - } - } - @Override public void startHinting(World w) { - if (!w.isRemote) return; - if (currentHints != null) hintOwners.add(currentHints); - currentHints = new LinkedList<>(); + if (!w.isRemote) + return; + if (currentHints != null) + endHinting(w); + currentHints = new HintGroup(); } private void ensureHinting() { - if (currentHints == null) currentHints = new LinkedList<>(); + if (currentHints == null) + currentHints = new HintGroup(); } @Override public void endHinting(World w) { - if (!w.isRemote) return; - while (!hintOwners.isEmpty() && hintOwners.size() >= ConfigurationHandler.INSTANCE.getMaxCoexistingHologram()) { - List list = hintOwners.remove(0); - list.forEach(EntityFXBlockHint::setDead); - list.clear(); + if (!w.isRemote || currentHints == null) + return; + while (!allGroups.isEmpty() && allGroups.size() >= ConfigurationHandler.INSTANCE.getMaxCoexistingHologram()) { + allGroups.remove(0); } - if (!currentHints.isEmpty()) hintOwners.add(currentHints); + if (!currentHints.getHints().isEmpty()) + allGroups.add(currentHints); + // we use the player existence time here as some worlds don't really advance the time ticker + currentHints.setCreationTime(getCurrentPlayer().ticksExisted); currentHints = null; } - @Override - public void preInit(FMLPreInitializationEvent e) { - MinecraftForge.EVENT_BUS.register(new ForgeEventHandler()); - } - @Override public long getOverworldTime() { // there is no overworld, better just hope current world time is ok... @@ -156,27 +186,57 @@ public class ClientProxy extends CommonProxy { StructureLib.net.sendToServer(new SetChannelDataMessage(trigger)); } + @Override + public void preInit(FMLPreInitializationEvent e) { + FMLCommonHandler.instance().bus().register(new FMLEventHandler()); + MinecraftForge.EVENT_BUS.register(new ForgeEventHandler()); + } + + private static class HintGroup { + private final List hints = new LinkedList<>(); + private int creationTime = -1; + + public List getHints() { + return hints; + } + + public void setCreationTime(int creationTime) { + this.creationTime = creationTime; + } + + public int getCreationTime() { + return creationTime; + } + } + private static class HintParticleInfo { private final int x, y, z; - private final List owner; - public HintParticleInfo(int x, int y, int z, List owner) { + private final double X, Y, Z; + private final IIcon[] icons; + private short[] tint; + + private final long creationTime = System.currentTimeMillis(); // use tick time instead maybe + + public HintParticleInfo(int x, int y, int z, IIcon[] icons, short[] tint) { this.x = x; this.y = y; this.z = z; - this.owner = owner; + X = x + 0.25; + Y = y + 0.5; + Z = z + 0.25; + this.icons = icons; + this.tint = tint; } @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof HintParticleInfo)) return false; HintParticleInfo that = (HintParticleInfo) o; - if (x != that.x) return false; - if (y != that.y) return false; - return z == that.z; + return x == that.x && y == that.y && z == that.z; } @Override @@ -186,15 +246,144 @@ public class ClientProxy extends CommonProxy { result = 31 * result + z; return result; } + + public void draw(Tessellator tes) { + double size = 0.5; + + tes.setColorRGBA((int) (tint[0] * .9F), (int) (tint[1] * .95F), (int) (tint[2] * 1F), 160); + + for (int i = 0; i < 6; i++) { + if (icons[i] == null) continue; + + double u = icons[i].getMinU(); + double U = icons[i].getMaxU(); + double v = icons[i].getMinV(); + double V = icons[i].getMaxV(); + switch (i) {//{DOWN, UP, NORTH, SOUTH, WEST, EAST} + case 0: + tes.addVertexWithUV(X, Y, Z + size, u, V); + tes.addVertexWithUV(X, Y, Z, u, v); + tes.addVertexWithUV(X + size, Y, Z, U, v); + tes.addVertexWithUV(X + size, Y, Z + size, U, V); + break; + case 1: + tes.addVertexWithUV(X, Y + size, Z, u, v); + tes.addVertexWithUV(X, Y + size, Z + size, u, V); + tes.addVertexWithUV(X + size, Y + size, Z + size, U, V); + tes.addVertexWithUV(X + size, Y + size, Z, U, v); + break; + case 2: + tes.addVertexWithUV(X, Y, Z, U, V); + tes.addVertexWithUV(X, Y + size, Z, U, v); + tes.addVertexWithUV(X + size, Y + size, Z, u, v); + tes.addVertexWithUV(X + size, Y, Z, u, V); + break; + case 3: + tes.addVertexWithUV(X + size, Y, Z + size, U, V); + tes.addVertexWithUV(X + size, Y + size, Z + size, U, v); + tes.addVertexWithUV(X, Y + size, Z + size, u, v); + tes.addVertexWithUV(X, Y, Z + size, u, V); + break; + case 4: + tes.addVertexWithUV(X, Y, Z + size, U, V); + tes.addVertexWithUV(X, Y + size, Z + size, U, v); + tes.addVertexWithUV(X, Y + size, Z, u, v); + tes.addVertexWithUV(X, Y, Z, u, V); + break; + case 5: + tes.addVertexWithUV(X + size, Y, Z, U, V); + tes.addVertexWithUV(X + size, Y + size, Z, U, v); + tes.addVertexWithUV(X + size, Y + size, Z + size, u, v); + tes.addVertexWithUV(X + size, Y, Z + size, u, V); + break; + } + } + } + + public long getCreationTime() { + return creationTime; + } + + public double getSquareDistanceTo(Vec3 point) { + return point.squareDistanceTo(X, Y, Z); + } + } + + public static class FMLEventHandler { + private void resetPlayerLocation() { + lastPlayerPos.xCoord = Minecraft.getMinecraft().thePlayer.posX; + lastPlayerPos.yCoord = Minecraft.getMinecraft().thePlayer.posY; + lastPlayerPos.zCoord = Minecraft.getMinecraft().thePlayer.posZ; + } + + @SubscribeEvent + public void onClientTick(ClientTickEvent e) { + if (e.phase == Phase.END && Minecraft.getMinecraft().thePlayer != null) { + Vec3 playerPos = Minecraft.getMinecraft().thePlayer.getPosition(1); + boolean sortRequired = false; + // The allGroups is implicitly sorted by creationTime + // here we exploit this ordering to reduce iteration size + int deadline = Minecraft.getMinecraft().thePlayer.ticksExisted - HOLOGRAM_LIFETIME; + int i; + for (i = 0; i < allGroups.size(); i++) { + if (allGroups.get(i).getCreationTime() > deadline) { + break; + } + } + if (i != 0) { + sortRequired = true; + // clear this block and go no further + List toRemove = allGroups.subList(0, i); + toRemove.forEach(ClientProxy::removeGroup); + toRemove.clear(); + } + if (allHintsDirty) { + allHintsForRender.clear(); + // we need greatly efficient sequential access and mildly efficient sorting + // rebuild is sort of rare, and it's expected to lag anyway + // so no min-heap here + for (HintGroup c : allGroups) allHintsForRender.addAll(c.getHints()); + sortRequired = true; + } + if (sortRequired || playerPos.squareDistanceTo(lastPlayerPos) > 1e-4) { + // only redo sort if player moved some distance + // default is 0.01 block + // if there was a full rebuild, go sort it as well + allHintsForRender.sort(Comparator.comparingDouble(info -> info.getSquareDistanceTo(playerPos))); + resetPlayerLocation(); + } + } + } } public static class ForgeEventHandler { @SubscribeEvent public void onWorldLoad(WorldEvent.Load e) { if (e.world.isRemote) { - allHints.clear(); - hintOwners.clear(); + allHintsForRender.clear(); + allGroups.clear(); + lastPlayerPos.yCoord = -1e30; } } + + @SubscribeEvent + public void onRenderWorldLast(RenderWorldLastEvent e) { + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);// TODO figure out original states + GL11.glDisable(GL11.GL_CULL_FACE); // we need the back facing rendered because the thing is transparent + GL11.glEnable(GL11.GL_BLEND); // enable blend so it is transparent + GL11.glBlendFunc(GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_SRC_ALPHA); + GL11.glDisable(GL11.GL_DEPTH_TEST); // to draw through these stuff +// GL11.glDepthMask(false); + Minecraft.getMinecraft().renderEngine.bindTexture(TextureMap.locationBlocksTexture); + Tessellator tes = Tessellator.instance; + tes.startDrawingQuads(); + tes.setTranslation(-RenderManager.renderPosX, -RenderManager.renderPosY, -RenderManager.renderPosZ); + for (HintParticleInfo hint : allHintsForRender) { + hint.draw(tes); + } + tes.setTranslation(0, 0, 0); + tes.draw(); + GL11.glPopAttrib(); + } } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/entity/fx/EntityFXBlockHint.java b/src/main/java/com/gtnewhorizon/structurelib/entity/fx/EntityFXBlockHint.java deleted file mode 100644 index 315b4771..00000000 --- a/src/main/java/com/gtnewhorizon/structurelib/entity/fx/EntityFXBlockHint.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.gtnewhorizon.structurelib.entity.fx; - -import com.gtnewhorizon.structurelib.ClientProxy; -import com.gtnewhorizon.structurelib.StructureLib; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import net.minecraft.block.Block; -import net.minecraft.client.particle.EntityFX; -import net.minecraft.client.renderer.Tessellator; -import net.minecraft.init.Blocks; -import net.minecraft.util.IIcon; -import net.minecraft.world.World; -import org.lwjgl.opengl.GL11; - -@SideOnly(Side.CLIENT) -public class EntityFXBlockHint extends EntityFX { - private IIcon[] icons; - private short[] mRGBa = {255, 255, 255, 0}; - - public EntityFXBlockHint(World world) { - this(world, 0, 0, 0, Blocks.stone, 0); - } - - /** - * @param world - * @param x - * @param y - * @param z - * @param icons DOWN, UP, NORTH, SOUTH, WEST, EAST - */ - public EntityFXBlockHint(World world, int x, int y, int z, IIcon[] icons) { - super(world, x + .25, y + .5, z + .25); - particleGravity = 0; - prevPosX = posX; - prevPosY = posY; - prevPosZ = posZ; - noClip = true; - particleMaxAge = 2000 + StructureLib.RANDOM.nextInt(200); - this.icons = icons; - } - - public EntityFXBlockHint(World world, int x, int y, int z, Block block, int meta) { - super(world, x + .25, y + .5, z + .25); - particleGravity = 0; - prevPosX = posX; - prevPosY = posY; - prevPosZ = posZ; - noClip = true; - particleMaxAge = 2000 + StructureLib.RANDOM.nextInt(200); - icons = new IIcon[6]; - for (int i = 0; i < 6; i++) { - icons[i] = block.getIcon(i, meta); - } - } - - public EntityFXBlockHint withColorTint(short[] coloure) { - this.mRGBa = coloure; - return this; - } - - @Override - public void renderParticle( - Tessellator tes, - float subTickTime, - float p_70539_3_, - float p_70539_4_, - float p_70539_5_, - float p_70539_6_, - float p_70539_7_) { - float size = .5f; - float X = (float) (prevPosX + (posX - prevPosX) * (double) subTickTime - EntityFX.interpPosX); - float Y = (float) (prevPosY + (posY - prevPosY) * (double) subTickTime - EntityFX.interpPosY) - size / 2; - float Z = (float) (prevPosZ + (posZ - prevPosZ) * (double) subTickTime - EntityFX.interpPosZ); - GL11.glDisable(GL11.GL_CULL_FACE); - GL11.glDepthMask(false); - tes.setColorRGBA((int) (mRGBa[0] * .9F), (int) (mRGBa[1] * .95F), (int) (mRGBa[2] * 1F), 192); - - // var8, var9 - X U - // var 10, var 11 - Y V - for (int i = 0; i < 6; i++) { - if (icons[i] == null) { - continue; - } - double u = icons[i].getMinU(); - double U = icons[i].getMaxU(); - double v = icons[i].getMinV(); - double V = icons[i].getMaxV(); - switch (i) { // {DOWN, UP, NORTH, SOUTH, WEST, EAST} - case 0: - tes.addVertexWithUV(X, Y, Z + size, u, V); - tes.addVertexWithUV(X, Y, Z, u, v); - tes.addVertexWithUV(X + size, Y, Z, U, v); - tes.addVertexWithUV(X + size, Y, Z + size, U, V); - break; - case 1: - tes.addVertexWithUV(X, Y + size, Z, u, v); - tes.addVertexWithUV(X, Y + size, Z + size, u, V); - tes.addVertexWithUV(X + size, Y + size, Z + size, U, V); - tes.addVertexWithUV(X + size, Y + size, Z, U, v); - break; - case 2: - tes.addVertexWithUV(X, Y, Z, U, V); - tes.addVertexWithUV(X, Y + size, Z, U, v); - tes.addVertexWithUV(X + size, Y + size, Z, u, v); - tes.addVertexWithUV(X + size, Y, Z, u, V); - break; - case 3: - tes.addVertexWithUV(X + size, Y, Z + size, U, V); - tes.addVertexWithUV(X + size, Y + size, Z + size, U, v); - tes.addVertexWithUV(X, Y + size, Z + size, u, v); - tes.addVertexWithUV(X, Y, Z + size, u, V); - break; - case 4: - tes.addVertexWithUV(X, Y, Z + size, U, V); - tes.addVertexWithUV(X, Y + size, Z + size, U, v); - tes.addVertexWithUV(X, Y + size, Z, u, v); - tes.addVertexWithUV(X, Y, Z, u, V); - break; - case 5: - tes.addVertexWithUV(X + size, Y, Z, U, V); - tes.addVertexWithUV(X + size, Y + size, Z, U, v); - tes.addVertexWithUV(X + size, Y + size, Z + size, u, v); - tes.addVertexWithUV(X + size, Y, Z + size, u, V); - break; - } - } - GL11.glEnable(GL11.GL_CULL_FACE); - GL11.glDepthMask(true); - } - - @Override - public int getFXLayer() { - return 1; - } - - @Override - public boolean shouldRenderInPass(int pass) { - return pass == 2; - } - - @Override - public void setDead() { - super.setDead(); - ClientProxy.onHintDead(this); - } -}