Parallax definition loading

This commit is contained in:
DBotThePony 2022-02-23 20:01:02 +07:00
parent 97e28ca0ee
commit b1ee5bf66d
Signed by: DBot
GPG Key ID: DCC23B5715498507
7 changed files with 339 additions and 44 deletions

View File

@ -4,11 +4,11 @@ import com.google.gson.*
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.api.IVFS
import ru.dbotthepony.kstarbound.api.PhysicalFS
import ru.dbotthepony.kstarbound.api.getPathFilename
import ru.dbotthepony.kstarbound.api.getPathFolder
import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.defs.projectile.*
import ru.dbotthepony.kstarbound.defs.world.SkyParameters
import ru.dbotthepony.kstarbound.defs.world.SkyType
import ru.dbotthepony.kstarbound.defs.world.dungeon.DungeonWorldDef
import ru.dbotthepony.kstarbound.io.*
import ru.dbotthepony.kstarbound.math.*
@ -25,8 +25,8 @@ import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
const val METRES_IN_STARBOUND_UNIT = 0.25
const val METRES_IN_STARBOUND_UNITf = 0.25f
const val METRES_IN_STARBOUND_UNIT = 0.5
const val METRES_IN_STARBOUND_UNITf = 0.5f
const val PIXELS_IN_STARBOUND_UNIT = 8.0
const val PIXELS_IN_STARBOUND_UNITf = 8.0f
@ -36,12 +36,16 @@ class ProjectileDefLoadingException(message: String, cause: Throwable? = null) :
object Starbound : IVFS {
private val LOGGER = LogManager.getLogger()
private val tiles = HashMap<String, TileDefinition>()
private val projectiles = HashMap<String, ConfiguredProjectile>()
val tilesAccess = Collections.unmodifiableMap(tiles)
val projectilesAccess = Collections.unmodifiableMap(projectiles)
private val parallax = HashMap<String, Parallax>()
val gson = GsonBuilder()
val tilesAccess: Map<String, TileDefinition> = Collections.unmodifiableMap(tiles)
val projectilesAccess: Map<String, ConfiguredProjectile> = Collections.unmodifiableMap(projectiles)
val parallaxAccess: Map<String, Parallax> = Collections.unmodifiableMap(parallax)
val gson: Gson = GsonBuilder()
.enableComplexMapKeySerialization()
.serializeNulls()
.setDateFormat(DateFormat.LONG)
@ -60,6 +64,7 @@ object Starbound : IVFS {
.also(ConfigurableProjectile::registerGson)
.also(SkyParameters::registerGson)
.also(DungeonWorldDef::registerGson)
.also(Parallax::registerGson)
.registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe())
@ -99,6 +104,25 @@ object Starbound : IVFS {
fun getTileDefinition(name: String) = tiles[name]
private val initCallbacks = ArrayList<() -> Unit>()
private fun loadStage(
callback: (Boolean, Boolean, String) -> Unit,
loader: ((String) -> Unit) -> Unit,
name: String,
) {
val time = System.currentTimeMillis()
callback(false, false, "Loading $name...")
loader {
if (terminateLoading) {
throw InterruptedException("Game is terminating")
}
callback(false, true, it)
}
callback(false, true, "Loaded $name in ${System.currentTimeMillis() - time}ms")
}
fun initializeGame(callback: (finished: Boolean, replaceStatus: Boolean, status: String) -> Unit) {
if (initializing) {
throw IllegalStateException("Already initializing!")
@ -125,36 +149,9 @@ object Starbound : IVFS {
}
}
run {
val localTime = System.currentTimeMillis()
callback(false, false, "Loading materials...")
loadTileMaterials {
if (terminateLoading) {
throw InterruptedException("Game is terminating")
}
callback(false, true, it)
}
callback(false, true, "Loaded materials in ${System.currentTimeMillis() - localTime}ms")
}
run {
val localTime = System.currentTimeMillis()
callback(false, false, "Loading projectiles...")
loadProjectiles {
if (terminateLoading) {
throw InterruptedException("Game is terminating")
}
callback(false, true, it)
}
callback(false, true, "Loaded Projectiles in ${System.currentTimeMillis() - localTime}ms")
}
loadStage(callback, this::loadTileMaterials, "materials")
loadStage(callback, this::loadProjectiles, "projectiles")
loadStage(callback, this::loadParallax, "parallax definitions")
initializing = false
initialized = true
@ -257,4 +254,21 @@ object Starbound : IVFS {
}
}
}
private fun loadParallax(callback: (String) -> Unit) {
for (fs in fileSystems) {
for (listedFile in fs.listAllFiles("parallax")) {
if (listedFile.endsWith(".parallax")) {
try {
callback("Loading $listedFile")
val def = gson.fromJson(getReader(listedFile), Parallax::class.java)
parallax[getPathFilename(listedFile).substringBefore('.')] = def
} catch(err: Throwable) {
LOGGER.error("Loading parallax file $listedFile", err)
}
}
}
}
}
}

View File

@ -92,7 +92,11 @@ interface IVFS {
}
fun getPathFolder(path: String): String {
return path.substring(0, path.lastIndexOf('/'))
return path.substringBeforeLast('/')
}
fun getPathFilename(path: String): String {
return path.substringAfterLast('/')
}
class PhysicalFS(root: File) : IVFS {

View File

@ -1,11 +1,39 @@
package ru.dbotthepony.kstarbound.client
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.gl.VertexTransformers
import ru.dbotthepony.kstarbound.client.render.renderLayeredList
import ru.dbotthepony.kstarbound.defs.Parallax
import ru.dbotthepony.kstarbound.math.encasingChunkPosAABB
import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.util2d.AABB
class DoubleEdgeProgression : Iterator<Int> {
var value = 0
override fun hasNext(): Boolean {
return true
}
override fun next(): Int {
return nextInt()
}
fun nextInt(): Int {
return if (value > 0) {
val ret = value
value = -value
ret
} else {
val ret = value
value = -value + 1
ret
}
}
}
class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWorld, ClientChunk>(seed) {
init {
physics.debugDraw = client.gl.box2dRenderer
@ -18,17 +46,77 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWo
)
}
var parallax: Parallax? = null
/**
* Отрисовывает этот с обрезкой невидимой геометрии с точки зрения [size] в Starbound Units
*
* Все координаты "местности" сохраняются, поэтому, если отрисовывать слишком далеко от 0, 0
* то геометрия может начать искажаться из-за погрешности плавающей запятой
*
* Обрезает всю заведомо невидимую геометрию на основе аргументов mins и maxs (в пикселях)
*/
fun render(
size: AABB,
) {
val parallax = parallax
if (parallax != null) {
client.gl.matrixStack.push()
client.gl.matrixStack.translateWithMultiplication(y = parallax.verticalOrigin.toFloat() - 20f)
client.gl.shaderVertexTexture.use()
val stateful = client.gl.flat2DTexturedQuads.statefulSmall
val builder = stateful.builder
client.gl.activeTexture = 0
client.gl.shaderVertexTexture["_texture"] = 0
val centre = size.centre
for (layer in parallax.layers) {
client.gl.matrixStack.push()
client.gl.matrixStack.translateWithMultiplication(x = layer.offset.x.toFloat() / PIXELS_IN_STARBOUND_UNITf, y = layer.offset.y.toFloat() / PIXELS_IN_STARBOUND_UNITf)
client.gl.shaderVertexTexture.transform.set(client.gl.matrixStack.last)
val texture = client.gl.loadNamedTextureSafe("/parallax/images/${layer.kind}/base/1.png")
texture.bind()
texture.textureMagFilter = GL_NEAREST
texture.textureMinFilter = GL_NEAREST
builder.begin()
for (xPos in DoubleEdgeProgression()) {
var x0 = xPos.toFloat() * texture.width / PIXELS_IN_STARBOUND_UNITf
var x1 = (xPos + 1f) * texture.width / PIXELS_IN_STARBOUND_UNITf
val diffx = layer.parallax.x * centre.x - centre.x
val diffy = (layer.parallax.y * (centre.y + 20.0) - centre.y - 20.0).toFloat() / PIXELS_IN_STARBOUND_UNITf
x0 += diffx.toFloat() / PIXELS_IN_STARBOUND_UNITf
x1 += diffx.toFloat() / PIXELS_IN_STARBOUND_UNITf
builder.quadZ(x0, diffy, x1, diffy + texture.height.toFloat() / PIXELS_IN_STARBOUND_UNITf, 1f, VertexTransformers.uv(0f, 1f, 1f, 0f))
/*if (x1 < size.mins.x) {
break
}*/
if (xPos < -40) {
break
}
}
stateful.upload()
stateful.draw()
client.gl.matrixStack.pop()
}
client.gl.matrixStack.pop()
}
val determineRenderers = ArrayList<ClientChunk>()
for (chunk in collectInternal(size.encasingChunkPosAABB())) {

View File

@ -216,9 +216,9 @@ class StarboundClient : AutoCloseable {
world?.render(
AABB.rectangle(
camera.pos.toDoubleVector(),
viewportWidth / settings.scale / PIXELS_IN_STARBOUND_UNIT,
viewportHeight / settings.scale / PIXELS_IN_STARBOUND_UNIT))
camera.pos.toDoubleVector(),
viewportWidth / settings.scale / PIXELS_IN_STARBOUND_UNIT,
viewportHeight / settings.scale / PIXELS_IN_STARBOUND_UNIT))
for (lambda in onPostDrawWorld) {
lambda.invoke()

View File

@ -252,7 +252,7 @@ class GLStateTracker {
private val named2DTextures = HashMap<String, GLTexture2D>()
fun loadNamedTexture(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D {
fun loadNamedTexture(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D {
return named2DTextures.computeIfAbsent(path) {
if (!Starbound.pathExists(path)) {
throw FileNotFoundException("Unable to locate $path")
@ -262,10 +262,20 @@ class GLStateTracker {
}
}
fun loadNamedTexture(path: String): GLTexture2D {
return named2DTextures.computeIfAbsent(path) {
if (!Starbound.pathExists(path)) {
throw FileNotFoundException("Unable to locate $path")
}
return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path)).generateMips()
}
}
private var loadedEmptyTexture = false
private val missingTexturePath = "/assetmissing.png"
fun loadNamedTextureSafe(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D {
fun loadNamedTextureSafe(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D {
if (!loadedEmptyTexture) {
loadedEmptyTexture = true
named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), memoryFormat, fileFormat).generateMips()
@ -281,6 +291,22 @@ class GLStateTracker {
}
}
fun loadNamedTextureSafe(path: String): GLTexture2D {
if (!loadedEmptyTexture) {
loadedEmptyTexture = true
named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips()
}
return named2DTextures.computeIfAbsent(path) {
if (!Starbound.pathExists(path)) {
LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
return@computeIfAbsent named2DTextures[missingTexturePath]!!
}
return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path)).generateMips()
}
}
fun bind(obj: GLVertexBufferObject): GLVertexBufferObject {
if (obj.type == VBOType.ARRAY)
VBO = obj

View File

@ -162,6 +162,40 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
return this
}
fun upload(path: File): GLTexture2D {
state.ensureSameThread()
if (!path.exists()) {
throw FileNotFoundException("${path.absolutePath} does not exist")
}
if (!path.isFile) {
throw FileNotFoundException("${path.absolutePath} is not a file")
}
val getwidth = intArrayOf(0)
val getheight = intArrayOf(0)
val getchannels = intArrayOf(0)
val bytes = STBImage.stbi_load(path.absolutePath, getwidth, getheight, getchannels, 0) ?: throw TextureLoadingException("Unable to load ${path.absolutePath}. Is it a valid image?")
require(getwidth[0] > 0) { "Image ${path.absolutePath} has bad width of ${getwidth[0]}" }
require(getheight[0] > 0) { "Image ${path.absolutePath} has bad height of ${getheight[0]}" }
val bufferFormat = when (val numChannels = getchannels[0]) {
1 -> GL_R
2 -> GL_RG
3 -> GL_RGB
4 -> GL_RGBA
else -> throw IllegalArgumentException("Weird amount of channels in file: $numChannels")
}
upload(bufferFormat, getwidth[0], getheight[0], bufferFormat, GL_UNSIGNED_BYTE, bytes)
STBImage.stbi_image_free(bytes)
return this
}
fun upload(buff: ByteBuffer, memoryFormat: Int, bufferFormat: Int): GLTexture2D {
state.ensureSameThread()
@ -180,6 +214,32 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
return this
}
fun upload(buff: ByteBuffer): GLTexture2D {
state.ensureSameThread()
val getwidth = intArrayOf(0)
val getheight = intArrayOf(0)
val getchannels = intArrayOf(0)
val bytes = STBImage.stbi_load_from_memory(buff, getwidth, getheight, getchannels, 0) ?: throw TextureLoadingException("Unable to load ${buff}. Is it a valid image?")
require(getwidth[0] > 0) { "Image $name has bad width of ${getwidth[0]}" }
require(getheight[0] > 0) { "Image $name has bad height of ${getheight[0]}" }
val bufferFormat = when (val numChannels = getchannels[0]) {
1 -> GL_R
2 -> GL_RG
3 -> GL_RGB
4 -> GL_RGBA
else -> throw IllegalArgumentException("Weird amount of channels in file: $numChannels")
}
upload(bufferFormat, getwidth[0], getheight[0], bufferFormat, GL_UNSIGNED_BYTE, bytes)
STBImage.stbi_image_free(bytes)
return this
}
var isValid = true
private set

View File

@ -0,0 +1,103 @@
package ru.dbotthepony.kstarbound.defs
import com.google.gson.GsonBuilder
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.io.KTypeAdapter
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import kotlin.properties.Delegates
class Parallax {
var verticalOrigin = 0.0
var layers = Array(0) { ParallaxLayer() }
companion object {
val ADAPTER = KTypeAdapter(::Parallax,
Parallax::verticalOrigin,
Parallax::layers,
)
fun registerGson(gsonBuilder: GsonBuilder) {
gsonBuilder.registerTypeAdapter(Parallax::class.java, ADAPTER)
gsonBuilder.registerTypeAdapter(ParallaxLayer::class.java, ParallaxLayer.ADAPTER)
gsonBuilder.registerTypeAdapter(ParallaxLayer.Parallax::class.java, ParallaxLayer.LAYER_PARALLAX_ADAPTER)
}
}
}
class ParallaxLayer {
class Parallax(val x: Double, val y: Double)
var timeOfDayCorrelation: String? = null
var offset = Vector2d.ZERO
var repeatY = false
var lightMapped = false
var tileLimitTop: Int? = null
var parallax by Delegates.notNull<Parallax>()
var unlit = false
var nohueshift = false
var minSpeed = 0
var maxSpeed = 0
var fadePercent = 0.0
var frequency = 1.0
var modCount = 0
var noRandomOffset = false
var directives: String? = null
var kind by Delegates.notNull<String>()
var baseCount = 1
companion object {
val LAYER_PARALLAX_ADAPTER = object : TypeAdapter<Parallax>() {
override fun write(out: JsonWriter?, value: Parallax?) {
TODO("Not yet implemented")
}
override fun read(reader: JsonReader): Parallax {
return when (val type = reader.peek()) {
JsonToken.BEGIN_ARRAY -> {
reader.beginArray()
val instance = Parallax(reader.nextDouble(), reader.nextDouble())
reader.endArray()
instance
}
JsonToken.NUMBER -> {
val num = reader.nextDouble()
Parallax(num, num)
}
else -> throw JsonSyntaxException("Unexpected token $type")
}
}
}
val ADAPTER = KTypeAdapter(::ParallaxLayer,
ParallaxLayer::timeOfDayCorrelation,
ParallaxLayer::offset,
ParallaxLayer::repeatY,
ParallaxLayer::lightMapped,
ParallaxLayer::tileLimitTop,
ParallaxLayer::parallax,
ParallaxLayer::unlit,
ParallaxLayer::nohueshift,
ParallaxLayer::minSpeed,
ParallaxLayer::maxSpeed,
ParallaxLayer::fadePercent,
ParallaxLayer::kind,
ParallaxLayer::baseCount,
ParallaxLayer::noRandomOffset,
ParallaxLayer::directives,
ParallaxLayer::frequency,
ParallaxLayer::modCount,
)
}
}