package ocelot.desktop.graphics import ocelot.desktop.color.{Color, RGBAColorNorm} import ocelot.desktop.geometry.{Rect2D, Size2D, Transform2D, Vector2D} import ocelot.desktop.graphics.Texture.MinFilteringMode import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D} import ocelot.desktop.graphics.render.InstanceRenderer import ocelot.desktop.ui.UiHandler import ocelot.desktop.util.{Logging, Resource, Spritesheet} import org.lwjgl.BufferUtils import org.lwjgl.opengl.{ARBFramebufferObject, GL11, GL21, GL30} import java.awt.image.BufferedImage import java.nio.ByteBuffer import scala.collection.mutable import scala.util.control.Breaks._ //noinspection ScalaWeakerAccess,ScalaUnusedSymbol class Graphics(private var width: Int, private var height: Int, private var scalingFactor: Float) extends Logging with Resource { private var time = 0f private var projection = Transform2D.viewport(width, height) private val shaderProgram = new ShaderProgram("general") private val renderer = new InstanceRenderer[MeshVertex2D, MeshInstance2D](Mesh2D.quad, MeshInstance2D, shaderProgram) private var _font: Font = Font.NormalFont private var oldFont: Font = _font private val stack = mutable.Stack[GraphicsState](GraphicsState()) private var spriteRect = Spritesheet.sprites("Empty") private val emptySpriteTrans = Transform2D.translate(spriteRect.x, spriteRect.y) >> Transform2D.scale(spriteRect.w, spriteRect.h) private val offscreenTexture = new Texture(width, height, GL21.GL_SRGB8_ALPHA8, GL11.GL_UNSIGNED_BYTE, GL11.GL_RGBA) private val offscreenFramebuffer = ARBFramebufferObject.glGenFramebuffers() GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, offscreenFramebuffer) GL30.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, offscreenTexture.texture, 0) private var currentFramebuffer = 0 private val currentScissor: Option[Rect2D] = None shaderProgram.set("uTexture", 0) shaderProgram.set("uTextTexture", 1) scale(scalingFactor) private[graphics] val screenShaderProgram = new ShaderProgram("general") screenShaderProgram.set("uTexture", 0) screenShaderProgram.set("uTextTexture", 1) def resize(width: Int, height: Int, scaling: Float): Boolean = { var viewportChanged = false if (scaling != scalingFactor) { scalingFactor = scaling stack.last.transform = Transform2D.scale(scalingFactor) viewportChanged = true } if (this.width != width || this.height != height) { this.width = width this.height = height offscreenTexture.bind() GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL21.GL_SRGB8_ALPHA8, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, null.asInstanceOf[ByteBuffer]) viewportChanged = true } viewportChanged } override def freeResource(): Unit = { super.freeResource() GL30.glDeleteFramebuffers(offscreenFramebuffer) offscreenTexture.freeResource() Spritesheet.freeResource() renderer.freeResource() shaderProgram.freeResource() screenShaderProgram.freeResource() } def font: Font = _font def setNormalFont(): Unit = { if (_font == Font.NormalFont) return flush() _font = Font.NormalFont } def setSmallFont(): Unit = { if (_font == Font.SmallFont) return flush() _font = Font.SmallFont } def save(): Unit = { stack.push(stack.head.copy()) } def restore(): Unit = { if (stack.length == 1) throw new RuntimeException("attempt to pop root graphics state") val old = stack.last if (old.scissor != stack.head.scissor) { flush() } stack.pop() } def beginGroupAlpha(): Unit = { flush() GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, offscreenFramebuffer) GL11.glDisable(GL11.GL_SCISSOR_TEST) GL11.glViewport(0, 0, width, height) GL11.glClearColor(0, 0, 0, 0) GL11.glClear(GL11.GL_COLOR_BUFFER_BIT) currentFramebuffer = offscreenFramebuffer } def endGroupAlpha(alpha: Float): Unit = { flush() currentFramebuffer = 0 foreground = RGBAColorNorm(1, 1, 1, alpha) spriteRect = Rect2D(0, 1f, 1f, -1f) _rect(0, 0, width / scalingFactor, height / scalingFactor, fixUV = false) flush(mainTexture = offscreenTexture) } def blitViewport3D(viewport: Viewport3D, bounds: Rect2D, alpha: Float = 1.0f): Unit = { flush() foreground = RGBAColorNorm(1, 1, 1, alpha) spriteRect = Rect2D(0, 1f, 1f, -1f) _rect(bounds.x, bounds.y, bounds.w, bounds.h, fixUV = false) flush(mainTexture = viewport.textureColor) } def blitScreenViewport( viewport: ScreenViewport, bounds: Rect2D, filteringMode: MinFilteringMode = MinFilteringMode.Nearest, alpha: Float = 1.0f, ): Unit = { flush() foreground = RGBAColorNorm(1, 1, 1, alpha) spriteRect = Rect2D(0, 1f, 1f, -1f) _rect(bounds.x, bounds.y, bounds.w, bounds.h, fixUV = false) viewport.texture.setMinFilter(filteringMode) flush(mainTexture = viewport.texture) } def begin(): Unit = { shaderProgram.set("uProj", Transform2D.viewport(width, height)) } def clear(): Unit = { flush(clear = true) } def setScissor(x: Float, y: Float, width: Float, height: Float): Unit = { flush() stack.head.scissor = Some((x, y, width, height)) } def clearScissor(): Unit = { flush() stack.head.scissor = None } def clear(x: Int, y: Int, width: Int, height: Int): Unit = { save() setScissor(x, y, width, height) clear() restore() } def foreground: RGBAColorNorm = stack.head.foreground def foreground_=(col: Color): Unit = { stack.head.foreground = col.toRGBANorm } def background: RGBAColorNorm = stack.head.background def background_=(col: Color): Unit = { stack.head.background = col.toRGBANorm } def fontSizeMultiplier: Float = stack.head.fontSizeMultiplier def fontSizeMultiplier_=(value: Float): Unit = { stack.head.fontSizeMultiplier = value } def alphaMultiplier: Float = stack.head.alphaMultiplier def alphaMultiplier_=(value: Float): Unit = { stack.head.alphaMultiplier = value } def sprite: String = stack.head.sprite def sprite_=(value: String): Unit = { stack.head.sprite = value spriteRect = Spritesheet.sprites(value) } def transform(t: Transform2D): Unit = { stack.head.transform = stack.head.transform >> t } def scale(v: Float): Unit = { transform(Transform2D.scale(v)) } def scale(x: Float, y: Float): Unit = { transform(Transform2D.scale(x, y)) } def translate(x: Float, y: Float): Unit = { transform(Transform2D.translate(x, y)) } def rotate(angle: Float): Unit = { transform(Transform2D.rotate(angle)) } def text(x: Float, y: Float, text: String, shrink: Int = 0): Unit = { var ox = x text.codePoints().forEach { c => char(ox, y, c) ox += _font.charWidth(c) - shrink } } def char(x: Float, y: Float, c: Int): Unit = { val fontSize = fontSizeMultiplier * _font.fontSize val rect = _font.map.getOrElse(c, _font.map('?')) val uvTransform = Transform2D.translate( rect.x, rect.y, ) >> Transform2D.scale( rect.w - 0.25f / _font.AtlasWidth, rect.h - 0.25f / _font.AtlasHeight, ) val transform = { stack.head.transform >> Transform2D.translate(x.round, y.round) >> Transform2D.scale(_font.charWidth(c), fontSize) } val foreground = stack.head.foreground.toRGBANorm.mapA(_ * alphaMultiplier) val background = stack.head.background.toRGBANorm.mapA(_ * alphaMultiplier) if (background.a > 0 || foreground.a > 0) renderer.schedule(MeshInstance2D(background, foreground, transform, emptySpriteTrans, uvTransform)) } // I hate scala. Overloaded methods with default arguments are not allowed def sprite(icon: IconSource, bounds: Rect2D): Unit = { sprite(icon, bounds.x, bounds.y, bounds.w, bounds.h, Color.White) } def sprite(icon: IconSource, bounds: Rect2D, color: Color): Unit = { sprite(icon, bounds.x, bounds.y, bounds.w, bounds.h, color) } def sprite(icon: IconSource, pos: Vector2D, size: Size2D): Unit = { sprite(icon, pos.x, pos.y, size.width, size.height, Color.White) } def sprite(icon: IconSource, pos: Vector2D, color: Color): Unit = { sprite(icon, pos.x, pos.y, color) } def sprite(icon: IconSource, pos: Vector2D, size: Size2D, color: Color): Unit = { sprite(icon, pos.x, pos.y, size.width, size.height, color) } def sprite(icon: IconSource, x: Float, y: Float): Unit = { sprite(icon, x, y, Color.White) } def sprite(icon: IconSource, x: Float, y: Float, width: Float, height: Float): Unit = { sprite(icon, x, y, width, height, Color.White) } def sprite(icon: IconSource, x: Float, y: Float, color: Color): Unit = { val size = Spritesheet.spriteSize(icon.path) sprite(icon, x, y, size.width, size.height, color) } def sprite( icon: IconSource, x: Float, y: Float, width: Float, height: Float, color: Color, ): Unit = { sprite = icon.path foreground = color val spriteRect = icon.animation.map { animation => val duration = animation.frames.map(_._2).sum var timeOffset = 0f var curFrame = 0 breakable { for ((idx, dur) <- animation.frames) { timeOffset += dur curFrame = idx if (timeOffset >= time % duration) break } } val size = animation.frameSize match { case Some(size) => Size2D(this.spriteRect.w, this.spriteRect.w * size.height / size.width) case None => Size2D(this.spriteRect.w, this.spriteRect.w) } this.spriteRect.copy(y = this.spriteRect.y + curFrame * size.height, h = size.height) } _rect(x, y, width, height, fixUV = true, spriteRect) } def sprite( name: String, x: Float, y: Float, width: Float, height: Float, color: Color, spriteRect: Option[Rect2D], fixUV: Boolean = true, ): Unit = { sprite = name foreground = color _rect(x, y, width, height, fixUV, spriteRect) } def rect(r: Rect2D, color: Color): Unit = { rect(r.x, r.y, r.w, r.h, color) } def rect(x: Float, y: Float, width: Float, height: Float, color: Color = RGBAColorNorm(1f, 1f, 1f)): Unit = { sprite(IconSource.Empty, x, y, width, height, color) } private def checkFont(): Unit = { if (_font != oldFont) { val newFont = _font _font = oldFont flush() _font = newFont oldFont = _font } } private def _rect(x: Float, y: Float, width: Float, height: Float, fixUV: Boolean = true, spriteRectOptional: Option[Rect2D] = None): Unit = { val spriteRect = spriteRectOptional.getOrElse(this.spriteRect) val uvTransform = Transform2D.translate(spriteRect.x, spriteRect.y) >> (if (fixUV) Transform2D.scale(spriteRect.w - 0.25f / 1024, spriteRect.h - 0.25f / 1024) else Transform2D.scale(spriteRect.w, spriteRect.h)) val transform = { stack.head.transform >> Transform2D.translate(x, y) >> Transform2D.scale(width, height) } val color = stack.head.foreground.toRGBANorm.mapA(_ * alphaMultiplier) if (color.a > 0) renderer.schedule(MeshInstance2D(color, Color.Transparent, transform, uvTransform, Transform2D.scale(0))) } def line(x1: Float, y1: Float, x2: Float, y2: Float, thickness: Float, color: Color): Unit = { save() val dy = x2 - x1 val dx = y2 - y1 val length = math.sqrt(dx * dx + dy * dy).toFloat val inclination = math.atan2(dy, dx).toFloat translate(x1, y1) rotate(-inclination + (math.Pi * 0.5).toFloat) rect(0, -thickness * 0.5f, length, thickness, color) restore() } def line(start: Vector2D, end: Vector2D, thickness: Float, color: Color): Unit = { line(start.x, start.y, end.x, end.y, thickness, color) } def screenshot(): BufferedImage = { val buffer = BufferUtils.createByteBuffer(width * height * 4) GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0) GL11.glReadPixels(0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer) val image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) for (y <- 0 until height) { for (x <- 0 until width) { val i = (x + (height - y - 1) * width) * 4 val r = buffer.get(i) & 0xff val g = buffer.get(i + 1) & 0xff val b = buffer.get(i + 2) & 0xff val rgb = (r << 16) | (g << 8) | b image.setRGB(x, y, rgb) } } image } def flush(mainTexture: Texture = Spritesheet.texture, clear: Boolean = false): Unit = { if (renderer.isEmpty && !clear) return GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, currentFramebuffer) GL11.glEnable(GL30.GL_FRAMEBUFFER_SRGB) GL11.glViewport(0, 0, width, height) GL11.glDisable(GL11.GL_DEPTH_TEST) GL11.glDisable(GL11.GL_CULL_FACE) GL11.glEnable(GL11.GL_BLEND) GL11.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA) stack.head.scissor match { case Some((x, y, w, h)) => GL11.glEnable(GL11.GL_SCISSOR_TEST) GL11.glScissor( Math.round(x * scalingFactor), Math.round(height - h * scalingFactor - y * scalingFactor), Math.round(w * scalingFactor), Math.round(h * scalingFactor), ) case _ => GL11.glDisable(GL11.GL_SCISSOR_TEST) } mainTexture.bind() _font.texture.bind(1) renderer.flush() if (clear) { GL11.glClearColor(1, 1, 1, 1) GL11.glClear(GL11.GL_COLOR_BUFFER_BIT) } } def update(): Unit = { time += UiHandler.dt * 20f } }