diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index c3eca367a..27faa43f2 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -67,6 +67,12 @@ opencomputers { # down with the actual scale of the hologram. hologramRenderDistance: 64 + # The distance at which to start fading out the hologram (as with + # hologramRenderDistance). This is purely cosmetic, to avoid image + # disappearing instantly when moving too far away from a projector. + # It does not affect performance. Holograms are transparent anyway. + hologramFadeStartDistance: 48 + # This controls how often holograms 'flicker'. This is the chance that it # flickers for *each frame*, meaning if you're running at high FPS you # may want to lower this a bit, to avoid it flickering too much. diff --git a/src/main/scala/li/cil/oc/Settings.scala b/src/main/scala/li/cil/oc/Settings.scala index 87dd2c8c7..c15777301 100644 --- a/src/main/scala/li/cil/oc/Settings.scala +++ b/src/main/scala/li/cil/oc/Settings.scala @@ -31,6 +31,7 @@ class Settings(config: Config) { val robotLabels = config.getBoolean("client.robotLabels") val soundVolume = config.getDouble("client.soundVolume").toFloat max 0 min 2 val fontCharScale = config.getDouble("client.fontCharScale") max 0.5 min 2 + val hologramFadeStartDistance = config.getDouble("client.hologramFadeStartDistance") max 0 val hologramRenderDistance = config.getDouble("client.hologramRenderDistance") max 0 val hologramFlickerFrequency = config.getDouble("client.hologramFlickerFrequency") max 0 diff --git a/src/main/scala/li/cil/oc/client/PacketHandler.scala b/src/main/scala/li/cil/oc/client/PacketHandler.scala index 109f6d1da..1ec0604bf 100644 --- a/src/main/scala/li/cil/oc/client/PacketHandler.scala +++ b/src/main/scala/li/cil/oc/client/PacketHandler.scala @@ -153,7 +153,6 @@ class PacketHandler extends CommonPacketHandler { p.readTileEntity[Hologram]() match { case Some(t) => t.scale = p.readDouble() - t.dirty = true case _ => // Invalid packet. } diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRenderer.scala index 039f12cf8..e63bd8d34 100644 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRenderer.scala @@ -8,18 +8,18 @@ import li.cil.oc.client.Textures import li.cil.oc.common.tileentity.Hologram import li.cil.oc.util.RenderState import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.client.renderer.{GLAllocation, Tessellator} import net.minecraft.tileentity.TileEntity -import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.{GL15, GL11} import scala.util.Random +import org.lwjgl.BufferUtils import li.cil.oc.Settings object HologramRenderer extends TileEntitySpecialRenderer with Callable[Int] with RemovalListener[TileEntity, Int] with ITickHandler { val random = new Random() - /** We cache the display lists for the projectors we render for performance. */ + /** We cache the VBOs for the projectors we render for performance. */ val cache = com.google.common.cache.CacheBuilder.newBuilder(). - expireAfterAccess(2, TimeUnit.SECONDS). + expireAfterAccess(10, TimeUnit.SECONDS). removalListener(this). asInstanceOf[CacheBuilder[Hologram, Int]]. build[Hologram, Int]() @@ -37,6 +37,11 @@ object HologramRenderer extends TileEntitySpecialRenderer with Callable[Int] wit RenderState.makeItBlend() GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE) + val playerDistSq = x*x + y*y + z*z + val maxDistSq = hologram.getMaxRenderDistanceSquared + val fadeDistSq = hologram.getFadeStartDistanceSquared + RenderState.setBlendAlpha(0.75f * (if (playerDistSq > fadeDistSq) math.max(0, 1 - ((playerDistSq - fadeDistSq) / (maxDistSq - fadeDistSq)).toFloat) else 1)) + GL11.glPushMatrix() GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) GL11.glScaled(1.001, 1.001, 1.001) // Avoid z-fighting with other blocks. @@ -48,16 +53,21 @@ object HologramRenderer extends TileEntitySpecialRenderer with Callable[Int] wit GL11.glTranslated(random.nextGaussian() * 0.01, random.nextGaussian() * 0.01, random.nextGaussian() * 0.01) } + // After the below scaling, hologram is drawn inside a [0..48]x[0..32]x[0..48] box + GL11.glScaled(hologram.scale / 16f, hologram.scale / 16f, hologram.scale / 16f) + // We do two passes here to avoid weird transparency effects: in the first // pass we find the front-most fragment, in the second we actually draw it. // TODO proper transparency shader? depth peeling e.g. + // evg-zhabotinsky: I'd rather not do it. Anyway it won't work for multiple holograms. + // Also I commented out the first pass to see what it will look like and I prefer it the way it is now. GL11.glColorMask(false, false, false, false) GL11.glDepthMask(true) - val list = cache.get(hologram, this) - compileOrDraw(list) + val privateBuf = cache.get(hologram, this) + compileOrDraw(privateBuf) GL11.glColorMask(true, true, true, true) GL11.glDepthFunc(GL11.GL_EQUAL) - compileOrDraw(list) + compileOrDraw(privateBuf) GL11.glPopMatrix() GL11.glPopAttrib() @@ -65,33 +75,30 @@ object HologramRenderer extends TileEntitySpecialRenderer with Callable[Int] wit RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") } - def compileOrDraw(list: Int) = if (hologram.dirty) { - val doCompile = !RenderState.compilingDisplayList - if (doCompile) { - hologram.dirty = false - GL11.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE) + val compileOrDraw = { + // WARNING works only if all the holograms have the same dimensions (in voxels) + var commonBuffer = 0 // Common for all holograms (a-la static variable) + (privateBuf: Int) => { + // Save current state (don't forget to restore) + GL11.glPushClientAttrib(GL11.GL_ALL_CLIENT_ATTRIB_BITS) + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) + + if (commonBuffer == 0) { // First run only + commonBuffer = GL15.glGenBuffers() + var tmpBuf = BufferUtils.createFloatBuffer(hologram.width * hologram.width * hologram.height * 24 * (2 + 3 + 3)) + def newVert = (x: Int, y: Int, z: Int, u: Int, v: Int, nx: Int, ny: Int, nz: Int) => { + tmpBuf.put(u) + tmpBuf.put(v) + tmpBuf.put(nx) + tmpBuf.put(ny) + tmpBuf.put(nz) + tmpBuf.put(x) + tmpBuf.put(y) + tmpBuf.put(z) } - - def value(hx: Int, hy: Int, hz: Int) = if (hx >= 0 && hy >= 0 && hz >= 0 && hx < hologram.width && hy < hologram.height && hz < hologram.width) hologram.getColor(hx, hy, hz) else 0 - - def isSolid(hx: Int, hy: Int, hz: Int) = value(hx, hy, hz) != 0 - - bindTexture(Textures.blockHologram) - val t = Tessellator.instance - t.startDrawingQuads() - - // TODO merge quads for better rendering performance - val s = 1f / 16f * hologram.scale - for (hx <- 0 until hologram.width) { - val wx = hx * s - for (hz <- 0 until hologram.width) { - val wz = hz * s - for (hy <- 0 until hologram.height) { - val wy = hy * s - - if (isSolid(hx, hy, hz)) { - t.setColorRGBA_I(hologram.colors(value(hx, hy, hz) - 1), 192) - + for (x <- 0 until hologram.width) { + for (z <- 0 until hologram.width) { + for (y <- 0 until hologram.height) { /* 0---1 | N | @@ -103,82 +110,185 @@ object HologramRenderer extends TileEntitySpecialRenderer with Callable[Int] wit */ // South - if (!isSolid(hx, hy, hz + 1)) { - t.setNormal(0, 0, 1) - t.addVertexWithUV(wx + s, wy + s, wz + s, 0, 0) // 5 - t.addVertexWithUV(wx + 0, wy + s, wz + s, 1, 0) // 4 - t.addVertexWithUV(wx + 0, wy + 0, wz + s, 1, 1) // 7 - t.addVertexWithUV(wx + s, wy + 0, wz + s, 0, 1) // 6 - } - // North - if (!isSolid(hx, hy, hz - 1)) { - t.setNormal(0, 0, -1) - t.addVertexWithUV(wx + s, wy + 0, wz + 0, 0, 0) // 3 - t.addVertexWithUV(wx + 0, wy + 0, wz + 0, 1, 0) // 2 - t.addVertexWithUV(wx + 0, wy + s, wz + 0, 1, 1) // 1 - t.addVertexWithUV(wx + s, wy + s, wz + 0, 0, 1) // 0 - } + newVert(x + 1, y + 1, z + 1, 0, 0, 0, 0, 1) // 5 + newVert(x + 0, y + 1, z + 1, 1, 0, 0, 0, 1) // 4 + newVert(x + 0, y + 0, z + 1, 1, 1, 0, 0, 1) // 7 + newVert(x + 1, y + 0, z + 1, 0, 1, 0, 0, 1) // 6 + // North + newVert(x + 1, y + 0, z + 0, 0, 0, 0, 0, -1) // 3 + newVert(x + 0, y + 0, z + 0, 1, 0, 0, 0, -1) // 2 + newVert(x + 0, y + 1, z + 0, 1, 1, 0, 0, -1) // 1 + newVert(x + 1, y + 1, z + 0, 0, 1, 0, 0, -1) // 0 - // East - if (!isSolid(hx + 1, hy, hz)) { - t.setNormal(1, 0, 0) - t.addVertexWithUV(wx + s, wy + s, wz + s, 1, 0) // 5 - t.addVertexWithUV(wx + s, wy + 0, wz + s, 1, 1) // 6 - t.addVertexWithUV(wx + s, wy + 0, wz + 0, 0, 1) // 3 - t.addVertexWithUV(wx + s, wy + s, wz + 0, 0, 0) // 0 - } - // West - if (!isSolid(hx - 1, hy, hz)) { - t.setNormal(-1, 0, 0) - t.addVertexWithUV(wx + 0, wy + 0, wz + s, 1, 0) // 7 - t.addVertexWithUV(wx + 0, wy + s, wz + s, 1, 1) // 4 - t.addVertexWithUV(wx + 0, wy + s, wz + 0, 0, 1) // 1 - t.addVertexWithUV(wx + 0, wy + 0, wz + 0, 0, 0) // 2 - } + // East + newVert(x + 1, y + 1, z + 1, 1, 0, 1, 0, 0) // 5 + newVert(x + 1, y + 0, z + 1, 1, 1, 1, 0, 0) // 6 + newVert(x + 1, y + 0, z + 0, 0, 1, 1, 0, 0) // 3 + newVert(x + 1, y + 1, z + 0, 0, 0, 1, 0, 0) // 0 + // West + newVert(x + 0, y + 0, z + 1, 1, 0, -1, 0, 0) // 7 + newVert(x + 0, y + 1, z + 1, 1, 1, -1, 0, 0) // 4 + newVert(x + 0, y + 1, z + 0, 0, 1, -1, 0, 0) // 1 + newVert(x + 0, y + 0, z + 0, 0, 0, -1, 0, 0) // 2 - // Up - if (!isSolid(hx, hy + 1, hz)) { - t.setNormal(0, 1, 0) - t.addVertexWithUV(wx + s, wy + s, wz + 0, 0, 0) // 0 - t.addVertexWithUV(wx + 0, wy + s, wz + 0, 1, 0) // 1 - t.addVertexWithUV(wx + 0, wy + s, wz + s, 1, 1) // 4 - t.addVertexWithUV(wx + s, wy + s, wz + s, 0, 1) // 5 - } - // Down - if (!isSolid(hx, hy - 1, hz)) { - t.setNormal(0, -1, 0) - t.addVertexWithUV(wx + s, wy + 0, wz + s, 0, 0) // 6 - t.addVertexWithUV(wx + 0, wy + 0, wz + s, 1, 0) // 7 - t.addVertexWithUV(wx + 0, wy + 0, wz + 0, 1, 1) // 2 - t.addVertexWithUV(wx + s, wy + 0, wz + 0, 0, 1) // 3 + // Up + newVert(x + 1, y + 1, z + 0, 0, 0, 0, 1, 0) // 0 + newVert(x + 0, y + 1, z + 0, 1, 0, 0, 1, 0) // 1 + newVert(x + 0, y + 1, z + 1, 1, 1, 0, 1, 0) // 4 + newVert(x + 1, y + 1, z + 1, 0, 1, 0, 1, 0) // 5 + // Down + newVert(x + 1, y + 0, z + 1, 0, 0, 0, -1, 0) // 6 + newVert(x + 0, y + 0, z + 1, 1, 0, 0, -1, 0) // 7 + newVert(x + 0, y + 0, z + 0, 1, 1, 0, -1, 0) // 2 + newVert(x + 1, y + 0, z + 0, 0, 1, 0, -1, 0) // 3 } } } + tmpBuf.rewind() // Important! + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, commonBuffer) + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, tmpBuf, GL15.GL_STATIC_DRAW) } + + if (hologram.dirty) { // Refresh hologram + def value(hx: Int, hy: Int, hz: Int) = if (hx >= 0 && hy >= 0 && hz >= 0 && hx < hologram.width && hy < hologram.height && hz < hologram.width) hologram.getColor(hx, hy, hz) else 0 + + def isSolid(hx: Int, hy: Int, hz: Int) = value(hx, hy, hz) != 0 + + var tmpBuf = BufferUtils.createIntBuffer(hologram.width * hologram.width * hologram.height * 24 * 2) + // Copy color information, identify which quads to render and prepare data for glDrawElements + hologram.visibleQuads = 0 + var c = 0 + tmpBuf.position(hologram.width * hologram.width * hologram.height * 24) + for (hx <- 0 until hologram.width) { + for (hz <- 0 until hologram.width) { + for (hy <- 0 until hologram.height) { + if (isSolid(hx, hy, hz)) { + val v: Int = hologram.colors(value(hx, hy, hz) - 1) + // South + if (!isSolid(hx, hy, hz + 1)) { + tmpBuf.put(c) + tmpBuf.put(c + 1) + tmpBuf.put(c + 2) + tmpBuf.put(c + 3) + tmpBuf.put(c, v) + tmpBuf.put(c + 1, v) + tmpBuf.put(c + 2, v) + tmpBuf.put(c + 3, v) + hologram.visibleQuads += 1 + } + c += 4 + // North + if (!isSolid(hx, hy, hz - 1)) { + tmpBuf.put(c) + tmpBuf.put(c + 1) + tmpBuf.put(c + 2) + tmpBuf.put(c + 3) + tmpBuf.put(c, v) + tmpBuf.put(c + 1, v) + tmpBuf.put(c + 2, v) + tmpBuf.put(c + 3, v) + hologram.visibleQuads += 1 + } + c += 4 + + // East + if (!isSolid(hx + 1, hy, hz)) { + tmpBuf.put(c) + tmpBuf.put(c + 1) + tmpBuf.put(c + 2) + tmpBuf.put(c + 3) + tmpBuf.put(c, v) + tmpBuf.put(c + 1, v) + tmpBuf.put(c + 2, v) + tmpBuf.put(c + 3, v) + hologram.visibleQuads += 1 + } + c += 4 + // West + if (!isSolid(hx - 1, hy, hz)) { + tmpBuf.put(c) + tmpBuf.put(c + 1) + tmpBuf.put(c + 2) + tmpBuf.put(c + 3) + tmpBuf.put(c, v) + tmpBuf.put(c + 1, v) + tmpBuf.put(c + 2, v) + tmpBuf.put(c + 3, v) + hologram.visibleQuads += 1 + } + c += 4 + + // Up + if (!isSolid(hx, hy + 1, hz)) { + tmpBuf.put(c) + tmpBuf.put(c + 1) + tmpBuf.put(c + 2) + tmpBuf.put(c + 3) + tmpBuf.put(c, v) + tmpBuf.put(c + 1, v) + tmpBuf.put(c + 2, v) + tmpBuf.put(c + 3, v) + hologram.visibleQuads += 1 + } + c += 4 + // Down + if (!isSolid(hx, hy - 1, hz)) { + tmpBuf.put(c) + tmpBuf.put(c + 1) + tmpBuf.put(c + 2) + tmpBuf.put(c + 3) + tmpBuf.put(c, v) + tmpBuf.put(c + 1, v) + tmpBuf.put(c + 2, v) + tmpBuf.put(c + 3, v) + hologram.visibleQuads += 1 + } + c += 4 + } else c += 24 + } + } + } + tmpBuf.rewind() // Important! + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, privateBuf) + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, tmpBuf, GL15.GL_STATIC_DRAW) + + hologram.dirty = false } - t.draw() + GL11.glEnable(GL11.GL_NORMALIZE) // Normalize normals!!! (Yes, glScale scales them too!) + GL11.glEnable(GL11.GL_CULL_FACE) + GL11.glCullFace(GL11.GL_BACK) // Because fragment processing started to slow things down + bindTexture(Textures.blockHologram) + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, commonBuffer) + GL11.glEnableClientState(GL11.GL_VERTEX_ARRAY) + GL11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY) + GL11.glEnableClientState(GL11.GL_NORMAL_ARRAY) + GL11.glInterleavedArrays(GL11.GL_T2F_N3F_V3F, 0, 0) + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, privateBuf) + GL11.glEnableClientState(GL11.GL_COLOR_ARRAY) + GL11.glColorPointer(3, GL11.GL_UNSIGNED_BYTE, 4, 0) + GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, privateBuf) - if (doCompile) { - GL11.glEndList() - } + GL11.glDrawElements(GL11.GL_QUADS, hologram.visibleQuads * 4, GL11.GL_UNSIGNED_INT, hologram.width * hologram.width * hologram.height * 24 * 4) - true + // Restore original state + GL11.glPopAttrib() + GL11.glPopClientAttrib() + } } - else GL11.glCallList(list) // ----------------------------------------------------------------------- // // Cache // ----------------------------------------------------------------------- // def call = { - val list = GLAllocation.generateDisplayLists(1) + val privateBuf = GL15.glGenBuffers() hologram.dirty = true // Force compilation. - list + privateBuf } def onRemoval(e: RemovalNotification[TileEntity, Int]) { - GLAllocation.deleteDisplayLists(e.getValue) + GL15.glDeleteBuffers(e.getValue) } // ----------------------------------------------------------------------- // diff --git a/src/main/scala/li/cil/oc/common/tileentity/Hologram.scala b/src/main/scala/li/cil/oc/common/tileentity/Hologram.scala index 38dda0071..d25514b08 100644 --- a/src/main/scala/li/cil/oc/common/tileentity/Hologram.scala +++ b/src/main/scala/li/cil/oc/common/tileentity/Hologram.scala @@ -34,6 +34,9 @@ class Hologram(var tier: Int) extends traits.Environment with SidedEnvironment w // Whether we need to send an update packet/recompile our display list. var dirty = false + // Store it here for convenience + var visibleQuads = 0 + // Interval of dirty columns. var dirtyFromX = Int.MaxValue var dirtyUntilX = -1 @@ -45,7 +48,7 @@ class Hologram(var tier: Int) extends traits.Environment with SidedEnvironment w var hasPower = true - val colorsByTier = Array(Array(0x00FF00), Array(0xFF0000, 0x00FF00, 0x0000FF)) + val colorsByTier = Array(Array(0x00FF00), Array(0x0000FF, 0x00FF00, 0xFF0000)) // 0xBBGGRR for rendering convenience def colors = colorsByTier(tier) def getColor(x: Int, y: Int, z: Int) = { @@ -203,7 +206,9 @@ class Hologram(var tier: Int) extends traits.Environment with SidedEnvironment w def getPaletteColor(computer: Context, args: Arguments): Array[AnyRef] = { val index = args.checkInteger(0) if (index < 0 || index >= colors.length) throw new ArrayIndexOutOfBoundsException() - result(colors(index)) + var col = colors(index) + // Colors are stored as 0xAABBGGRR for rendering convenience, so convert them back here + result(((col & 0xFF) << 16) | (col & 0xFF00) | ((col >>> 16) & 0xFF)) } @Callback(doc = """function(index:number, value:number):number -- Set the color defined for the specified value.""") @@ -212,7 +217,9 @@ class Hologram(var tier: Int) extends traits.Environment with SidedEnvironment w if (index < 0 || index >= colors.length) throw new ArrayIndexOutOfBoundsException() val value = args.checkInteger(1) val oldValue = colors(index) - colors(index) = value & 0xFFFFFF + // Change byte order here to allow passing stored color to OpenGL "as-is" + // (as whole Int, i.e. 0xAABBGGRR, alpha is unused but present for alignment) + colors(index) = ((value & 0xFF) << 16) | (value & 0xFF00) | ((value >>> 16) & 0xFF) ServerPacketSender.sendHologramColor(this, index, value) result(oldValue) } @@ -275,6 +282,7 @@ class Hologram(var tier: Int) extends traits.Environment with SidedEnvironment w override def shouldRenderInPass(pass: Int) = pass == 1 override def getMaxRenderDistanceSquared = scale / Settings.hologramMaxScaleByTier.max * Settings.get.hologramRenderDistance * Settings.get.hologramRenderDistance + def getFadeStartDistanceSquared = scale / Settings.hologramMaxScaleByTier.max * Settings.get.hologramFadeStartDistance * Settings.get.hologramFadeStartDistance override def getRenderBoundingBox = AxisAlignedBB.getAABBPool.getAABB(xCoord + 0.5 - 1.5 * scale, yCoord, zCoord - scale, xCoord + 0.5 + 1.5 * scale, yCoord + 0.25 + 2 * scale, zCoord + 0.5 + 1.5 * scale) @@ -303,7 +311,6 @@ class Hologram(var tier: Int) extends traits.Environment with SidedEnvironment w nbt.getIntArray("colors").copyToArray(colors) scale = nbt.getDouble("scale") hasPower = nbt.getBoolean("hasPower") - dirty = true } override def writeToNBTForClient(nbt: NBTTagCompound) {