2025-08-21 11:34:06 +02:00

478 lines
14 KiB
Scala

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
}
}