improve world renderer

This commit is contained in:
Bixilon 2021-11-13 16:06:22 +01:00
parent 20c5901fea
commit 3fd30c65b0
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
6 changed files with 193 additions and 40 deletions

View File

@ -27,7 +27,7 @@ Minosoft is an open source minecraft client, written from scratch in kotlin (and
- CPU: Minosoft works mostly asynchronous, so multiple cores are good. For FPS more clock speed is better.
- RAM: Minimum 300 MiB, 1 GiB recommended
- Disk space: Minosoft itself is pretty small (2-3 MiB), the libraries are a bit bigger (~80 MiB). You also need to have the "normal" minecraft assets (~ 300 MiB per minecraft version).
- GPU: OpenGL 3.3+. Every modern GPU works and is recommended.
- GPU: OpenGL 3.0+. Every modern GPU works and is recommended.
- Java 11+, newest version recommended (This is really important, we use features that are only available in this version. Java 8 is currently **not** supported).
## Rendering

View File

@ -16,6 +16,7 @@ package de.bixilon.minosoft.gui.rendering.block
import de.bixilon.minosoft.data.assets.AssetsUtil
import de.bixilon.minosoft.data.assets.Resources
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.data.world.Chunk
import de.bixilon.minosoft.data.world.ChunkSection
import de.bixilon.minosoft.data.world.World
import de.bixilon.minosoft.gui.rendering.RenderWindow
@ -24,22 +25,32 @@ import de.bixilon.minosoft.gui.rendering.RendererBuilder
import de.bixilon.minosoft.gui.rendering.block.mesh.ChunkSectionMeshes
import de.bixilon.minosoft.gui.rendering.block.preparer.AbstractSectionPreparer
import de.bixilon.minosoft.gui.rendering.block.preparer.GenericSectionPreparer
import de.bixilon.minosoft.gui.rendering.input.camera.Frustum
import de.bixilon.minosoft.gui.rendering.modding.events.FrustumChangeEvent
import de.bixilon.minosoft.gui.rendering.models.ModelLoader
import de.bixilon.minosoft.gui.rendering.system.base.RenderSystem
import de.bixilon.minosoft.gui.rendering.system.base.phases.OpaqueDrawable
import de.bixilon.minosoft.gui.rendering.system.base.phases.TranslucentDrawable
import de.bixilon.minosoft.gui.rendering.system.base.phases.TransparentDrawable
import de.bixilon.minosoft.gui.rendering.util.VecUtil.chunkPosition
import de.bixilon.minosoft.gui.rendering.util.VecUtil.sectionHeight
import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3iUtil.EMPTY
import de.bixilon.minosoft.modding.event.events.BlockSetEvent
import de.bixilon.minosoft.modding.event.events.ChunkDataChangeEvent
import de.bixilon.minosoft.modding.event.events.ChunkUnloadEvent
import de.bixilon.minosoft.modding.event.events.MassBlockSetEvent
import de.bixilon.minosoft.modding.event.invoker.CallbackEventInvoker
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
import de.bixilon.minosoft.util.KUtil.synchronizedMapOf
import de.bixilon.minosoft.util.KUtil.synchronizedSetOf
import de.bixilon.minosoft.util.KUtil.toResourceLocation
import de.bixilon.minosoft.util.KUtil.toSynchronizedMap
import de.bixilon.minosoft.util.KUtil.unsafeCast
import de.bixilon.minosoft.util.collections.SynchronizedMap
import de.bixilon.minosoft.util.task.pool.DefaultThreadPool
import de.bixilon.minosoft.util.task.pool.ThreadPool.Priorities.LOW
import de.bixilon.minosoft.util.task.pool.ThreadPoolRunnable
import glm_.vec2.Vec2i
import glm_.vec3.Vec3i
import java.io.FileInputStream
import java.util.zip.GZIPInputStream
import java.util.zip.ZipInputStream
@ -49,19 +60,26 @@ class WorldRenderer(
override val renderWindow: RenderWindow,
) : Renderer, OpaqueDrawable, TranslucentDrawable, TransparentDrawable {
override val renderSystem: RenderSystem = renderWindow.renderSystem
private val frustum = renderWindow.inputHandler.camera.frustum
private val shader = renderSystem.createShader("minosoft:world".toResourceLocation())
private val transparentShader = renderSystem.createShader("minosoft:world".toResourceLocation())
private val world: World = connection.world
private val sectionPreparer: AbstractSectionPreparer = GenericSectionPreparer(renderWindow)
private val lightMap = LightMap(connection)
private val meshes: SynchronizedMap<Vec2i, SynchronizedMap<Int, ChunkSectionMeshes>> = synchronizedMapOf()
private var visibleMeshes: MutableSet<ChunkSectionMeshes> = mutableSetOf() // ToDo: Split in opaque, transparent, translucent meshes
private val meshes: SynchronizedMap<Vec2i, SynchronizedMap<Int, ChunkSectionMeshes>> = synchronizedMapOf() // all prepared (and up to date) meshes
private var visibleMeshes: MutableSet<ChunkSectionMeshes> = mutableSetOf() // ToDo: Split in opaque, transparent, translucent meshes and sort (opaque and transparent front to back, translucent back to front)
private var incomplete: MutableSet<Vec2i> = synchronizedSetOf() // Queue of chunk positions that can not be rendered yet (data not complete or neighbours not completed yet)
private var queue: MutableMap<Vec2i, MutableSet<Int>> = synchronizedMapOf() // Chunk sections that can be prepared or have changed, but are not required to get rendered yet (i.e. culled chunks)
val visibleSize: Int
get() = visibleMeshes.size
val preparedSize: Int
get() = visibleMeshes.size
val queuedSize: Int
get() = queue.size
val incompleteSize: Int
get() = incomplete.size
override fun init() {
val asset = Resources.getAssetVersionByVersion(connection.version)
@ -84,37 +102,159 @@ class WorldRenderer(
renderWindow.textureManager.staticTextures.animator.use(transparentShader)
lightMap.use(transparentShader)
connection.registerEvent(CallbackEventInvoker.of<FrustumChangeEvent> { onFrustumChange(it.frustum) })
connection.registerEvent(CallbackEventInvoker.of<FrustumChangeEvent> { onFrustumChange() })
connection.registerEvent(CallbackEventInvoker.of<ChunkDataChangeEvent> {
val sections = it.chunk.sections ?: return@of
for ((sectionHeight, section) in sections) {
prepareSection(it.chunkPosition, sectionHeight, section)
}
})
connection.registerEvent(CallbackEventInvoker.of<ChunkUnloadEvent> {
val meshes = this.meshes.remove(it.chunkPosition)?.values ?: return@of
renderWindow.queue += {
for (mesh in meshes) {
mesh.unload()
this.visibleMeshes -= mesh
}
connection.registerEvent(CallbackEventInvoker.of<ChunkDataChangeEvent> { updateChunk(it.chunkPosition, it.chunk, true) })
connection.registerEvent(CallbackEventInvoker.of<BlockSetEvent> { updateSection(it.blockPosition.chunkPosition, it.blockPosition.sectionHeight) })
connection.registerEvent(CallbackEventInvoker.of<MassBlockSetEvent> {
val chunk = world[it.chunkPosition] ?: return@of
val meshes = meshes.getOrPut(it.chunkPosition) { synchronizedMapOf() }
val sectionHeights: MutableSet<Int> = mutableSetOf()
for (blockPosition in it.blocks.keys) {
sectionHeights += blockPosition.sectionHeight
}
for (sectionHeight in sectionHeights) {
updateSection(it.chunkPosition, sectionHeight, chunk, meshes)
}
})
connection.registerEvent(CallbackEventInvoker.of<ChunkUnloadEvent> { unloadChunk(it.chunkPosition) })
}
@Synchronized
private fun prepareSection(chunkPosition: Vec2i, sectionHeight: Int, section: ChunkSection? = world[chunkPosition]?.sections?.get(sectionHeight)) {
if (section == null) {
private fun unloadChunk(chunkPosition: Vec2i) {
TODO()
}
/**
* @return All 8 fully loaded neighbour chunks or null
*/
private fun getChunkNeighbours(neighbourPositions: Array<Vec2i>): Array<Chunk?> {
val chunks: Array<Chunk?> = arrayOfNulls(neighbourPositions.size)
for ((index, neighbourPosition) in neighbourPositions.withIndex()) {
val chunk = world[neighbourPosition] ?: continue
if (!chunk.isFullyLoaded) {
continue
}
chunks[index] = chunk
}
return chunks
}
private fun getChunkNeighbourPositions(chunkPosition: Vec2i): Array<Vec2i> {
return arrayOf(
chunkPosition + Vec2i(-1, -1),
chunkPosition + Vec2i(-1, 0),
chunkPosition + Vec2i(-1, 1),
chunkPosition + Vec2i(0, -1),
chunkPosition + Vec2i(0, 1),
chunkPosition + Vec2i(1, -1),
chunkPosition + Vec2i(1, 0),
chunkPosition + Vec2i(1, 1),
)
}
/**
* @param neighbourChunks: **Fully loaded** neighbour chunks
*/
private fun getSectionNeighbours(neighbourChunks: Array<Chunk>, chunk: Chunk, sectionHeight: Int): Array<ChunkSection?> {
val sections = chunk.sections!!
return arrayOf(
sections[sectionHeight - 1],
sections[sectionHeight + 1],
neighbourChunks[3].sections!![sectionHeight],
neighbourChunks[4].sections!![sectionHeight],
neighbourChunks[1].sections!![sectionHeight],
neighbourChunks[7].sections!![sectionHeight],
)
}
/**
* Called when chunk data changes
* Checks if the chunk is visible and if so, updates the mesh. If not visible, unloads the current mesh and queues it for loading
*/
private fun updateChunk(chunkPosition: Vec2i, chunk: Chunk = world.chunks[chunkPosition]!!, checkQueue: Boolean) {
if (!chunk.isFullyLoaded) {
return
}
val neighbourPositions = getChunkNeighbourPositions(chunkPosition)
val neighbours = getChunkNeighbours(neighbourPositions)
val mesh = sectionPreparer.prepare(chunkPosition, sectionHeight, section, arrayOfNulls(6))
var neighboursLoaded = true
for (neighbour in neighbours) {
if (neighbour?.isFullyLoaded != true) {
neighboursLoaded = false
}
}
if (checkQueue) {
for ((index, neighbourPosition) in neighbourPositions.withIndex()) {
if (neighbourPosition !in incomplete) {
continue
}
val neighbourChunk = neighbours[index] ?: continue
updateChunk(neighbourPosition, neighbourChunk, false)
}
}
if (!neighboursLoaded) {
incomplete += chunkPosition
return
}
incomplete -= chunkPosition
val meshes = this.meshes.getOrPut(chunkPosition) { synchronizedMapOf() }
for ((sectionHeight, section) in chunk.sections!!) {
updateSection(chunkPosition, sectionHeight, chunk, meshes)
}
}
private fun updateSection(chunkPosition: Vec2i, sectionHeight: Int, chunk: Chunk = world[chunkPosition]!!, meshes: SynchronizedMap<Int, ChunkSectionMeshes> = this.meshes.getOrPut(chunkPosition) { synchronizedMapOf() }) {
val task = ThreadPoolRunnable(priority = LOW) {
updateSectionSync(chunkPosition, sectionHeight, chunk, meshes)
}
DefaultThreadPool += task
}
private fun updateSectionSync(chunkPosition: Vec2i, sectionHeight: Int, chunk: Chunk, meshes: SynchronizedMap<Int, ChunkSectionMeshes>) {
if (!chunk.isFullyLoaded || incomplete.contains(chunkPosition)) {
// chunk not loaded and/or neighbours also not fully loaded
return
}
val section = chunk.sections!![sectionHeight] ?: return
val visible = isChunkVisible(chunkPosition, sectionHeight, Vec3i.EMPTY, Vec3i(16, 16, 16)) // ToDo: min/maxPosition
val previousMesh = meshes[sectionHeight]
if (previousMesh != null && !visible) {
meshes.remove(sectionHeight)
renderWindow.queue += {
visibleMeshes -= previousMesh
previousMesh.unload()
}
}
if (visible) {
// ToDo: Possible threading issue
val sectionQueue = queue[chunkPosition]
if (sectionQueue != null) {
sectionQueue -= sectionHeight
if (sectionQueue.isEmpty()) {
queue.remove(chunkPosition)
}
}
val neighbours = getSectionNeighbours(getChunkNeighbours(getChunkNeighbourPositions(chunkPosition)).unsafeCast(), chunk, sectionHeight)
prepareSection(chunkPosition, sectionHeight, section, neighbours, meshes)
} else {
queue.getOrPut(chunkPosition) { synchronizedSetOf() } += sectionHeight
}
}
/**
* Preparse a chunk section, loads in (in the renderQueue) and stores it in the meshes. Should run on another thread
*/
private fun prepareSection(chunkPosition: Vec2i, sectionHeight: Int, section: ChunkSection, neighbours: Array<ChunkSection?>, meshes: SynchronizedMap<Int, ChunkSectionMeshes> = this.meshes.getOrPut(chunkPosition) { synchronizedMapOf() }) {
val mesh = sectionPreparer.prepare(chunkPosition, sectionHeight, section, neighbours)
val currentMesh = meshes.remove(sectionHeight)
renderWindow.queue += {
@ -125,7 +265,9 @@ class WorldRenderer(
mesh.load()
meshes[sectionHeight] = mesh
this.visibleMeshes += mesh
if (isChunkVisible(chunkPosition, sectionHeight, mesh.minPosition, mesh.maxPosition)) {
this.visibleMeshes += mesh
}
}
}
@ -162,19 +304,35 @@ class WorldRenderer(
}
}
private fun onFrustumChange(frustum: Frustum) {
private fun isChunkVisible(chunkPosition: Vec2i, sectionHeight: Int, minPosition: Vec3i, maxPosition: Vec3i): Boolean {
// ToDo: Cave culling, frustum clipping, improve performance
return frustum.containsChunk(chunkPosition, sectionHeight, minPosition, maxPosition)
}
private fun onFrustumChange() {
val visible: MutableSet<ChunkSectionMeshes> = mutableSetOf()
// ToDo
for ((chunkPosition, meshes) in this.meshes.toSynchronizedMap()) {
for ((sectionHeight, mesh) in meshes) {
if (!frustum.containsChunk(chunkPosition, sectionHeight, mesh.minPosition, mesh.maxPosition)) {
if (!isChunkVisible(chunkPosition, sectionHeight, mesh.minPosition, mesh.maxPosition)) {
continue
}
visible += mesh
}
}
for ((chunkPosition, sectionHeights) in this.queue.toSynchronizedMap()) {
val chunk = world[chunkPosition]
if (chunk == null) {
this.queue.remove(chunkPosition)
continue
}
val meshes = this.meshes.getOrPut(chunkPosition) { synchronizedMapOf() }
for (sectionHeight in sectionHeights) {
updateSection(chunkPosition, sectionHeight, chunk, meshes)
}
}
this.visibleMeshes = visible
}

View File

@ -53,6 +53,7 @@ class ChunkSectionMeshes(
} else {
mesh.load()
}
maxPosition += 1
}
@Synchronized

View File

@ -87,7 +87,7 @@ class DebugHUDElement(hudRenderer: HUDRenderer) : LayoutedHUDElement<GridLayout>
layout += TextElement(hudRenderer, TextComponent(RunConfiguration.VERSION_STRING, ChatColors.RED))
layout += AutoTextElement(hudRenderer, 1) { "FPS ${renderWindow.renderStats.smoothAvgFPS.round10}" }
renderWindow[WorldRenderer]?.apply {
layout += AutoTextElement(hudRenderer, 1) { "C v=${visibleSize}, p=${preparedSize}, q=-1, t=${connection.world.chunks.size}" }
layout += AutoTextElement(hudRenderer, 1) { "C v=$visibleSize, p=$preparedSize, q=$queuedSize, i=$incompleteSize, t=${connection.world.chunks.size}" }
}
layout += AutoTextElement(hudRenderer, 1) { "E t=${connection.world.entities.size}" }

View File

@ -20,9 +20,6 @@ import de.bixilon.minosoft.protocol.packets.s2c.play.MassBlockSetS2CP
import glm_.vec2.Vec2i
import glm_.vec3.Vec3i
/**
* Fired when at least block is changed
*/
class MassBlockSetEvent(
connection: PlayConnection,
initiator: EventInitiators,

View File

@ -53,7 +53,7 @@ class MassBlockSetS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
}
}
buffer.versionId < ProtocolVersions.V_20W28A -> {
chunkPosition = Vec2i(buffer.readInt(), buffer.readInt())
chunkPosition = buffer.readChunkPosition()
val count = buffer.readVarInt()
for (i in 0 until count) {
val position = buffer.readByte().toInt()
@ -77,18 +77,15 @@ class MassBlockSetS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
}
override fun handle(connection: PlayConnection) {
if (blocks.isEmpty()) {
return
}
val chunk = connection.world[chunkPosition] ?: return // thanks mojang
if (chunk.sections == null) {
return
}
chunk.setBlocks(blocks)
// tweak
if (!connection.version.isFlattened()) {
for ((position, blockState) in blocks) {
chunk[position] = blockState
}
}
connection.fireEvent(MassBlockSetEvent(connection, this))
}