blob: b6a49bd1cec229c4a983afb1719b4d797213a78a [file] [log] [blame] [raw]
package li.cil.oc.client.renderer.tileentity
import com.google.common.cache.{CacheBuilder, RemovalNotification, RemovalListener}
import cpw.mods.fml.common.{TickType, ITickHandler}
import java.util
import java.util.concurrent.{TimeUnit, Callable}
import li.cil.oc.Config
import li.cil.oc.client.renderer.MonospaceFontRenderer
import li.cil.oc.common.tileentity.Screen
import li.cil.oc.util.RenderState
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.GLAllocation
import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer
import net.minecraft.tileentity.TileEntity
import net.minecraftforge.common.ForgeDirection
import org.lwjgl.opengl.GL11
object ScreenRenderer extends TileEntitySpecialRenderer with Callable[Int] with RemovalListener[TileEntity, Int] with ITickHandler {
private val maxRenderDistanceSq = Config.maxScreenTextRenderDistance * Config.maxScreenTextRenderDistance
private val fadeDistanceSq = Config.screenTextFadeStartDistance * Config.screenTextFadeStartDistance
private val fadeRatio = 1.0 / (maxRenderDistanceSq - fadeDistanceSq)
/** We cache the display lists for the screens we render for performance. */
val cache = com.google.common.cache.CacheBuilder.newBuilder().
expireAfterAccess(2, TimeUnit.SECONDS).
removalListener(this).
asInstanceOf[CacheBuilder[Screen, Int]].
build[Screen, Int]()
/** Used to pass the current screen along to call(). */
private var screen: Screen = null
// ----------------------------------------------------------------------- //
// Rendering
// ----------------------------------------------------------------------- //
override def renderTileEntityAt(t: TileEntity, x: Double, y: Double, z: Double, f: Float) {
screen = t.asInstanceOf[Screen]
if (!screen.isOrigin || !screen.hasPower) {
return
}
val distance = playerDistanceSq()
if (distance > maxRenderDistanceSq) {
return
}
// Crude check whether screen text can be seen by the local player based
// on the player's position -> angle relative to screen.
val screenFacing = screen.facing.getOpposite
if (screenFacing.offsetX * (x + 0.5) + screenFacing.offsetY * (y + 0.5) + screenFacing.offsetZ * (z + 0.5) < 0) {
return
}
GL11.glPushAttrib(0xFFFFFF)
RenderState.disableLighting()
RenderState.makeItBlend()
GL11.glPushMatrix()
GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5)
if (distance > fadeDistanceSq) {
RenderState.setBlendAlpha(0f max (1 - (distance - fadeDistanceSq) * fadeRatio).toFloat)
}
MonospaceFontRenderer.init(tileEntityRenderer.renderEngine)
val list = cache.get(screen, this)
compileOrDraw(list)
GL11.glPopMatrix()
GL11.glPopAttrib()
}
private def compileOrDraw(list: Int) = if (screen.hasChanged && !RenderState.compilingDisplayList) {
screen.hasChanged = false
val (sx, sy) = (screen.width, screen.height)
val (tw, th) = (sx * 16f, sy * 16f)
GL11.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE)
screen.yaw match {
case ForgeDirection.WEST => GL11.glRotatef(-90, 0, 1, 0)
case ForgeDirection.NORTH => GL11.glRotatef(180, 0, 1, 0)
case ForgeDirection.EAST => GL11.glRotatef(90, 0, 1, 0)
case _ => // No yaw.
}
screen.pitch match {
case ForgeDirection.DOWN => GL11.glRotatef(90, 1, 0, 0)
case ForgeDirection.UP => GL11.glRotatef(-90, 1, 0, 0)
case _ => // No pitch.
}
// Fit area to screen (bottom left = bottom left).
GL11.glTranslatef(-0.5f, -0.5f, 0.5f)
GL11.glTranslatef(0, sy, 0)
// Flip text upside down.
GL11.glScalef(1, -1, 1)
// Offset from border.
GL11.glTranslatef(sx * 2.25f / tw, sy * 2.25f / th, 0)
// Inner size (minus borders).
val isx = sx - (4.5f / 16)
val isy = sy - (4.5f / 16)
// Scale based on actual buffer size.
val (resX, resY) = screen.instance.resolution
val sizeX = resX * MonospaceFontRenderer.fontWidth
val sizeY = resY * MonospaceFontRenderer.fontHeight
val scaleX = isx / sizeX
val scaleY = isy / sizeY
if (true) {
if (scaleX > scaleY) {
GL11.glTranslatef(sizeX * 0.5f * (scaleX - scaleY), 0, 0)
GL11.glScalef(scaleY, scaleY, 1)
}
else {
GL11.glTranslatef(0, sizeY * 0.5f * (scaleY - scaleX), 0)
GL11.glScalef(scaleX, scaleX, 1)
}
}
else {
// Stretch to fit.
GL11.glScalef(scaleX, scaleY, 1)
}
// Slightly offset the text so it doesn't clip into the screen.
GL11.glTranslatef(0, 0, 0.01f)
for (((line, color), i) <- screen.instance.lines.zip(screen.instance.color).zipWithIndex) {
MonospaceFontRenderer.drawString(0, i * MonospaceFontRenderer.fontHeight, line, color, screen.instance.depth)
}
GL11.glEndList()
true
}
else GL11.glCallList(list)
private def playerDistanceSq() = {
val player = Minecraft.getMinecraft.thePlayer
val bounds = screen.getRenderBoundingBox
val px = player.posX
val py = player.posY
val pz = player.posZ
val ex = bounds.maxX - bounds.minX
val ey = bounds.maxY - bounds.minY
val ez = bounds.maxZ - bounds.minZ
val cx = bounds.minX + ex * 0.5
val cy = bounds.minY + ey * 0.5
val cz = bounds.minZ + ez * 0.5
val dx = px - cx
val dy = py - cy
val dz = pz - cz
(if (dx < -ex) {
val d = dx + ex
d * d
}
else if (dx > ex) {
val d = dx - ex
d * d
}
else 0) + (if (dy < -ey) {
val d = dy + ey
d * d
}
else if (dy > ey) {
val d = dy - ey
d * d
}
else 0) + (if (dz < -ez) {
val d = dz + ez
d * d
}
else if (dz > ez) {
val d = dz - ez
d * d
}
else 0)
}
// ----------------------------------------------------------------------- //
// Cache
// ----------------------------------------------------------------------- //
def call = {
val list = GLAllocation.generateDisplayLists(1)
screen.hasChanged = true // Force compilation.
list
}
def onRemoval(e: RemovalNotification[TileEntity, Int]) {
GLAllocation.deleteDisplayLists(e.getValue)
}
// ----------------------------------------------------------------------- //
// ITickHandler
// ----------------------------------------------------------------------- //
def getLabel = "OpenComputers.Screen"
def ticks() = util.EnumSet.of(TickType.CLIENT)
def tickStart(tickType: util.EnumSet[TickType], tickData: AnyRef*) = cache.cleanUp()
def tickEnd(tickType: util.EnumSet[TickType], tickData: AnyRef*) {}
}