From 3fd30c65b0d4e95243c681f9d61f4fc57c27d56e Mon Sep 17 00:00:00 2001 From: Bixilon Date: Sat, 13 Nov 2021 16:06:22 +0100 Subject: [PATCH] improve world renderer --- ReadMe.md | 2 +- .../gui/rendering/block/WorldRenderer.kt | 214 +++++++++++++++--- .../block/mesh/ChunkSectionMeshes.kt | 1 + .../gui/hud/elements/other/DebugHUDElement.kt | 2 +- .../modding/event/events/MassBlockSetEvent.kt | 3 - .../packets/s2c/play/MassBlockSetS2CP.kt | 11 +- 6 files changed, 193 insertions(+), 40 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 07ca56903..3ddd44a20 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -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 diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/block/WorldRenderer.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/block/WorldRenderer.kt index 59eeb0df2..4a187636a 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/block/WorldRenderer.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/block/WorldRenderer.kt @@ -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> = synchronizedMapOf() - private var visibleMeshes: MutableSet = mutableSetOf() // ToDo: Split in opaque, transparent, translucent meshes + private val meshes: SynchronizedMap> = synchronizedMapOf() // all prepared (and up to date) meshes + private var visibleMeshes: MutableSet = mutableSetOf() // ToDo: Split in opaque, transparent, translucent meshes and sort (opaque and transparent front to back, translucent back to front) + private var incomplete: MutableSet = synchronizedSetOf() // Queue of chunk positions that can not be rendered yet (data not complete or neighbours not completed yet) + private var queue: MutableMap> = 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 { onFrustumChange(it.frustum) }) + connection.registerEvent(CallbackEventInvoker.of { onFrustumChange() }) - - connection.registerEvent(CallbackEventInvoker.of { - val sections = it.chunk.sections ?: return@of - for ((sectionHeight, section) in sections) { - prepareSection(it.chunkPosition, sectionHeight, section) - } - }) - - connection.registerEvent(CallbackEventInvoker.of { - 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 { updateChunk(it.chunkPosition, it.chunk, true) }) + connection.registerEvent(CallbackEventInvoker.of { updateSection(it.blockPosition.chunkPosition, it.blockPosition.sectionHeight) }) + connection.registerEvent(CallbackEventInvoker.of { + val chunk = world[it.chunkPosition] ?: return@of + val meshes = meshes.getOrPut(it.chunkPosition) { synchronizedMapOf() } + val sectionHeights: MutableSet = mutableSetOf() + for (blockPosition in it.blocks.keys) { + sectionHeights += blockPosition.sectionHeight + } + for (sectionHeight in sectionHeights) { + updateSection(it.chunkPosition, sectionHeight, chunk, meshes) } }) + connection.registerEvent(CallbackEventInvoker.of { 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): Array { + val chunks: Array = 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 { + 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, sectionHeight: Int): Array { + 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 = 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) { + 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, meshes: SynchronizedMap = 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 = 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 } diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/block/mesh/ChunkSectionMeshes.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/block/mesh/ChunkSectionMeshes.kt index 68d8c1fe4..68bca427e 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/block/mesh/ChunkSectionMeshes.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/block/mesh/ChunkSectionMeshes.kt @@ -53,6 +53,7 @@ class ChunkSectionMeshes( } else { mesh.load() } + maxPosition += 1 } @Synchronized diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/gui/hud/elements/other/DebugHUDElement.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/gui/hud/elements/other/DebugHUDElement.kt index f347565e5..0c69fb697 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/gui/hud/elements/other/DebugHUDElement.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/gui/hud/elements/other/DebugHUDElement.kt @@ -87,7 +87,7 @@ class DebugHUDElement(hudRenderer: HUDRenderer) : LayoutedHUDElement 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}" } diff --git a/src/main/java/de/bixilon/minosoft/modding/event/events/MassBlockSetEvent.kt b/src/main/java/de/bixilon/minosoft/modding/event/events/MassBlockSetEvent.kt index 39784cd24..9dfa944be 100644 --- a/src/main/java/de/bixilon/minosoft/modding/event/events/MassBlockSetEvent.kt +++ b/src/main/java/de/bixilon/minosoft/modding/event/events/MassBlockSetEvent.kt @@ -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, diff --git a/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/play/MassBlockSetS2CP.kt b/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/play/MassBlockSetS2CP.kt index 123f13e52..6a4f543ea 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/play/MassBlockSetS2CP.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/play/MassBlockSetS2CP.kt @@ -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)) }