mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-09 15:29:20 -04:00
improve world renderer
This commit is contained in:
parent
20c5901fea
commit
3fd30c65b0
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,7 @@ class ChunkSectionMeshes(
|
||||
} else {
|
||||
mesh.load()
|
||||
}
|
||||
maxPosition += 1
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
@ -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}" }
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user