Атлас спрайтов (frames), переделан с нуля

This commit is contained in:
DBotThePony 2022-12-30 23:52:35 +07:00
parent 49e9dd2735
commit 8ef4ab0eb1
Signed by: DBot
GPG Key ID: DCC23B5715498507
10 changed files with 399 additions and 23 deletions

View File

@ -15,6 +15,7 @@ import ru.dbotthepony.kstarbound.api.NonExistingFile
import ru.dbotthepony.kstarbound.api.PhysicalFile
import ru.dbotthepony.kstarbound.api.explore
import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.defs.animation.SpriteReference
import ru.dbotthepony.kstarbound.defs.item.ItemDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemRarity
import ru.dbotthepony.kstarbound.defs.liquid.LiquidDefinition
@ -32,6 +33,7 @@ import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2iTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector4iTypeAdapter
import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.util2d.AABBi
@ -135,12 +137,13 @@ object Starbound {
.registerTypeAdapter(stringTypeAdapter)
// math
.registerTypeAdapter(AABB::class.java, AABBTypeAdapter)
.registerTypeAdapter(AABBi::class.java, AABBiTypeAdapter)
.registerTypeAdapter(Vector2d::class.java, Vector2dTypeAdapter)
.registerTypeAdapter(Vector2f::class.java, Vector2fTypeAdapter)
.registerTypeAdapter(Vector2i::class.java, Vector2iTypeAdapter)
.registerTypeAdapter(Poly::class.java, PolyTypeAdapter)
.registerTypeAdapter(AABBTypeAdapter)
.registerTypeAdapter(AABBiTypeAdapter)
.registerTypeAdapter(Vector2dTypeAdapter)
.registerTypeAdapter(Vector2fTypeAdapter)
.registerTypeAdapter(Vector2iTypeAdapter)
.registerTypeAdapter(Vector4iTypeAdapter)
.registerTypeAdapter(PolyTypeAdapter)
.also(ConfigurableProjectile::registerGson)
.also(SkyParameters::registerGson)
@ -154,6 +157,7 @@ object Starbound {
.also(LiquidDefinition::registerGson)
.also(ItemDefinition::registerGson)
.also(ItemRarity::registerGson)
.also(SpriteReference::registerGson)
.registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe())

View File

@ -28,6 +28,10 @@ interface IStarboundFile {
fun orNull(): IStarboundFile? = if (exists) this else null
operator fun get(name: String): IStarboundFile? {
return children?.get(name)
}
fun computeFullPath(): String {
var path = name
var parent = parent

View File

@ -1,6 +1,5 @@
package ru.dbotthepony.kstarbound.client.render.entity
import org.lwjgl.opengl.GL46
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.ClientChunk
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
@ -11,28 +10,31 @@ import ru.dbotthepony.kvector.matrix.Matrix4fStack
class ItemRenderer(state: GLStateTracker, entity: ItemEntity, chunk: ClientChunk?) : EntityRenderer(state, entity, chunk) {
private val def = entity.def
private val texture = def.inventoryIcon?.let(state::loadNamedTextureSafe)
private val textures = def.inventoryIcon?.stream()?.map { state.loadNamedTextureSafe(it.image.path) }?.toList() ?: listOf()
override fun render(stack: Matrix4fStack) {
if (texture == null)
if (textures.isEmpty())
return
state.shaderVertexTexture.use()
state.shaderVertexTexture.transform.set(stack.last)
state.activeTexture = 0
state.shaderVertexTexture["_texture"] = 0
texture.bind()
val builder = state.flat2DTexturedQuads.small
for (texture in textures) {
texture.bind()
builder.begin()
val builder = state.flat2DTexturedQuads.small
val width = (texture.width / PIXELS_IN_STARBOUND_UNITf) / 2f
val height = (texture.height / PIXELS_IN_STARBOUND_UNITf) / 2f
builder.begin()
builder.quadRotatedZ(-width, -height, width, height, 5f, 0f, 0f, entity.movement.angle, QuadTransformers.uv(0f, 1f, 1f, 0f))
val width = (texture.width / PIXELS_IN_STARBOUND_UNITf) / 2f
val height = (texture.height / PIXELS_IN_STARBOUND_UNITf) / 2f
builder.upload()
builder.draw()
builder.quadRotatedZ(-width, -height, width, height, 5f, 0f, 0f, entity.movement.angle, QuadTransformers.uv(0f, 1f, 1f, 0f))
builder.upload()
builder.draw()
}
}
}

View File

@ -0,0 +1,233 @@
package ru.dbotthepony.kstarbound.defs.animation
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonArray
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.stream.JsonReader
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.gl.GLTexture2D
import ru.dbotthepony.kstarbound.io.json.stream
import ru.dbotthepony.kvector.vector.nint.Vector2i
import ru.dbotthepony.kvector.vector.nint.Vector4i
import java.util.concurrent.ConcurrentHashMap
/**
* Атлас спрайтов, собранный вручную артистом
*
* В файлах игры именуется frames
*/
class AtlasDefinition private constructor(
val name: String,
/**
* Спрайты данного атласа, включая спрайты под псевдонимами
*/
val sprites: ImmutableMap<String, Sprite>,
) {
/**
* Первый спрайт, по своему имени (естественная сортировка)
*/
val first: Sprite = sprites[sprites.keys.stream().sorted().findFirst().orElseThrow { NoSuchElementException("No a single key present in $name") }] ?: throw NoSuchElementException("IMPOSSIBRU in $name")
operator fun get(name: String): Sprite? {
return sprites[name]
}
fun any(name: String): Sprite {
return get(name) ?: first
}
fun any(): Sprite {
return get("root") ?: get("0") ?: first
}
class Sprite(
/**
* Имя данного спрайта, на которое прототипы могут ссылаться
*
* К примеру, если имя будет указано как
*
* **`active.2`**
*
* а сам атлас называется
*
* **`mything.png`**
*
* то прототипы могут ссылаться на данный спрайт как
*
* **`mything.png:active.2`**
*/
val name: String,
/**
* Позиция и размеры данного спрайта, в пикселях
*/
val position: Vector4i,
) {
/**
* Вычисляет uv координаты данного спрайта на заданном полотне
*/
fun compute(width: Int, height: Int): UVCoordinates {
return UVCoordinates(
u0 = position.x.toFloat() / width.toFloat(),
v0 = position.y.toFloat() / height.toFloat(),
u1 = position.z.toFloat() / width.toFloat(),
v1 = position.w.toFloat() / height.toFloat(),
)
}
/**
* Вычисляет uv координаты данного спрайта на заданном полотне
*/
fun compute(dimensions: Vector2i): UVCoordinates {
return compute(dimensions.x, dimensions.y)
}
/**
* Вычисляет uv координаты данного спрайта на заданном полотне
*/
fun compute(texture: GLTexture2D): UVCoordinates {
return compute(texture.width, texture.height)
}
override fun toString(): String {
return "FrameGrid.Sprite[name=$name, position=$position]"
}
}
companion object {
val EMPTY = AtlasDefinition("null", ImmutableMap.of("root", Sprite("root", Vector4i(0, 0, 1, 1))))
private val cache = ConcurrentHashMap<String, AtlasDefinition>()
private fun generateFakeNames(dimensions: Vector2i): JsonArray {
return JsonArray(dimensions.y).also {
var stripElem = 0
for (stripNum in 0 until dimensions.y) {
val strip = JsonArray(dimensions.x)
for (i in 0 until dimensions.x) {
strip.add(stripElem.toString())
stripElem++
}
it.add(strip)
}
}
}
private fun parseFrames(input: JsonReader, name: String): AtlasDefinition {
val read = TypeAdapters.JSON_ELEMENT.read(input)
if (read !is JsonObject) {
throw JsonSyntaxException("Expected to have object as top most element, $read given")
}
val frameGrid = read["frameGrid"]
val sprites = HashMap<String, Sprite>()
if (frameGrid is JsonObject) {
val size = Starbound.gson.fromJson(frameGrid["size"] ?: throw JsonSyntaxException("Missing frameGrid.size"), Vector2i::class.java)
val dimensions = Starbound.gson.fromJson(frameGrid["dimensions"] ?: throw JsonSyntaxException("Missing frameGrid.dimensions"), Vector2i::class.java)
require(size.x >= 0) { "Invalid size.x: ${size.x}" }
require(size.y >= 0) { "Invalid size.y: ${size.y}" }
require(dimensions.x >= 0) { "Invalid dimensions.x: ${dimensions.x}" }
require(dimensions.y >= 0) { "Invalid dimensions.y: ${dimensions.y}" }
val names = (frameGrid["names"] as? JsonArray ?: generateFakeNames(dimensions))
.stream().map { (it as? JsonArray)?.stream()?.map { if (it == JsonNull.INSTANCE) null else it.asString }?.toList() }.toList()
val spriteList = ArrayList<Sprite>()
for ((y, strip) in names.withIndex()) {
// разрешаем вставлять null как ленту кадров, что означает что мы должны пропустить её полностью
if (strip == null)
continue
for ((x, spriteName) in strip.withIndex()) {
// если кадр не имеет имени...
if (spriteName == null)
continue
spriteList.add(Sprite(spriteName, Vector4i(x * size.x, y * size.y, (x + 1) * size.x, (y + 1) * size.y)))
}
}
for (sprite in spriteList)
sprites[sprite.name] = sprite
} else if (frameGrid != null) {
throw JsonSyntaxException("Unexpected frameGrid element: $frameGrid")
} else {
val frameList = read["frameList"] ?: throw JsonSyntaxException("Frame grid must have either frameGrid or frameList object defined")
if (frameList !is JsonObject) {
throw JsonSyntaxException("frameList is not an object")
}
for ((spriteName, coords) in frameList.entrySet()) {
sprites[spriteName] = Sprite(spriteName, Starbound.gson.fromJson(coords, Vector4i::class.java))
}
}
val aliases = read["aliases"]
if (aliases != null) {
if (aliases !is JsonObject)
throw JsonSyntaxException("aliases expected to be a Json object, $aliases given")
for ((k, v) in aliases.entrySet())
sprites[k] = sprites[v.asString] ?: throw JsonSyntaxException("$k want to refer to sprite $v, but it does not exist")
}
return AtlasDefinition(name, ImmutableMap.copyOf(sprites))
}
private fun recursiveGet(name: String, folder: String): AtlasDefinition? {
var current = folder
while (current != "/" && current != "") {
val get = cache.computeIfAbsent("$current/$name") {
val file = Starbound.locate("$it.frames")
if (file.exists) {
try {
return@computeIfAbsent parseFrames(JsonReader(file.reader()), "$it.frames")
} catch (err: Throwable) {
throw JsonSyntaxException("Reading frame grid $it.frames")
}
}
return@computeIfAbsent EMPTY
}
if (get !== EMPTY) {
return get
}
current = current.substringBeforeLast('/')
}
return null
}
fun get(path: String): AtlasDefinition {
require(path[0] == '/') { "$path is not an absolute path" }
val folder = path.substringBeforeLast('/').lowercase()
val filename = path.substringAfterLast('/').substringBefore('.').lowercase()
val direct = recursiveGet(filename, folder)
if (direct != null) return direct
val default = recursiveGet("default", folder)
if (default != null) return default
return EMPTY
}
}
}

View File

@ -0,0 +1,26 @@
package ru.dbotthepony.kstarbound.defs.animation
import ru.dbotthepony.kvector.api.IStruct4f
interface IUVCoordinates : IStruct4f {
val u0: Float
val v0: Float
val u1: Float
val v1: Float
override fun component1(): Float {
return u0
}
override fun component2(): Float {
return v0
}
override fun component3(): Float {
return u1
}
override fun component4(): Float {
return v1
}
}

View File

@ -0,0 +1,37 @@
package ru.dbotthepony.kstarbound.defs.animation
import com.google.gson.GsonBuilder
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.registerTypeAdapter
data class SpriteReference(
val path: String,
val sprite: AtlasDefinition.Sprite
) {
companion object : TypeAdapter<SpriteReference>() {
fun parse(input: String): SpriteReference {
val grid = AtlasDefinition.get(input.substringBefore(':'))
return when (input.count { it == ':' }) {
0 -> SpriteReference(input, grid.any())
1 -> SpriteReference(input.substringBefore(':'), grid.get(input.substringAfter(':')) ?: throw NoSuchElementException("No such sprite with name ${input.substringAfter(':')} present in frame grid ${grid.name} (atlas ${input.substringBefore(':')})"))
else -> throw IllegalArgumentException("Invalid sprite reference: $input")
}
}
override fun write(out: JsonWriter, value: SpriteReference) {
out.value(value.path + ":" + value.sprite.name)
}
override fun read(`in`: JsonReader): SpriteReference {
return parse(Starbound.readingFolderTransformer(`in`.nextString()))
}
fun registerGson(gsonBuilder: GsonBuilder) {
gsonBuilder.registerTypeAdapter(this)
}
}
}

View File

@ -0,0 +1,8 @@
package ru.dbotthepony.kstarbound.defs.animation
data class UVCoordinates(
override val u0: Float,
override val v0: Float,
override val u1: Float,
override val v1: Float,
) : IUVCoordinates

View File

@ -1,12 +1,10 @@
package ru.dbotthepony.kstarbound.defs.item
import com.google.gson.GsonBuilder
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.animation.SpriteReference
import ru.dbotthepony.kstarbound.io.json.KConcreteTypeAdapter
import ru.dbotthepony.kstarbound.io.json.ListAdapter
import ru.dbotthepony.kstarbound.io.json.asJsonObject
import ru.dbotthepony.kstarbound.io.json.asList
import ru.dbotthepony.kstarbound.io.json.ifString
@ -37,7 +35,7 @@ data class ItemDefinition(
/**
* Иконка в инвентаре, относительный и абсолютный пути
*/
val inventoryIcon: String? = null,
val inventoryIcon: List<InventoryIcon>? = null,
/**
* Описание предмета
@ -260,13 +258,21 @@ data class ItemDefinition(
val amount: Double = 0.0,
)
data class InventoryIcon(
val image: SpriteReference
)
companion object {
val INVENTORY_ICON_ADAPTER = KConcreteTypeAdapter.Builder(InventoryIcon::class)
.auto(InventoryIcon::image)
.build()
val ADAPTER = KConcreteTypeAdapter.Builder(ItemDefinition::class)
.auto(ItemDefinition::itemName)
.auto(ItemDefinition::price)
.auto(ItemDefinition::rarity)
.auto(ItemDefinition::category)
.auto(ItemDefinition::inventoryIcon, Starbound::readingFolderTransformerNullable)
.add(ItemDefinition::inventoryIcon, ListAdapter(INVENTORY_ICON_ADAPTER).ifString { listOf(InventoryIcon(SpriteReference.parse(Starbound.readingFolderTransformer(it)))) }.nullSafe())
.auto(ItemDefinition::description)
.auto(ItemDefinition::shortdescription)
@ -342,6 +348,7 @@ data class ItemDefinition(
gsonBuilder.registerTypeAdapter(FOSSIL_ADAPTER)
gsonBuilder.registerTypeAdapter(ARMOR_FRAMES_ADAPTER)
gsonBuilder.registerTypeAdapter(STATUS_EFFECT_ADAPTER)
gsonBuilder.registerTypeAdapter(INVENTORY_ICON_ADAPTER)
}
}
}

View File

@ -0,0 +1,30 @@
package ru.dbotthepony.kstarbound.io.json
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import it.unimi.dsi.fastutil.objects.ObjectSpliterator
import it.unimi.dsi.fastutil.objects.ObjectSpliterators
import java.util.stream.Stream
import java.util.stream.StreamSupport
class JsonArraySpliterator(private val obj: JsonArray, offset: Int = 0, private val maxPos: Int = obj.size()) : ObjectSpliterators.AbstractIndexBasedSpliterator<JsonElement>(offset) {
init {
require(offset >= 0) { "Invalid offset $offset" }
require(offset + maxPos <= obj.size()) { "$offset -> $maxPos while having only size of ${obj.size()}!" }
}
override fun get(location: Int): JsonElement {
return obj[location]
}
override fun getMaxPos(): Int {
return maxPos
}
override fun makeForSplit(pos: Int, maxPos: Int): ObjectSpliterator<JsonElement> {
return JsonArraySpliterator(obj, pos, maxPos)
}
}
fun JsonArray.elementSpliterator() = JsonArraySpliterator(this)
fun JsonArray.stream(): Stream<out JsonElement> = StreamSupport.stream(elementSpliterator(), false)

View File

@ -6,6 +6,31 @@ import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import ru.dbotthepony.kvector.vector.nfloat.Vector2f
import ru.dbotthepony.kvector.vector.nint.Vector2i
import ru.dbotthepony.kvector.vector.nint.Vector4i
object Vector4iTypeAdapter : TypeAdapter<Vector4i>() {
override fun write(out: JsonWriter, value: Vector4i) {
`out`.beginArray()
`out`.value(value.x)
`out`.value(value.y)
`out`.value(value.z)
`out`.value(value.w)
`out`.endArray()
}
override fun read(`in`: JsonReader): Vector4i {
`in`.beginArray()
val x = `in`.nextInt()
val y = `in`.nextInt()
val z = `in`.nextInt()
val w = `in`.nextInt()
`in`.endArray()
return Vector4i(x, y, z, w)
}
}
object Vector2iTypeAdapter : TypeAdapter<Vector2i>() {
override fun write(out: JsonWriter, value: Vector2i) {