ImageReference, SpriteAnimator и почти избавились от старого FrameGrid

This commit is contained in:
DBotThePony 2023-01-01 18:07:46 +07:00
parent 9357835f4e
commit 69a5061e9e
Signed by: DBot
GPG Key ID: DCC23B5715498507
11 changed files with 207 additions and 59 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.AtlasConfiguration
import ru.dbotthepony.kstarbound.defs.animation.SpriteReference
import ru.dbotthepony.kstarbound.defs.item.ItemDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemRarity
@ -65,6 +66,9 @@ object Starbound {
private set
fun readingFolderTransformer(input: String): String {
val readingFolder = readingFolder
require(readingFolder != null) { "Not reading an asset on current thread" }
if (input[0] == '/')
return input
@ -72,6 +76,8 @@ object Starbound {
}
fun readingFolderTransformerNullable(input: String?): String? {
require(readingFolder != null) { "Not reading an asset on current thread" }
if (input != null)
return readingFolderTransformer(input)
@ -158,6 +164,7 @@ object Starbound {
.also(ItemDefinition::registerGson)
.also(ItemRarity::registerGson)
.also(SpriteReference::registerGson)
.also(AtlasConfiguration::registerGson)
.registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe())
@ -371,6 +378,7 @@ object Starbound {
try {
callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory()
val def = gson.fromJson(listedFile.reader(), ConfigurableProjectile::class.java).assemble(listedFile.computeDirectory())
check(projectiles[def.projectileName] == null) { "Already has projectile with ID ${def.projectileName} loaded!" }
projectiles[def.projectileName] = def
@ -384,6 +392,8 @@ object Starbound {
}
}
}
readingFolder = null
}
private fun loadFunctions(callback: (String) -> Unit) {
@ -392,6 +402,7 @@ object Starbound {
try {
callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory()
val readObject = JsonParser.parseReader(listedFile.reader()) as JsonObject
for (key in readObject.keySet()) {
@ -407,6 +418,8 @@ object Starbound {
}
}
}
readingFolder = null
}
private fun loadParallax(callback: (String) -> Unit) {
@ -415,6 +428,7 @@ object Starbound {
try {
callback("Loading $listedFile")
readingFolder = listedFile.computeDirectory()
val def = gson.fromJson(listedFile.reader(), ParallaxPrototype::class.java)
parallax[listedFile.name.substringBefore('.')] = def
} catch(err: Throwable) {
@ -426,6 +440,8 @@ object Starbound {
}
}
}
readingFolder = null
}
private fun loadMaterialModifiers(callback: (String) -> Unit) {
@ -478,6 +494,8 @@ object Starbound {
}
}
}
readingFolder = null
}
private fun loadItemDefinitions(callback: (String) -> Unit) {
@ -502,5 +520,7 @@ object Starbound {
}
}
}
readingFolder = null
}
}

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.gl.GLTexture2D
import ru.dbotthepony.kstarbound.client.gl.vertex.QuadTransformers
import ru.dbotthepony.kstarbound.defs.animation.AtlasConfiguration
import ru.dbotthepony.kstarbound.defs.animation.IUVCoordinates
import ru.dbotthepony.kstarbound.defs.animation.SpriteReference
@ -10,20 +11,18 @@ import ru.dbotthepony.kstarbound.defs.animation.SpriteReference
* Связка текстуры-атласа + координат спрайта на ней
*/
class BoundSprite(
val reference: SpriteReference,
val sprite: AtlasConfiguration.Sprite,
val texture: GLTexture2D
) : IUVCoordinates {
inline val sprite get() = reference.sprite
/**
* Настоящая ширина спрайта, в пикселях
*/
inline val width get() = reference.sprite.width(texture.width)
inline val width get() = sprite.width(texture.width)
/**
* Настоящая высота спрайта, в пикселях
*/
inline val height get() = reference.sprite.height(texture.height)
inline val height get() = sprite.height(texture.height)
override val u0: Float
override val v0: Float
@ -31,7 +30,7 @@ class BoundSprite(
override val v1: Float
init {
val coords = reference.sprite.compute(texture)
val coords = sprite.compute(texture)
this.u0 = coords.u0
this.v0 = coords.v0
@ -47,12 +46,19 @@ class BoundSprite(
* Создаёт связку текстуры-атласа + координгат спрайта на ней
*/
fun SpriteReference.bind(texture: GLTexture2D): BoundSprite {
return BoundSprite(this, texture)
return BoundSprite(sprite, texture)
}
/**
* Создаёт связку текстуры-атласа, которая загружается через [GLStateTracker.loadNamedTextureSafe] + координгат спрайта на ней
*/
fun SpriteReference.bind(state: GLStateTracker): BoundSprite {
return BoundSprite(this, state.loadNamedTextureSafe(path))
return BoundSprite(sprite, state.loadNamedTextureSafe(image))
}
/**
* Создаёт связку текстуры-атласа + координгат спрайта на ней
*/
fun AtlasConfiguration.Sprite.bind(texture: GLTexture2D): BoundSprite {
return BoundSprite(this, texture)
}

View File

@ -1,13 +1,20 @@
package ru.dbotthepony.kstarbound.client.render
import org.lwjgl.glfw.GLFW.glfwGetTime
import ru.dbotthepony.kstarbound.defs.FrameSet
/**
* Анимирует заданный FrameSet
* Таймер для анимирования набора спрайтов
*/
class FrameSetAnimator(
val set: FrameSet,
open class FrameAnimator(
/**
* Первый кадр в анимации
*/
var firstFrame: Int = 0,
/**
* Последний кадр в анимации
*/
var lastFrame: Int,
/**
* Сколько времени занимает один кадр
@ -17,23 +24,11 @@ class FrameSetAnimator(
/**
* Зациклить ли анимацию
*/
var animationLoops: Boolean,
var animationLoops: Boolean = true,
) {
/**
* Последний кадр анимации
*/
var lastFrame = set.frameCount - 1
/**
* Первый кадр анимации
*/
var firstFrame = 0
var frame = 0
private set
val frameObj get() = set.frames[frame + firstFrame]
/**
* Возвращает разницу между последним и первым кадром анимации
*/

View File

@ -0,0 +1,56 @@
package ru.dbotthepony.kstarbound.client.render
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.gl.GLTexture2D
import ru.dbotthepony.kstarbound.defs.animation.AtlasConfiguration
import ru.dbotthepony.kstarbound.defs.animation.ImageReference
class SpriteAnimator(
val sprites: List<BoundSprite>,
animationCycle: Double,
animationLoops: Boolean = true,
firstFrame: Int = 0,
lastFrame: Int = sprites.size - 1
) : FrameAnimator(
firstFrame = firstFrame,
lastFrame = lastFrame,
animationCycle = animationCycle,
animationLoops = animationLoops,
) {
constructor(
image: GLTexture2D,
atlas: AtlasConfiguration,
animationCycle: Double,
animationLoops: Boolean = true,
firstFrame: Int = 0,
lastFrame: Int = atlas.spriteList.size - 1
) : this(
atlas.spriteList.stream().map { it.bind(image) }.collect(ImmutableList.toImmutableList()),
animationCycle = animationCycle,
animationLoops = animationLoops,
firstFrame = firstFrame,
lastFrame = lastFrame,
)
constructor(
image: ImageReference,
state: GLStateTracker,
animationCycle: Double,
animationLoops: Boolean = true,
firstFrame: Int = 0,
lastFrame: Int = image.config.spriteList.size - 1
) : this(state.loadNamedTextureSafe(image.image), image.config, animationCycle, animationLoops, firstFrame, lastFrame)
val sprite get() = sprites[frame]
}
fun ImageReference.makeSpriteAnimator(
state: GLStateTracker,
animationCycle: Double,
animationLoops: Boolean = true,
firstFrame: Int = 0,
lastFrame: Int = config.spriteList.size - 1
): SpriteAnimator {
return SpriteAnimator(this, state, animationCycle, animationLoops, firstFrame, lastFrame)
}

View File

@ -5,39 +5,36 @@ import ru.dbotthepony.kstarbound.client.ClientChunk
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.gl.vertex.QuadTransformers
import ru.dbotthepony.kstarbound.client.gl.vertex.quadRotatedZ
import ru.dbotthepony.kstarbound.client.render.FrameSetAnimator
import ru.dbotthepony.kstarbound.client.render.FrameAnimator
import ru.dbotthepony.kstarbound.client.render.SpriteAnimator
import ru.dbotthepony.kstarbound.client.render.makeSpriteAnimator
import ru.dbotthepony.kstarbound.world.entities.projectile.Projectile
import ru.dbotthepony.kvector.matrix.Matrix4fStack
open class ProjectileRenderer(state: GLStateTracker, entity: Projectile, chunk: ClientChunk?) : EntityRenderer(state, entity, chunk) {
private val def = entity.def
private val texture = state.loadNamedTextureSafe(def.image.texture)
private val animator = FrameSetAnimator(def.image, def.animationCycle, entity.def.animationLoops)
private val animator = def.image.makeSpriteAnimator(state, def.animationCycle, def.animationLoops)
override fun render(stack: Matrix4fStack) {
state.shaderVertexTexture.use()
state.shaderVertexTexture.transform.set(stack.last)
state.activeTexture = 0
state.shaderVertexTexture["_texture"] = 0
texture.bind()
animator.advance()
val sprite = animator.sprite
sprite.texture.bind()
val builder = state.flat2DTexturedQuads.small
builder.begin()
val (u0, v0) = texture.pixelToUV(animator.frameObj.texturePosition)
val (u1, v1) = texture.pixelToUV(animator.frameObj.textureEndPosition)
val width = (sprite.width / PIXELS_IN_STARBOUND_UNITf) / 2f
val height = (sprite.height / PIXELS_IN_STARBOUND_UNITf) / 2f
val width = (animator.frameObj.width / PIXELS_IN_STARBOUND_UNITf) / 2f
val height = (animator.frameObj.height / PIXELS_IN_STARBOUND_UNITf) / 2f
builder.quadRotatedZ(-width, -height, width, height, 5f, 0f, 0f, entity.movement.angle,
QuadTransformers.uv(u0, v0, u1, v1)
)
builder.quadRotatedZ(-width, -height, width, height, 5f, 0f, 0f, entity.movement.angle, sprite.transformer)
builder.upload()
builder.draw()
}
}
}

View File

@ -2,15 +2,19 @@ package ru.dbotthepony.kstarbound.defs.animation
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
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.kstarbound.io.json.transform
import ru.dbotthepony.kstarbound.registerTypeAdapter
import ru.dbotthepony.kvector.vector.nint.Vector2i
import ru.dbotthepony.kvector.vector.nint.Vector4i
import java.util.concurrent.ConcurrentHashMap
@ -22,7 +26,7 @@ import java.util.concurrent.ConcurrentHashMap
*
* Несмотря на название, НЕ ЯВЛЯЕТСЯ изображением, а лишь метаданными для *множества* возможных изображений
*/
class AtlasDefinition private constructor(
class AtlasConfiguration private constructor(
/**
* Имя данного атласа (путь к файлу)
*/
@ -149,14 +153,14 @@ class AtlasDefinition private constructor(
}
companion object {
val EMPTY: AtlasDefinition
val EMPTY: AtlasConfiguration
init {
val sprite = Sprite("root", Vector4i(0, 0, 0, 0))
EMPTY = AtlasDefinition("null", ImmutableMap.of("root", sprite, "default", sprite, "0", sprite), ImmutableList.of(sprite))
EMPTY = AtlasConfiguration("null", ImmutableMap.of("root", sprite, "default", sprite, "0", sprite), ImmutableList.of(sprite))
}
private val cache = ConcurrentHashMap<String, AtlasDefinition>()
private val cache = ConcurrentHashMap<String, AtlasConfiguration>()
private fun generateFakeNames(dimensions: Vector2i): JsonArray {
return JsonArray(dimensions.y).also {
@ -175,7 +179,7 @@ class AtlasDefinition private constructor(
}
}
private fun parseFrames(input: JsonReader, name: String): AtlasDefinition {
private fun parseFrames(input: JsonReader, name: String): AtlasConfiguration {
val read = TypeAdapters.JSON_ELEMENT.read(input)
if (read !is JsonObject) {
@ -241,10 +245,10 @@ class AtlasDefinition private constructor(
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), spriteList)
return AtlasConfiguration(name, ImmutableMap.copyOf(sprites), spriteList)
}
private fun recursiveGet(name: String, folder: String): AtlasDefinition? {
private fun recursiveGet(name: String, folder: String): AtlasConfiguration? {
var current = folder
while (current != "/" && current != "") {
@ -272,10 +276,24 @@ class AtlasDefinition private constructor(
return null
}
fun get(path: String): AtlasDefinition {
/**
* Пытается найти конфигурацию (файл "frames") атласа для заданного пути (заданного изображения/атласа)
*
* Алгоритм поиска конфигурации таков:
* * Проверяется папка с целевым файлом на наличие файла с таким же именем, но с расширением frames;
* * Если файл не был найден, процесс повторяется переходом в папку выше, пока не будет достигнут корень файловой системы;
* * Если файл не был найден, процесс начинается заново, внутри целевой папки, но уже происходит поиск файла с именем default.frames;
* * Процесс повторяется как в пункте 2 пока не будет найден файл в одной из вышестоящих папок.
*
* Если ни один файл не был найден, то возвращается [EMPTY]
*
* Данная функция кеширует результаты поиска
*
*/
fun get(path: String): AtlasConfiguration {
require(path[0] == '/') { "$path is not an absolute path" }
val folder = path.substringBeforeLast('/').lowercase()
val filename = path.substringAfterLast('/').substringBefore('.').lowercase()
val filename = path.substringAfterLast('/').substringBefore(':').substringBefore('.').lowercase()
val direct = recursiveGet(filename, folder)
if (direct != null) return direct
@ -285,5 +303,11 @@ class AtlasDefinition private constructor(
return EMPTY
}
val ADAPTER: TypeAdapter<AtlasConfiguration?> = Starbound.stringTypeAdapter.transform(read = read@{ get(it ?: return@read it as AtlasConfiguration?) }, write = write@{ it?.name })
fun registerGson(gsonBuilder: GsonBuilder) {
gsonBuilder.registerTypeAdapter(ADAPTER)
}
}
}

View File

@ -0,0 +1,45 @@
package ru.dbotthepony.kstarbound.defs.animation
import com.google.gson.JsonSyntaxException
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
/**
* Хранит данные (пару) вида "/example/animated.png" у которого, вероятнее всего, есть "/example/animated.frames"
*
* @see [AtlasConfiguration.Companion.get]
*/
data class ImageReference(
val image: String,
val config: AtlasConfiguration,
) {
/**
* Вызывает [AtlasConfiguration.Companion.get] автоматически
*
* @see ImageReference
*/
constructor(image: String) : this(image, AtlasConfiguration.get(image))
companion object : TypeAdapter<ImageReference>() {
override fun write(out: JsonWriter, value: ImageReference) {
out.value(value.image)
}
override fun read(`in`: JsonReader): ImageReference {
if (`in`.peek() == JsonToken.STRING) {
val image = Starbound.readingFolderTransformer(`in`.nextString())
if (image.contains(':')) {
throw JsonSyntaxException("Expected atlas/image reference, but got sprite reference: $image")
}
return ImageReference(image, AtlasConfiguration.get(image))
}
throw JsonSyntaxException("Expected atlas/image reference, but got: ${`in`.peek()}")
}
}
}

View File

@ -7,13 +7,16 @@ import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.registerTypeAdapter
/**
* Хранит данные (пару) вида "/example/image.png:sprite.name"
*/
data class SpriteReference(
val path: String,
val sprite: AtlasDefinition.Sprite
val image: String,
val sprite: AtlasConfiguration.Sprite
) {
companion object : TypeAdapter<SpriteReference>() {
fun parse(input: String): SpriteReference {
val grid = AtlasDefinition.get(input.substringBefore(':'))
val grid = AtlasConfiguration.get(input.substringBefore(':'))
return when (input.count { it == ':' }) {
0 -> SpriteReference(input, grid.any())
@ -23,7 +26,7 @@ data class SpriteReference(
}
override fun write(out: JsonWriter, value: SpriteReference) {
out.value(value.path + ":" + value.sprite.name)
out.value(value.image + ":" + value.sprite.name)
}
override fun read(`in`: JsonReader): SpriteReference {

View File

@ -7,6 +7,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.defs.animation.ImageReference
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
import ru.dbotthepony.kstarbound.registerTypeAdapter
@ -79,7 +80,7 @@ class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredPr
lightColor = lightColor,
onlyHitTerrain = onlyHitTerrain,
orientationLocked = orientationLocked,
image = IFrameGrid.loadFrameStrip(ensureAbsolutePath(requireNotNull(image) { "image is null" }, directory), weak = true),
image = ImageReference(Starbound.readingFolderTransformer(requireNotNull(image) { "image is null" })),
timeToLive = timeToLive,
animationCycle = animationCycle,
bounces = bounces,

View File

@ -6,6 +6,7 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssembledPrototype
import ru.dbotthepony.kstarbound.defs.DamageType
import ru.dbotthepony.kstarbound.defs.FrameSet
import ru.dbotthepony.kstarbound.defs.animation.ImageReference
import ru.dbotthepony.kstarbound.world.entities.projectile.AbstractProjectileMovementController
import ru.dbotthepony.kstarbound.world.entities.projectile.Projectile
import ru.dbotthepony.kvector.vector.Color
@ -21,7 +22,7 @@ class ConfiguredProjectile(
val lightColor: Color?,
val onlyHitTerrain: Boolean,
val orientationLocked: Boolean,
val image: FrameSet,
val image: ImageReference,
val timeToLive: Double,
val animationCycle: Double,
val bounces: Int,

View File

@ -29,14 +29,14 @@ fun <T> TypeAdapter<T>.transformWrite(transformer: (T) -> T): TypeAdapter<T> {
}
}
fun <T> TypeAdapter<T>.transform(transformRead: (T) -> T, transformWrite: (T) -> T): TypeAdapter<T> {
return object : TypeAdapter<T>() {
override fun write(out: JsonWriter, value: T) {
return this@transform.write(out, transformWrite(value))
fun <In, Out> TypeAdapter<In>.transform(read: (In) -> Out, write: (Out) -> In): TypeAdapter<Out> {
return object : TypeAdapter<Out>() {
override fun write(out: JsonWriter, value: Out) {
return this@transform.write(out, write(value))
}
override fun read(`in`: JsonReader): T {
return transformRead(this@transform.read(`in`))
override fun read(`in`: JsonReader): Out {
return read(this@transform.read(`in`))
}
}
}