Тест аниматора, RebindableSprite и ещё приборка в GL

This commit is contained in:
DBotThePony 2023-02-21 22:49:25 +07:00
parent 0a00595520
commit 9da968695e
Signed by: DBot
GPG Key ID: DCC23B5715498507
14 changed files with 183 additions and 118 deletions

View File

@ -6,6 +6,8 @@ import org.apache.logging.log4j.LogManager
import org.lwjgl.Version
import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.Animator
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.DynamicItemDefinition
import ru.dbotthepony.kstarbound.io.BTreeDB
import ru.dbotthepony.kstarbound.world.ChunkPos
@ -189,6 +191,16 @@ fun main() {
}
// println(Starbound.statusEffects["firecharge"])
starbound.pathStack.push("/animations/dust4")
val def = starbound.gson.fromJson(starbound.locate("/animations/dust4/dust4.animation").reader(), AnimationDefinition::class.java)
starbound.pathStack.pop()
val animator = Animator(client.world!!, def)
client.onPostDrawWorld {
animator.render(client.gl.matrixStack)
}
}
//ent.position += Vector2d(y = 14.0, x = -10.0)

View File

@ -424,7 +424,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
private val entityRenderers = HashMap<Entity, EntityRenderer>()
override fun onEntityAdded(entity: Entity) {
entityRenderers[entity] = EntityRenderer.getRender(state, entity, this)
entityRenderers[entity] = EntityRenderer.getRender(world.client, entity, this)
}
override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) {

View File

@ -18,15 +18,12 @@ import ru.dbotthepony.kstarbound.client.render.Box2DRenderer
import ru.dbotthepony.kstarbound.client.render.Font
import ru.dbotthepony.kvector.api.IStruct4f
import ru.dbotthepony.kvector.matrix.Matrix4fStack
import ru.dbotthepony.kvector.matrix.nfloat.Matrix4f
import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Color
import java.io.File
import java.io.FileNotFoundException
import java.lang.ref.Cleaner
import java.util.*
import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicLong
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.math.roundToInt
@ -437,61 +434,30 @@ class GLStateTracker(val locator: ISBFileLocator) {
private val named2DTextures = HashMap<String, GLTexture2D>()
fun loadNamedTexture(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D {
return named2DTextures.computeIfAbsent(path) {
if (!locator.exists(path)) {
throw FileNotFoundException("Unable to locate $path")
}
return@computeIfAbsent newTexture(path).upload(locator.readDirect(path), memoryFormat, fileFormat).generateMips()
}
}
fun loadNamedTexture(path: String): GLTexture2D {
return named2DTextures.computeIfAbsent(path) {
if (!locator.exists(path)) {
throw FileNotFoundException("Unable to locate $path")
}
return@computeIfAbsent newTexture(path).upload(locator.readDirect(path)).generateMips()
}
}
private var loadedEmptyTexture = false
private var missingTexture: GLTexture2D? = null
private val missingTexturePath = "/assetmissing.png"
fun loadNamedTextureSafe(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D {
if (!loadedEmptyTexture) {
loadedEmptyTexture = true
named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(locator.readDirect(missingTexturePath), memoryFormat, fileFormat).generateMips()
}
fun loadTexture(path: String): GLTexture2D {
ensureSameThread()
return named2DTextures.computeIfAbsent(path) {
if (!locator.exists(path)) {
LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
return@computeIfAbsent named2DTextures[missingTexturePath]!!
}
return@computeIfAbsent newTexture(path).upload(locator.readDirect(path), memoryFormat, fileFormat).generateMips()
}
}
fun loadNamedTextureSafe(path: String): GLTexture2D {
if (!loadedEmptyTexture) {
loadedEmptyTexture = true
named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(locator.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips()
}
return named2DTextures.computeIfAbsent(path) {
if (!locator.exists(path)) {
LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
return@computeIfAbsent named2DTextures[missingTexturePath]!!
}
return@computeIfAbsent newTexture(path).upload(locator.readDirect(path)).generateMips().also {
if (missingTexture == null) {
missingTexture = newTexture(missingTexturePath).upload(locator.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips().also {
it.textureMinFilter = GL_NEAREST
it.textureMagFilter = GL_NEAREST
}
}
return named2DTextures.computeIfAbsent(path) {
if (!locator.exists(path)) {
LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
missingTexture!!
} else {
newTexture(path).upload(locator.readDirect(path)).generateMips().also {
it.textureMinFilter = GL_NEAREST
it.textureMagFilter = GL_NEAREST
}
}
}
}
fun bind(obj: VertexBufferObject): VertexBufferObject {

View File

@ -13,7 +13,7 @@ class TextureLoadingException(message: String) : Throwable(message)
data class UVCoord(val u: Float, val v: Float)
class GLTexturePropertyTracker(private val flag: Int, var value: Int) {
private class GLTexturePropertyTracker(private val flag: Int, private var value: Int) {
operator fun getValue(thisRef: GLTexture2D, property: KProperty<*>): Int {
return value
}
@ -62,8 +62,6 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
return height.toFloat() / width.toFloat()
}
private var mipsWarning = 2
var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR)
var textureMagFilter by GLTexturePropertyTracker(GL_TEXTURE_MAG_FILTER, GL_LINEAR)
@ -71,15 +69,6 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
var textureWrapT by GLTexturePropertyTracker(GL_TEXTURE_WRAP_T, GL_REPEAT)
fun bind(): GLTexture2D {
if (textureMinFilter != GL_LINEAR && textureMinFilter != GL_NEAREST) {
if (mipsWarning == 1) {
LOGGER.warn("(Likely) Trying to use texture {} before generated it's mips, this probably won't work!", this)
mipsWarning = 0
} else if (mipsWarning == 2) {
mipsWarning = 1
}
}
state.texture2D = this
return this
}
@ -87,8 +76,7 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
fun generateMips(): GLTexture2D {
state.ensureSameThread()
glGenerateTextureMipmap(pointer)
checkForGLError()
mipsWarning = 0
checkForGLError("Generating texture mipmaps")
return this
}

View File

@ -1,24 +1,51 @@
package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.ClientWorld
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kvector.matrix.Matrix4fStack
import ru.dbotthepony.kvector.vector.nfloat.Vector3f
class Animator(
val world: ClientWorld,
val def: AnimationDefinition
val def: AnimationDefinition,
val renderParams: ((String) -> String?)? = null
) {
inline val state get() = world.client.gl
val frameAnimator: FrameAnimator?
val mainSprite: RebindableSprite?
init {
if (def.frames != null && def.animationCycle != null && def.frameNumber != null) {
frameAnimator = FrameAnimator(lastFrame = def.frameNumber - 1, time = world.client.time, animationCycle = def.animationCycle)
mainSprite = RebindableSprite(world.client, def.frames, ::getRenderParam)
} else {
frameAnimator = null
mainSprite = null
}
}
fun render(stack: Matrix4fStack) {
private fun getRenderParam(name: String): String? {
if (name == "frame") {
return frameAnimator?.frameString
}
return renderParams?.invoke(name)
}
fun render(stack: Matrix4fStack) {
frameAnimator?.advance()
val sprite = mainSprite?.update() ?: return
sprite.texture.bind()
stack.push().translateWithMultiplication(Vector3f(world.client.camera.pos))
state.programs.textured.use()
state.programs.textured.transform = stack.last
state.activeTexture = 0
state.programs.textured.texture = 0
state.flat2DTexturedQuads.singleSprite(sprite.width / PIXELS_IN_STARBOUND_UNITf, sprite.height / PIXELS_IN_STARBOUND_UNITf, 0.0, sprite.transformer)
stack.pop()
}
}

View File

@ -17,12 +17,12 @@ class BoundSprite(
/**
* Настоящая ширина спрайта, в пикселях
*/
inline val width get() = sprite.width(texture.width)
val width = sprite.width(texture.width)
/**
* Настоящая высота спрайта, в пикселях
*/
inline val height get() = sprite.height(texture.height)
val height = sprite.height(texture.height)
override val u0: Float
override val v0: Float
@ -41,24 +41,3 @@ class BoundSprite(
fun bind() = texture.bind()
val transformer = QuadTransformers.uv(u0, v0, u1, v1)
}
/**
* Создаёт связку текстуры-атласа + координгат спрайта на ней
*/
fun ImageReference.bind(texture: GLTexture2D): BoundSprite {
return BoundSprite(sprite!!, texture)
}
/**
* Создаёт связку текстуры-атласа, которая загружается через [GLStateTracker.loadNamedTextureSafe] + координгат спрайта на ней
*/
fun ImageReference.bind(state: GLStateTracker): BoundSprite {
return BoundSprite(sprite!!, state.loadNamedTextureSafe(imagePath.value!!))
}
/**
* Создаёт связку текстуры-атласа + координгат спрайта на ней
*/
fun AtlasConfiguration.Sprite.bind(texture: GLTexture2D): BoundSprite {
return BoundSprite(this, texture)
}

View File

@ -32,6 +32,13 @@ class FrameAnimator(
var frame = 0
private set
/**
* Эффективное преобразование [frame] в строку
*/
val frameString: String get() {
return framenames.getOrNull(frame) ?: frame.toString()
}
private var counter = 0.0
private var lastRender = time.seconds
@ -73,4 +80,14 @@ class FrameAnimator(
lastRender = time.seconds
}
}
companion object {
private val framenames = ArrayList<String>()
init {
for (i in 0 .. 500) {
framenames.add(i.toString())
}
}
}
}

View File

@ -0,0 +1,50 @@
package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.defs.image.ImageReference
class RebindableSprite(
val client: StarboundClient,
ref: ImageReference,
val renderParams: ((String) -> String?)? = null
) {
var sprite: BoundSprite? = null
private set
var ref: ImageReference = ref
private set
init {
val unbound = ref.sprite
if (unbound != null) {
sprite = BoundSprite(unbound, client.gl.loadTexture(ref.imagePath.value!!))
}
}
/**
* Обновляет [ref] и [sprite] по значениям, которые выдаст [renderParams]
*
* Возвращает новое значение [sprite]
*/
fun update(): BoundSprite? {
client.gl.ensureSameThread()
if (renderParams == null)
return sprite
val newRef = ref.with(renderParams)
if (newRef !== ref) {
ref = newRef
sprite = null
val unbound = newRef.sprite
if (unbound != null) {
sprite = BoundSprite(unbound, client.gl.loadTexture(newRef.imagePath.value!!))
}
}
return sprite
}
}

View File

@ -186,7 +186,7 @@ private class ModifierEqualityTester(val definition: MaterialModifier) : Equalit
class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
val state get() = renderers.state
val texture = state.loadNamedTexture(def.renderParameters.texture.imagePath.value!!).also {
val texture = state.loadTexture(def.renderParameters.texture.imagePath.value!!).also {
it.textureMagFilter = GL_NEAREST
}
@ -255,9 +255,9 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
for (renderPiece in matchPiece.pieces) {
if (renderPiece.piece.texture != null) {
val program = if (background) {
renderers.background(state.loadNamedTexture(renderPiece.piece.texture!!))
renderers.background(state.loadTexture(renderPiece.piece.texture!!))
} else {
renderers.foreground(state.loadNamedTexture(renderPiece.piece.texture!!))
renderers.foreground(state.loadTexture(renderPiece.piece.texture!!))
}
tesselateAt(self, renderPiece.piece, getter, layers.computeIfAbsent(program, def.renderParameters.zLevel, ::vertexTextureBuilder), pos, renderPiece.offset, isModifier)

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.client.render.entity
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import ru.dbotthepony.kstarbound.client.ClientChunk
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.matrix.Matrix4fStack
@ -13,7 +14,8 @@ import java.io.Closeable
*
* Считается, что процесс отрисовки ограничен лишь одним слоем (т.е. отрисовка происходит в один проход)
*/
open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open var chunk: ClientChunk?) : Closeable {
open class EntityRenderer(val client: StarboundClient, val entity: Entity, open var chunk: ClientChunk?) : Closeable {
inline val state: GLStateTracker get() = client.gl
open val renderPos: Vector2d get() = entity.position
open fun render(stack: Matrix4fStack) {
@ -39,20 +41,20 @@ open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open va
*/
const val Z_LEVEL_ENTITIES = 30000
private val renderers = Reference2ObjectOpenHashMap<Class<*>, (state: GLStateTracker, entity: Entity, chunk: ClientChunk?) -> EntityRenderer>()
private val renderers = Reference2ObjectOpenHashMap<Class<*>, (client: StarboundClient, entity: Entity, chunk: ClientChunk?) -> EntityRenderer>()
@Suppress("unchecked_cast")
fun <T : Entity> registerRenderer(clazz: Class<T>, renderer: (state: GLStateTracker, entity: T, chunk: ClientChunk?) -> EntityRenderer) {
check(renderers.put(clazz, renderer as (state: GLStateTracker, entity: Entity, chunk: ClientChunk?) -> EntityRenderer) == null) { "Already has renderer for ${clazz.canonicalName}!" }
fun <T : Entity> registerRenderer(clazz: Class<T>, renderer: (client: StarboundClient, entity: T, chunk: ClientChunk?) -> EntityRenderer) {
check(renderers.put(clazz, renderer as (client: StarboundClient, entity: Entity, chunk: ClientChunk?) -> EntityRenderer) == null) { "Already has renderer for ${clazz.canonicalName}!" }
}
inline fun <reified T : Entity> registerRenderer(noinline renderer: (state: GLStateTracker, entity: T, chunk: ClientChunk?) -> EntityRenderer) {
inline fun <reified T : Entity> registerRenderer(noinline renderer: (client: StarboundClient, entity: T, chunk: ClientChunk?) -> EntityRenderer) {
registerRenderer(T::class.java, renderer)
}
fun getRender(state: GLStateTracker, entity: Entity, chunk: ClientChunk? = null): EntityRenderer {
val factory = renderers[entity::class.java] ?: return EntityRenderer(state, entity, chunk)
return factory.invoke(state, entity, chunk)
fun getRender(client: StarboundClient, entity: Entity, chunk: ClientChunk? = null): EntityRenderer {
val factory = renderers[entity::class.java] ?: return EntityRenderer(client, entity, chunk)
return factory.invoke(client, entity, chunk)
}
init {

View File

@ -2,14 +2,14 @@ package ru.dbotthepony.kstarbound.client.render.entity
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.ClientChunk
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.render.bind
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.RebindableSprite
import ru.dbotthepony.kstarbound.world.entities.ItemEntity
import ru.dbotthepony.kvector.matrix.Matrix4fStack
class ItemRenderer(state: GLStateTracker, entity: ItemEntity, chunk: ClientChunk?) : EntityRenderer(state, entity, chunk) {
class ItemRenderer(client: StarboundClient, entity: ItemEntity, chunk: ClientChunk?) : EntityRenderer(client, entity, chunk) {
private val def = entity.def
private val textures = def.inventoryIcon?.stream()?.map { it.image.bind(state) }?.toList() ?: listOf()
private val textures = def.inventoryIcon?.stream()?.map { RebindableSprite(client, it.image) }?.toList() ?: listOf()
override fun render(stack: Matrix4fStack) {
if (textures.isEmpty())
@ -20,10 +20,10 @@ class ItemRenderer(state: GLStateTracker, entity: ItemEntity, chunk: ClientChunk
state.activeTexture = 0
state.programs.textured.texture = 0
for (texture in textures) {
texture.bind()
state.flat2DTexturedQuads.singleSprite(texture.width / PIXELS_IN_STARBOUND_UNITf, texture.height / PIXELS_IN_STARBOUND_UNITf, entity.movement.angle, texture.transformer)
for (unbound in textures) {
val sprite = unbound.update() ?: continue
sprite.texture.bind()
state.flat2DTexturedQuads.singleSprite(sprite.width / PIXELS_IN_STARBOUND_UNITf, sprite.height / PIXELS_IN_STARBOUND_UNITf, entity.movement.angle, sprite.transformer)
}
}
}

View File

@ -58,16 +58,25 @@ class AtlasConfiguration private constructor(
}
}
/**
* @return [Sprite] если он существует с данным [name], или `null`
*/
operator fun get(name: String): Sprite? {
return sprites[name]
}
/**
* @return [Sprite] если он существует с данным [name], или `null`
*/
operator fun get(name: Int): Sprite? {
return intIndexed[name]
}
/**
* @return [Sprite] если он существует с данным [name], или первый спрайт в атласе
*/
fun any(name: String): Sprite {
return get(name) ?: first
return get(name) ?: any()
}
private val any by lazy { get("root") ?: get("0") ?: get("default") ?: first }

View File

@ -13,7 +13,7 @@ import ru.dbotthepony.kstarbound.util.PathStack
import ru.dbotthepony.kstarbound.util.SBPattern
/**
* @see [AtlasConfiguration.Companion.get]
* @see [AtlasConfiguration.Registry.get]
*/
class ImageReference private constructor(
val raw: DirectAssetReference,
@ -26,8 +26,9 @@ class ImageReference private constructor(
* Спрайт, на которое ссылается данный референс, или `null` если:
* * [atlas] равен `null`
* * [spritePath] является шаблоном и определены не все значения
* * [spritePath] не является правильным именем спрайта внутри [atlas] (смотрим [AtlasConfiguration.get])
*/
val sprite by lazy {
val sprite by lazy(LazyThreadSafetyMode.NONE) {
if (atlas == null)
null
else if (spritePath == null)
@ -38,7 +39,15 @@ class ImageReference private constructor(
fun with(values: (String) -> String?): ImageReference {
val imagePath = this.imagePath.with(values)
val spritePath = this.spritePath?.with(values)
var spritePath = this.spritePath?.with(values)
if (spritePath == null) {
val frame = values.invoke("frame")
if (frame != null) {
spritePath = SBPattern.FRAME.with(values)
}
}
if (imagePath != this.imagePath || spritePath != this.spritePath) {
if (imagePath != this.imagePath) {

View File

@ -52,8 +52,11 @@ class SBPattern private constructor(
}
fun resolve(values: (String) -> String?): String? {
if (names.isEmpty())
if (names.isEmpty()) {
return raw
} else if (pieces.size == 1) {
return pieces[0].resolve(values, params::get)
}
val buffer = ArrayList<String>(pieces.size)
@ -116,6 +119,9 @@ class SBPattern private constructor(
@JvmField
val EMPTY = raw("")
@JvmField
val FRAME = of("<frame>")
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == SBPattern::class.java) {
return object : TypeAdapter<SBPattern>() {
@ -126,7 +132,7 @@ class SBPattern private constructor(
}
override fun read(`in`: JsonReader): SBPattern? {
return interner.intern(of(strings.read(`in`) ?: return null))
return of(strings.read(`in`) ?: return null)
}
} as TypeAdapter<T>
}
@ -159,7 +165,7 @@ class SBPattern private constructor(
throw IllegalArgumentException("Malformed pattern string: $raw")
}
pieces.add(Piece(name = raw.substring(open + 1, closing - 1)))
pieces.add(Piece(name = raw.substring(open + 1, closing)))
i = closing + 1
}
}