KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt

316 lines
9.9 KiB
Kotlin

package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.matrix.Matrix3f
import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kommons.vector.Vector3f
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kstarbound.client.render.IGeometryLayer
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.math.Line2d
@JsonAdapter(Drawable.Adapter::class)
sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbright: Boolean) {
@JsonFactory
data class Transformations(val centered: Boolean = false, val rotation: Float = 0f, val mirrored: Boolean = false, val scale: Either<Float, Vector2f> = Either.left(1f))
class Line(
val line: Line2d,
val width: Float,
position: Vector2f = Vector2f.ZERO,
color: RGBAColor = RGBAColor.WHITE,
fullbright: Boolean = false
) : Drawable(position, color, fullbright) {
override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) {
TODO("Not yet implemented")
}
override fun flop(): Drawable {
TODO("Not yet implemented")
}
override fun boundingBox(cropImages: Boolean): AABB {
TODO("Not yet implemented")
}
override fun scale(scale: Vector2f): Drawable {
TODO("Not yet implemented")
}
override fun translate(translation: Vector2f): Drawable {
TODO("Not yet implemented")
}
}
class Poly(
val vertices: ImmutableList<Vector2f>,
position: Vector2f = Vector2f.ZERO,
color: RGBAColor = RGBAColor.WHITE,
fullbright: Boolean = false
) : Drawable(position, color, fullbright) {
override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) {
TODO("Not yet implemented")
}
override fun flop(): Drawable {
TODO("Not yet implemented")
}
override fun boundingBox(cropImages: Boolean): AABB {
TODO("Not yet implemented")
}
override fun scale(scale: Vector2f): Drawable {
TODO("Not yet implemented")
}
override fun translate(translation: Vector2f): Drawable {
TODO("Not yet implemented")
}
}
class Image(
val path: SpriteReference,
val transform: Either<Matrix3f, Transformations>,
position: Vector2f = Vector2f.ZERO,
color: RGBAColor = RGBAColor.WHITE,
fullbright: Boolean = false
) : Drawable(position, color, fullbright) {
override fun with(values: (String) -> String?): Image {
val newPath = path.with(values)
if (newPath == path) {
return this
}
return Image(newPath, transform, position, color, fullbright)
}
override fun flop(): Drawable {
return Image(path, transform.flatMap({ it.copy().scale(-1f, 1f) }, { it.copy(mirrored = !it.mirrored) }), position, color, fullbright)
}
override fun scale(scale: Vector2f): Drawable {
return Image(path, transform.flatMap({ it.scale(scale) }, { it.copy(scale = Either.right(it.scale.map({ scale * it }, { scale * it }))) }), position, color, fullbright)
}
override fun translate(translation: Vector2f): Drawable {
return Image(path, transform, position + translation, color, fullbright)
}
private fun getTransforms(sprite: ru.dbotthepony.kstarbound.defs.image.Image.Sprite) = transform.map({ it.copy() }, {
val mat = Matrix3f.identity()
it.scale.map({ mat.scale(it / PIXELS_IN_STARBOUND_UNITf, it / PIXELS_IN_STARBOUND_UNITf) }, { mat.scale(it / PIXELS_IN_STARBOUND_UNITf) })
if (it.centered) {
mat.translate(sprite.width / -2f, sprite.height / -2f)
}
if (it.rotation != 0f) {
mat.rotateAroundZ(it.rotation)
}
if (it.mirrored) {
mat.translate(sprite.width.toFloat(), 0f)
mat.scale(-1f, 1f)
}
mat
})
// Original engine here calculates bounding box wrong
// if "cropImages" is false
override fun boundingBox(cropImages: Boolean): AABB {
val sprite = path.sprite ?: return AABB.ZERO
var result: AABB
if (cropImages) {
result = AABB(
Vector2d(sprite.nonEmptyRegion.x.toDouble(), sprite.nonEmptyRegion.y.toDouble()),
Vector2d(sprite.nonEmptyRegion.z.toDouble(), sprite.nonEmptyRegion.w.toDouble()),
)
} else {
result = AABB(
Vector2d.ZERO,
Vector2d(sprite.width.toDouble(), sprite.height.toDouble())
)
}
val transforms = getTransforms(sprite)
result = result.expand((Vector2f(result.mins.x.toFloat(), result.mins.y.toFloat()) * transforms).toDoubleVector())
result = result.expand((Vector2f(result.mins.x.toFloat(), result.maxs.y.toFloat()) * transforms).toDoubleVector())
result = result.expand((Vector2f(result.maxs.x.toFloat(), result.mins.y.toFloat()) * transforms).toDoubleVector())
result = result.expand((Vector2f(result.maxs.x.toFloat(), result.maxs.y.toFloat()) * transforms).toDoubleVector())
result += position.toDoubleVector()
return result
}
override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) {
val sprite = path.sprite ?: return
val texture = path.image!!.texture
val program = if (fullbright) client.programs.positionTexture else client.programs.positionTextureLightmap
val mat = getTransforms(sprite)
val builder = layer.getBuilder(program.config(texture))
mat.preTranslate(position.x + x, position.y + y)
client.stack.last().mulIntoOther(mat)
builder.mode(GeometryType.QUADS)
builder.vertex(mat, 0f, 0f).uv(sprite.u0, sprite.v0)
builder.vertex(mat, 0f + sprite.width, 0f).uv(sprite.u1, sprite.v0)
builder.vertex(mat, 0f + sprite.width, 0f + sprite.height).uv(sprite.u1, sprite.v1)
builder.vertex(mat, 0f, 0f + sprite.height).uv(sprite.u0, sprite.v1)
}
}
class Empty(position: Vector2f = Vector2f.ZERO, color: RGBAColor = RGBAColor.WHITE, fullbright: Boolean = false) : Drawable(position, color, fullbright) {
override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) {}
override fun flop(): Drawable {
return this
}
override fun boundingBox(cropImages: Boolean): AABB {
return AABB.ZERO
}
override fun scale(scale: Vector2f): Drawable {
return this
}
override fun translate(translation: Vector2f): Drawable {
return Empty(position + translation, color, fullbright)
}
}
open fun with(values: (String) -> String?): Drawable {
return this
}
abstract fun render(client: StarboundClient = StarboundClient.current(), layer: IGeometryLayer, x: Float = 0f, y: Float = 0f)
abstract fun boundingBox(cropImages: Boolean = false): AABB
abstract fun scale(scale: Vector2f): Drawable
fun scale(scale: Float): Drawable {
if (scale == 1f)
return this
return scale(Vector2f(scale, scale))
}
abstract fun translate(translation: Vector2f): Drawable
/**
* mirror along X axis
*/
abstract fun flop(): Drawable
companion object {
val EMPTY = Empty()
val CENTERED = Transformations(true)
private val LOGGER = LogManager.getLogger()
}
class Adapter(gson: Gson) : TypeAdapter<Drawable>() {
private val lines = gson.getAdapter(Line2d::class.java)
private val vectors = gson.getAdapter(Vector2f::class.java)
private val vectors3 = gson.getAdapter(Vector3f::class.java)
private val colors = gson.getAdapter(RGBAColor::class.java)
private val images = gson.getAdapter(SpriteReference::class.java)
private val vertices = gson.getAdapter(TypeToken.getParameterized(ImmutableList::class.java, Vector2f::class.java)) as TypeAdapter<ImmutableList<Vector2f>>
private val transformations = gson.getAdapter(Transformations::class.java)
override fun write(out: JsonWriter?, value: Drawable?) {
TODO("Not yet implemented")
}
override fun read(`in`: JsonReader): Drawable {
if (`in`.consumeNull()) {
return EMPTY
} else {
val value = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)!!
val position = value["position"]?.let { vectors.fromJsonTree(it) } ?: Vector2f.ZERO
val color = value["color"]?.let { colors.fromJsonTree(it) } ?: RGBAColor.WHITE
val fullbright = value["fullbright"]?.asBoolean ?: false
if ("line" in value) {
return Line(lines.fromJsonTree(value["line"]), value["width"].asFloat, position, color, fullbright)
} else if ("poly" in value) {
return Poly(vertices.fromJsonTree(value["poly"]), position, color, fullbright)
} else if ("image" in value) {
val image = images.fromJsonTree(value["image"])
if ("transformation" in value) {
val mat = Matrix3f.identity()
val array = value["transformation"].asJsonArray
// original starbound use GLM, which reflects OpenGL, which in turn make matrices row-major
val row0 = vectors3.fromJsonTree(array[0])
val row1 = vectors3.fromJsonTree(array[1])
val row2 = vectors3.fromJsonTree(array[2])
mat.r00 = row0.x
mat.r01 = row0.y
mat.r02 = row0.z
mat.r10 = row1.x
mat.r11 = row1.y
mat.r12 = row1.z
mat.r20 = row2.x
mat.r21 = row2.y
mat.r22 = row2.z
return Image(
image,
Either.left(mat),
position,
color,
fullbright
)
} else {
return Image(
image,
Either.right(transformations.fromJsonTree(value)),
position,
color,
fullbright
)
}
} else {
return Empty(position, color, fullbright)
}
}
}
}
}