refactor heightmap

This commit is contained in:
Moritz Zwerger 2023-07-27 23:06:23 +02:00
parent ebb5b8a04e
commit 4d02696bc5
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
10 changed files with 326 additions and 140 deletions

View File

@ -14,6 +14,8 @@
package de.bixilon.minosoft.data.world.chunk.light
import de.bixilon.kotlinglm.vec3.Vec3i
import de.bixilon.minosoft.data.registries.blocks.GlassTest0
import de.bixilon.minosoft.data.registries.blocks.LeavesTest0
import de.bixilon.minosoft.data.registries.blocks.types.stone.StoneTest0
import de.bixilon.minosoft.data.world.chunk.ChunkTestingUtil.createChunkWithNeighbours
import de.bixilon.minosoft.data.world.chunk.chunk.Chunk
@ -25,15 +27,6 @@ import org.testng.annotations.Test
@Test(groups = ["light"], dependsOnGroups = ["block"])
class GeneralHeightmapTest {
fun testMaxHeightEast() {
val chunk: Chunk = createChunkWithNeighbours()
chunk[Vec3i(2, 10, 3)] = StoneTest0.state
chunk[Vec3i(3, 11, 2)] = StoneTest0.state
chunk[Vec3i(3, 12, 4)] = StoneTest0.state
chunk[Vec3i(4, 13, 3)] = StoneTest0.state
assertEquals(chunk.light.getNeighbourMaxHeight(chunk.neighbours.get()!!, 3, 3), 14)
}
fun testMinHeightEast() {
val chunk: Chunk = createChunkWithNeighbours()
chunk[Vec3i(2, 10, 3)] = StoneTest0.state
@ -43,16 +36,6 @@ class GeneralHeightmapTest {
assertEquals(chunk.light.getNeighbourMinHeight(chunk.neighbours.get()!!, 3, 3), 11)
}
fun testMaxHeightNeighbourEast() {
val chunk: Chunk = createChunkWithNeighbours()
val neighbours = chunk.neighbours.get()!!
chunk[Vec3i(14, 10, 3)] = StoneTest0.state
chunk[Vec3i(15, 11, 2)] = StoneTest0.state
chunk[Vec3i(15, 12, 4)] = StoneTest0.state
neighbours[ChunkNeighbours.EAST][Vec3i(0, 13, 3)] = StoneTest0.state
assertEquals(chunk.light.getNeighbourMaxHeight(neighbours, 15, 3), 14)
}
fun testMinHeightNeighbourEast() {
val chunk: Chunk = createChunkWithNeighbours()
val neighbours = chunk.neighbours.get()!!
@ -62,6 +45,24 @@ class GeneralHeightmapTest {
neighbours[ChunkNeighbours.EAST][Vec3i(0, 10, 3)] = StoneTest0.state
assertEquals(chunk.light.getNeighbourMinHeight(neighbours, 15, 3), 11)
}
// TODO: Test other directions
fun `top of the world and not passing`() {
val chunk: Chunk = createChunkWithNeighbours()
chunk[Vec3i(2, 255, 3)] = StoneTest0.state
assertEquals(chunk.light.heightmap[2, 3], 256)
}
fun `top of the world and entering`() {
val chunk: Chunk = createChunkWithNeighbours()
chunk[Vec3i(2, 255, 3)] = LeavesTest0.state
assertEquals(chunk.light.heightmap[2, 3], 255)
}
fun `top of the world and passing`() {
val chunk: Chunk = createChunkWithNeighbours()
chunk[Vec3i(2, 255, 3)] = GlassTest0.state
assertEquals(chunk.light.heightmap[2, 3], Int.MIN_VALUE)
}
}

View File

@ -0,0 +1,59 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.world.chunk.light
import de.bixilon.kotlinglm.vec3.Vec3i
import de.bixilon.minosoft.data.registries.blocks.types.stone.StoneTest0
import de.bixilon.minosoft.data.world.chunk.light.LightTestUtil.assertLight
import de.bixilon.minosoft.protocol.network.connection.play.ConnectionTestUtil.createConnection
import org.testng.annotations.Test
@Test(groups = ["light"], dependsOnGroups = ["block"], threadPoolSize = 8, priority = 1000)
class SkyLightTraceIT {
fun `check level below block`() {
val world = createConnection(3, light = true).world
world[Vec3i(8, 10, 8)] = StoneTest0.state
world.assertLight(8, 9, 8, 0xD0)
}
fun `heightmap optimization west, upper block set`() {
val world = createConnection(3, light = true).world
world[Vec3i(8, 10, 8)] = StoneTest0.state
world[Vec3i(7, 12, 8)] = StoneTest0.state
world.assertLight(7, 11, 8, 0xD0)
}
fun `heightmap optimization east, upper block set`() {
val world = createConnection(3, light = true).world
world[Vec3i(8, 10, 8)] = StoneTest0.state
world[Vec3i(9, 12, 8)] = StoneTest0.state
world.assertLight(9, 11, 8, 0xD0)
}
fun `heightmap optimization north, upper block set`() {
val world = createConnection(3, light = true).world
world[Vec3i(8, 10, 8)] = StoneTest0.state
world[Vec3i(8, 12, 7)] = StoneTest0.state
world.assertLight(8, 11, 7, 0xD0)
}
fun `heightmap optimization south, upper block set`() {
val world = createConnection(3, light = true).world
world[Vec3i(8, 10, 8)] = StoneTest0.state
world[Vec3i(8, 12, 9)] = StoneTest0.state
world.assertLight(8, 11, 9, 0xD0)
}
}

View File

@ -23,7 +23,7 @@ import de.bixilon.minosoft.protocol.network.connection.play.ConnectionTestUtil.c
import org.testng.annotations.Test
@Test(groups = ["light"], dependsOnGroups = ["block"], threadPoolSize = 8, priority = -100)
@Test(groups = ["light"], dependsOnGroups = ["block"], threadPoolSize = 8, priority = 1000)
class SkyLightPlaceIT {
fun aboveBlock() {

View File

@ -228,7 +228,7 @@ class World(
reset += { chunk.light.reset() }
calculate += {
if (heightmap) {
chunk.light.recalculateHeightmap()
chunk.light.heightmap.recalculate()
}
chunk.light.calculate()
}

View File

@ -62,7 +62,7 @@ class Chunk(
init {
light.recalculateHeightmap()
light.heightmap.recalculate()
}
@Deprecated("neighbours.complete", ReplaceWith("neighbours.complete"))
@ -162,7 +162,7 @@ class Chunk(
if (executed.isEmpty()) {
return lock.unlock()
}
light.recalculateHeightmap()
light.heightmap.recalculate()
light.recalculate()
for (section in sections) {

View File

@ -0,0 +1,26 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.world.chunk.heightmap
import de.bixilon.minosoft.data.registries.blocks.state.BlockState
interface AbstractHeightmap {
fun recalculate()
operator fun get(x: Int, z: Int): Int
operator fun get(index: Int): Int
fun onBlockChange(x: Int, y: Int, z: Int, next: BlockState?)
}

View File

@ -0,0 +1,30 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.world.chunk.heightmap
import de.bixilon.minosoft.data.registries.blocks.state.BlockState
class FixedHeightmap(val value: Int) : AbstractHeightmap {
override fun recalculate() = Unit
override fun get(x: Int, z: Int) = value
override fun get(index: Int) = value
override fun onBlockChange(x: Int, y: Int, z: Int, next: BlockState?) = Unit
companion object {
val MAX_VALUE = FixedHeightmap(Int.MAX_VALUE)
}
}

View File

@ -0,0 +1,117 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.world.chunk.heightmap
import de.bixilon.minosoft.data.registries.blocks.state.BlockState
import de.bixilon.minosoft.data.world.chunk.chunk.Chunk
import de.bixilon.minosoft.gui.rendering.util.VecUtil.sectionHeight
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
abstract class Heightmap(protected val chunk: Chunk) : AbstractHeightmap {
protected val heightmap = IntArray(ProtocolDefinition.SECTION_WIDTH_X * ProtocolDefinition.SECTION_WIDTH_Z) { Int.MIN_VALUE }
override fun get(index: Int) = heightmap[index]
override fun get(x: Int, z: Int) = heightmap[(z shl 4) or x]
protected abstract fun passes(state: BlockState): HeightmapPass
protected abstract fun onHeightmapUpdate(x: Int, z: Int, previous: Int, now: Int)
override fun recalculate() {
chunk.lock.lock()
val maxY = chunk.maxSection * ProtocolDefinition.SECTION_HEIGHT_Y
for (x in 0 until ProtocolDefinition.SECTION_WIDTH_X) {
for (z in 0 until ProtocolDefinition.SECTION_WIDTH_Z) {
trace(x, maxY, z, false)
}
}
chunk.lock.unlock()
}
private fun trace(x: Int, startY: Int, z: Int, notify: Boolean) {
val sections = chunk.sections
var y = Int.MIN_VALUE
sectionLoop@ for (sectionIndex in (startY.sectionHeight - chunk.minSection) downTo 0) {
if (sectionIndex >= sections.size) {
// starting from above world
continue
}
val section = sections[sectionIndex] ?: continue
if (section.blocks.isEmpty) continue
section.acquire()
for (sectionY in ProtocolDefinition.SECTION_MAX_Y downTo 0) {
val state = section.blocks[x, sectionY, z] ?: continue
val pass = passes(state)
if (pass == HeightmapPass.PASSES) continue
y = (sectionIndex + chunk.minSection) * ProtocolDefinition.SECTION_HEIGHT_Y + sectionY + 1
if (pass == HeightmapPass.ABOVE) y++
section.release()
break@sectionLoop
}
section.release()
}
val index = (z shl 4) or x
val previous = heightmap[index]
if (previous == y) return
heightmap[index] = y
if (notify) {
onHeightmapUpdate(x, z, previous, y)
}
}
override fun onBlockChange(x: Int, y: Int, z: Int, next: BlockState?) {
chunk.lock.lock()
val index = (z shl 4) or x
val current = heightmap[index]
if (current > y + 1) {
// our block is/was not the highest, ignore everything
chunk.lock.unlock()
return
}
if (next == null) {
trace(x, y, z, true)
chunk.lock.unlock()
return
}
when (passes(next)) {
HeightmapPass.ABOVE -> heightmap[index] = y + 1
HeightmapPass.IN -> heightmap[index] = y
HeightmapPass.PASSES -> Unit
}
chunk.lock.unlock()
}
protected enum class HeightmapPass {
ABOVE,
IN,
PASSES,
;
}
}

View File

@ -0,0 +1,63 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.world.chunk.heightmap
import de.bixilon.minosoft.data.direction.Directions
import de.bixilon.minosoft.data.registries.blocks.state.BlockState
import de.bixilon.minosoft.data.world.chunk.chunk.Chunk
import de.bixilon.minosoft.gui.rendering.util.VecUtil.sectionHeight
class LightHeightmap(chunk: Chunk) : Heightmap(chunk) {
override fun recalculate() {
super.recalculate()
chunk.light.calculateSkylight()
}
override fun onHeightmapUpdate(x: Int, z: Int, previous: Int, now: Int) {
if (previous > now) {
// block is lower
return chunk.light.startSkylightFloodFill(x, z)
}
// block is now higher
// ToDo: Neighbours
val sections = chunk.sections
val maxIndex = previous.sectionHeight - chunk.minSection
val minIndex = now.sectionHeight - chunk.minSection
chunk.light.bottom.reset()
for (index in maxIndex downTo minIndex) {
val section = sections[index] ?: continue
section.light.reset()
}
for (index in maxIndex downTo minIndex) {
val section = sections[index] ?: continue
section.light.calculate()
}
chunk.light.calculateSkylight()
}
override fun passes(state: BlockState): HeightmapPass {
val light = state.block.getLightProperties(state)
if (!light.skylightEnters) return HeightmapPass.ABOVE
if (!light.filtersSkylight && light.propagatesLight(Directions.DOWN)) {
// can go through block
return HeightmapPass.PASSES
}
return HeightmapPass.IN
}
}

View File

@ -20,6 +20,8 @@ import de.bixilon.minosoft.data.registries.blocks.state.BlockState
import de.bixilon.minosoft.data.registries.dimension.DimensionProperties
import de.bixilon.minosoft.data.world.chunk.ChunkSection
import de.bixilon.minosoft.data.world.chunk.chunk.Chunk
import de.bixilon.minosoft.data.world.chunk.heightmap.FixedHeightmap
import de.bixilon.minosoft.data.world.chunk.heightmap.LightHeightmap
import de.bixilon.minosoft.data.world.chunk.neighbours.ChunkNeighbours
import de.bixilon.minosoft.data.world.chunk.update.AbstractWorldUpdate
import de.bixilon.minosoft.data.world.chunk.update.chunk.ChunkLightUpdate
@ -29,7 +31,7 @@ import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
class ChunkLight(private val chunk: Chunk) {
private val connection = chunk.connection
val heightmap = IntArray(ProtocolDefinition.SECTION_WIDTH_X * ProtocolDefinition.SECTION_WIDTH_Z) { if (chunk.world.dimension.canSkylight()) Int.MIN_VALUE else Int.MAX_VALUE }
val heightmap = if (chunk.world.dimension.canSkylight()) LightHeightmap(chunk) else FixedHeightmap.MAX_VALUE
val bottom = BorderSectionLight(false, chunk)
val top = BorderSectionLight(true, chunk)
@ -39,10 +41,7 @@ class ChunkLight(private val chunk: Chunk) {
if (!chunk.world.dimension.light) {
return
}
val heightmapIndex = (z shl 4) or x
val previous = heightmap[heightmapIndex]
recalculateHeightmap(x, y, z, next)
onHeightmapUpdate(x, y, z, previous, heightmap[heightmapIndex])
heightmap.onBlockChange(x, y, z, next)
val neighbours = chunk.neighbours.get() ?: return
@ -180,118 +179,8 @@ class ChunkLight(private val chunk: Chunk) {
}
}
fun recalculateHeightmap() {
if (!chunk.world.dimension.canSkylight()) {
return
}
chunk.lock.lock()
val maxY = chunk.maxSection * ProtocolDefinition.SECTION_HEIGHT_Y
for (x in 0 until ProtocolDefinition.SECTION_WIDTH_X) {
for (z in 0 until ProtocolDefinition.SECTION_WIDTH_Z) {
checkHeightmapY(x, maxY, z)
}
}
chunk.lock.unlock()
calculateSkylight()
}
private fun checkHeightmapY(x: Int, startY: Int, z: Int) {
val sections = chunk.sections
var y = Int.MIN_VALUE
sectionLoop@ for (sectionIndex in (startY.sectionHeight - chunk.minSection) downTo 0) {
if (sectionIndex >= sections.size) {
// starting from above world
continue
}
val section = sections[sectionIndex] ?: continue
if (section.blocks.isEmpty) continue
section.acquire()
for (sectionY in ProtocolDefinition.SECTION_MAX_Y downTo 0) {
val state = section.blocks[x, sectionY, z] ?: continue
val light = state.block.getLightProperties(state)
if (light.skylightEnters && !light.filtersSkylight && light.propagatesLight(Directions.DOWN)) {
// can go through block
continue
}
y = (sectionIndex + chunk.minSection) * ProtocolDefinition.SECTION_HEIGHT_Y + sectionY
if (!light.skylightEnters) {
y++
}
section.release()
break@sectionLoop
}
section.release()
}
val heightmapIndex = (z shl 4) or x
heightmap[heightmapIndex] = y
}
private fun onHeightmapUpdate(x: Int, y: Int, z: Int, previous: Int, now: Int) {
if (previous == now) {
return
}
if (previous < y) {
// block is now higher
// ToDo: Neighbours
val sections = chunk.sections
val maxIndex = previous.sectionHeight - chunk.minSection
val minIndex = now.sectionHeight - chunk.minSection
bottom.reset()
for (index in maxIndex downTo minIndex) {
val section = sections[index] ?: continue
section.light.reset()
}
for (index in maxIndex downTo minIndex) {
val section = sections[index] ?: continue
section.light.calculate()
}
calculateSkylight()
} else if (previous > y && chunk.world.dimension.canSkylight()) {
// block is lower
startSkylightFloodFill(x, z)
}
}
private fun recalculateHeightmap(x: Int, y: Int, z: Int, blockState: BlockState?) {
if (!chunk.world.dimension.canSkylight()) {
return
}
chunk.lock.lock()
val index = (z shl 4) or x
val current = heightmap[index]
if (current > y + 1) {
// our block is/was not the highest, ignore everything
chunk.lock.unlock()
return
}
if (blockState == null) {
checkHeightmapY(x, y, z)
chunk.lock.unlock()
return
}
// we are the highest block now
// check if light can pass
val light = blockState.block.getLightProperties(blockState)
if (!light.skylightEnters) {
heightmap[index] = y + 1
} else if (light.filtersSkylight || !light.propagatesLight(Directions.DOWN)) {
heightmap[index] = y
}
chunk.lock.unlock()
return
}
private fun calculateSkylight() {
fun calculateSkylight() {
if (!chunk.world.dimension.canSkylight() || !chunk.neighbours.complete) {
// no need to calculate it
return
@ -400,6 +289,7 @@ class ChunkLight(private val chunk: Chunk) {
}
}
@Deprecated("heightmap")
inline fun getMaxHeight(x: Int, z: Int): Int {
return heightmap[(z shl 4) or x]
}