add noise based terrain, with caves, and better mesh render times.

This commit is contained in:
William Bell committed 2025-10-05 05:25:21 +01:00
1 parent 00f639451e
commit c0586fd60a
12 files changed
+315 -132

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>blocktopia</w>
<w>coords</w>
</words>
</dictionary>
</component>
Generated
+3
View File
@@ -2,5 +2,8 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libs/mc_math_java" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libs/mc_noise_java" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libs/mc_terrain_java" vcs="Git" />
</component>
</project>
+39 -15
View File
@@ -4,11 +4,19 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="3ee4ea58-b8cf-44ca-b7e6-78580145f11f" name="Changes" comment="finish working sunlight system with light flooding with vertical lighting to improve speed">
<list default="true" id="3ee4ea58-b8cf-44ca-b7e6-78580145f11f" name="Changes" comment="add render for the specific session instead of directly calling world">
<change afterPath="$PROJECT_DIR$/.idea/dictionaries/project.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/kotlin/App.kt" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/kotlin/App.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/kotlin/game/Game.kt" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/kotlin/game/Game.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/kotlin/game/session/session.kt" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/kotlin/game/session/session.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/kotlin/game/camera/Camera.kt" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/kotlin/game/camera/Camera.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/kotlin/game/session/world/World.kt" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/kotlin/game/session/world/World.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/kotlin/game/session/world/chunk/Chunk.kt" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/kotlin/game/session/world/chunk/Chunk.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/kotlin/game/session/world/player/Player.kt" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/kotlin/game/session/world/player/Player.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/kotlin/game/settings/settings.kt" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/kotlin/game/settings/settings.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/resources/shaders/fragment.glsl" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/resources/assets/Blocktopia/shaders/core/terrain.fsh" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/resources/shaders/vertex.glsl" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/resources/assets/Blocktopia/shaders/core/terrain.vsh" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -69,24 +77,31 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Gradle./home/william/Documents/GitHub/Buildtopia [run].executor&quot;: &quot;Run&quot;,
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Gradle./home/william/Documents/GitHub/Buildtopia [run].executor": "Run",
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
"git-widget-placeholder": "master",
"kotlin-language-version-configured": "true"
}
}</component>
}]]></component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/app/src/main/resources/assets" />
<recent name="$PROJECT_DIR$/app/src/main/resources/Blocktopia/shaders/core" />
</key>
</component>
<component name="RunManager">
<configuration name="/home/william/Documents/GitHub/Buildtopia [run]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="scriptParameters" value="--info" />
<option name="taskDescriptions">
<list />
</option>
@@ -125,11 +140,20 @@
<option name="project" value="LOCAL" />
<updated>1759549009134</updated>
</task>
<option name="localTasksCounter" value="2" />
<task id="LOCAL-00002" summary="add render for the specific session instead of directly calling world">
<option name="closed" value="true" />
<created>1759549824770</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1759549824770</updated>
</task>
<option name="localTasksCounter" value="3" />
<servers />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="finish working sunlight system with light flooding with vertical lighting to improve speed" />
<option name="LAST_COMMIT_MESSAGE" value="finish working sunlight system with light flooding with vertical lighting to improve speed" />
<MESSAGE value="add render for the specific session instead of directly calling world" />
<option name="LAST_COMMIT_MESSAGE" value="add render for the specific session instead of directly calling world" />
</component>
</project>
-17
View File
@@ -31,23 +31,6 @@ fun createShader(vertexSrc: String, fragmentSrc: String): Int {
return program
}
fun updateProjection(shaderProgram: Int, width: Int, height: Int) {
glViewport(0, 0, width, height)
val fov = 90f
val aspect = width.toFloat() / height
val near = 0.1f
val far = 10000f
val projection = Matrix4f().perspective(Math.toRadians(fov.toDouble()).toFloat(), aspect, near, far)
val projBuffer = FloatArray(16)
projection.get(projBuffer)
val projLoc = glGetUniformLocation(shaderProgram, "projection")
glUseProgram(shaderProgram)
glUniformMatrix4fv(projLoc, false, projBuffer)
}
fun main() {
+27 -18
View File
@@ -3,10 +3,9 @@ package dev.wbell.buildtopia.app.game
import dev.wbell.buildtopia.app.createShader
import dev.wbell.buildtopia.app.game.session.Session
import dev.wbell.buildtopia.app.game.session.world.World
import dev.wbell.buildtopia.app.game.session.world.chunk.ChunkSection
import dev.wbell.buildtopia.app.game.session.world.player.Player
import dev.wbell.buildtopia.app.loadResource
import dev.wbell.buildtopia.app.updateProjection
import org.joml.Matrix4f
import org.joml.Vector3d
import org.lwjgl.BufferUtils
import org.lwjgl.glfw.GLFW.*
@@ -15,10 +14,10 @@ import org.lwjgl.opengl.GL20.glGetUniformLocation
import org.lwjgl.opengl.GL20.glUseProgram
import org.lwjgl.system.MemoryUtil.NULL
import org.lwjgl.opengl.GL13.*
import org.lwjgl.opengl.GL20.glUniformMatrix4fv
import org.lwjgl.stb.STBImage
import java.nio.IntBuffer
import kotlin.math.PI
import kotlin.random.Random
fun loadTexture(path: String): Int {
@@ -75,6 +74,22 @@ object Game {
var viewLoc = 0
var projLoc = 0
fun updateFov(fov: Float) {
glViewport(0, 0, width, height)
val aspect = width.toFloat() / height
val near = 0.1f
val far = 10000f
val projection = Matrix4f().perspective(Math.toRadians(fov.toDouble()).toFloat(), aspect, near, far)
val projBuffer = FloatArray(16)
projection.get(projBuffer)
val projLoc = glGetUniformLocation(shaderProgram, "projection")
glUseProgram(shaderProgram)
glUniformMatrix4fv(projLoc, false, projBuffer)
}
fun init() {
// Initialize GLFW
if (!glfwInit()) throw RuntimeException("Failed to initialize GLFW")
@@ -95,12 +110,12 @@ object Game {
glFrontFace(GL_CCW) // counter-clockwise triangles are front-facing
glDepthFunc(GL_LESS)
textureId = loadTexture("/resource_pack/textures/block/cobblestone.png")
textureId = loadTexture("/assets/Blocktopia/textures/block/cobblestone.png")
// Simple shaders
val vertexShaderSrc = loadResource("/shaders/vertex.glsl")
val vertexShaderSrc = loadResource("/assets/Blocktopia/shaders/core/terrain.vsh")
val fragmentShaderSrc = loadResource("/shaders/fragment.glsl")
val fragmentShaderSrc = loadResource("/assets/Blocktopia/shaders/core/terrain.fsh")
shaderProgram = createShader(vertexShaderSrc, fragmentShaderSrc)
glUseProgram(shaderProgram)
@@ -108,21 +123,10 @@ object Game {
modelLoc = glGetUniformLocation(shaderProgram, "model")
viewLoc = glGetUniformLocation(shaderProgram, "view")
projLoc = glGetUniformLocation(shaderProgram, "projection")
session.World = World(Player(Vector3d(0.0, 4.0, 0.0), 0f, 0f), session)
session.World = World(Player(Vector3d(0.0, 400.0, 0.0), 0f, 0f), session)
session.World!!.player.world = session.World
session.World!!.init()
// Initial projection
// GLFW callback for resizing
glfwSetFramebufferSizeCallback(window) { _, w, h ->
width = w
height = h
updateProjection(shaderProgram, w, h)
}
updateProjection(shaderProgram, width, height)
// Initially disable the cursor
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED)
@@ -163,6 +167,11 @@ object Game {
}
fun render(deltaTime: Double): Boolean {
val widthBuffer = IntArray(1)
val heightBuffer = IntArray(1)
glfwGetFramebufferSize(window!!, widthBuffer, heightBuffer)
width = widthBuffer[0]
height = heightBuffer[0]
session.render(deltaTime)
return true
}
+2 -1
View File
@@ -6,6 +6,7 @@ class Camera(
val position: Vector3d,
var pitch: Float,
var yaw: Float,
var roll: Float
var roll: Float,
var fov: Float
) {
}
+150 -50
View File
@@ -1,5 +1,6 @@
package dev.wbell.buildtopia.app.game.session.world
import OpenSimplex2S.OpenSimplex2S
import dev.wbell.buildtopia.app.game.Game
import dev.wbell.buildtopia.app.game.camera.Camera
import dev.wbell.buildtopia.app.game.session.Session
@@ -11,16 +12,17 @@ import dev.wbell.buildtopia.app.game.session.world.chunk.LightAccessor
import dev.wbell.buildtopia.app.game.session.world.player.Player
import dev.wbell.buildtopia.app.game.settings.SettingKey
import dev.wbell.buildtopia.app.game.settings.Settings
import kotlinx.coroutines.*
import org.joml.Matrix4f
import org.joml.Vector2i
import org.joml.Vector3d
import org.lwjgl.glfw.GLFW.*
import org.lwjgl.opengl.GL20.*
import kotlinx.coroutines.*
import org.joml.Matrix4f
import java.util.PriorityQueue
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.PriorityBlockingQueue
import kotlin.math.PI
import kotlin.random.Random
fun floorDiv(a: Int, b: Int): Int {
// rounds down (towards -∞), unlike / which rounds toward 0
@@ -40,53 +42,36 @@ fun floorMod(a: Int, b: Int): Int {
class World(val player: Player, val session: Session) {
private val chunkRenderer = CoroutineScope(Dispatchers.Default)
val chunkMeshQueue = ConcurrentLinkedQueue<() -> Unit>()
private val chunkRenderQueue = PriorityQueue<Pair<Chunk, Int>>(compareBy { it.second })
private val chunkRenderQueue = PriorityBlockingQueue<Pair<Chunk, Int>>(11, compareBy { it.second })
private val physics = CoroutineScope(Dispatchers.Default)
private val chunks = mutableMapOf<Long, Chunk>()
private val chunks = ConcurrentHashMap<Long, Chunk>()
private val toLoadChunks = ConcurrentHashMap<Long, Vector2i>()
private var isActive = true
var lastTick = glfwGetTime()
var dayNightLoc = glGetUniformLocation(Game.shaderProgram, "dayNight")
var dayNightTick = 0
var seed: Long = 12345
fun init() {
val centerChunk = Vector2i(
(player.position.x / ChunkSection.LENGTH).toInt(),
(player.position.z / ChunkSection.LENGTH).toInt()
)
val renderDistance = Settings.get(SettingKey.RENDERDISTANCE) ?: 32
for (x in -renderDistance..<renderDistance) {
for (z in -renderDistance..<renderDistance) {
chunks[packChunkKey(centerChunk.x + x, centerChunk.y + z)] =
Chunk(
this,
Vector2i(centerChunk.x + x, centerChunk.y + z),
24,
4,
Array(ChunkSection.LENGTH * ChunkSection.LENGTH * ChunkSection.LENGTH * 24) { i ->
val x = i % ChunkSection.LENGTH
val z = (i / ChunkSection.LENGTH) % ChunkSection.LENGTH
val y = (i / (ChunkSection.LENGTH * ChunkSection.LENGTH)) - 4 * ChunkSection.LENGTH
if (y <= Random.nextInt(0, 5)) Block() else null
})
}
}
startPhysics()
startChunkRenderer()
startChunkLoaderRenderer()
startChunkRendererChild()
}
private fun markChunkDirty(chunk: Chunk, center: Vector2i) {
private fun markChunkDirty(chunk: Chunk) {
if (!chunk.isQueuedForRender) {
val dx = chunk.coords.x - center.x
val dz = chunk.coords.y - center.y
val dx = chunk.coords.x
val dz = chunk.coords.y
val dist2 = dx * dx + dz * dz
chunkRenderQueue.add(chunk to dist2)
chunk.isQueuedForRender = true
}
}
private fun startChunkRenderer() {
private fun startChunkLoaderRenderer() {
chunkRenderer.launch {
val tickTime = 50L
while (isActive) {
@@ -94,21 +79,83 @@ class World(val player: Player, val session: Session) {
(player.position.x / ChunkSection.LENGTH).toInt(),
(player.position.z / ChunkSection.LENGTH).toInt()
)
val chunksToRender = toLoadChunks.toList().toMutableList() // (dx, dz)
chunksToRender.sortBy { (_, coords) -> (coords.x - centerChunk.x) * (coords.x - centerChunk.x) + (coords.y - centerChunk.y) * (coords.y - centerChunk.y) }
var i = 0
for ((key, coords) in chunksToRender) {
var peak = -4* ChunkSection.SIZE
val blockData = Array(ChunkSection.LENGTH * ChunkSection.LENGTH * ChunkSection.LENGTH * 24) { i ->
val x = i % ChunkSection.LENGTH
val z = (i / ChunkSection.LENGTH) % ChunkSection.LENGTH
val y = (i / (ChunkSection.LENGTH * ChunkSection.LENGTH)) - 4 * ChunkSection.LENGTH
val renderDistance = Settings.get(SettingKey.RENDERDISTANCE) ?: 32
// world-space coordinates
val worldX = coords.x * ChunkSection.LENGTH + x
val worldZ = coords.y * ChunkSection.LENGTH + z
val worldY = y
for (dx in -renderDistance - 1..renderDistance) {
for (dz in -renderDistance - 1..renderDistance) {
val chunk = getChunk(centerChunk.x + dx, centerChunk.y + dz)
if (chunk != null) {
if (chunk.toRenderMesh()) markChunkDirty(chunk, centerChunk)
// --- Biome / height noise ---
val biomeFactor = OpenSimplex2S.noise2(seed + 1000, worldX * 0.01, worldZ * 0.01)
val baseHeight = 64 + (biomeFactor * 8).toInt() // plains ~56-72
// Hills / mountains
val hillNoise = OpenSimplex2S.noise2(seed + 2000, worldX * 0.02, worldZ * 0.02)
val hillHeight = (hillNoise * 16).toInt()
val terrainHeight = baseHeight + hillHeight
// --- Multi-octave 3D caves ---
fun caveDensity(x: Double, y: Double, z: Double): Double {
var density = 0.0
var frequency = 0.02
var amplitude = 1.0
for (octave in 0 until 3) { // 3 octaves, can increase for more detail
density += OpenSimplex2S.noise3_ImproveXY(seed + 3000 + octave * 1000, x * frequency, y * frequency, z * frequency) * amplitude
frequency *= 2.0
amplitude /= 2.0
}
return density
}
}
}
// Process N chunks per tick
repeat(4) {
val next = chunkRenderQueue.poll() ?: return@repeat
val density = caveDensity(worldX.toDouble(), worldY.toDouble(), worldZ.toDouble())
// Optional: reduce cave openings near surface
val heightFactor = ((terrainHeight - worldY).toDouble() / 30.0).coerceIn(0.0, 1.0)
val isCave = density > 0.5 * heightFactor // adjust threshold for more/less caves
// --- Set block ---
val block: Block? = if (worldY <= terrainHeight && !isCave) {
when {
worldY == terrainHeight -> Block()
worldY >= terrainHeight - 3 -> Block()
else -> Block()
}
} else null
if (terrainHeight > peak) peak = terrainHeight
block
}
chunks[key] = Chunk(
session.World!!,
coords,
24,
4,
blockData,
peak
)
toLoadChunks.remove(key)
i++
}
if (i == 0) delay(tickTime)
}
}
}
private fun startChunkRendererChild() {
chunkRenderer.launch {
val tickTime = 50L
while (isActive) {
while (chunkRenderQueue.isNotEmpty()) {
val next = chunkRenderQueue.poll()
val (chunk, _) = next
chunk.isQueuedForRender = false
@@ -117,6 +164,18 @@ class World(val player: Player, val session: Session) {
chunkMeshQueue.add { chunk.uploadChunkMesh() }
}
}
delay(tickTime)
}
}
}
private fun startChunkRenderer() {
chunkRenderer.launch {
val tickTime = 50L
while (isActive) {
for (chunk in chunks.values) {
if (chunk.toRenderMesh()) markChunkDirty(chunk)
}
if (chunkRenderQueue.isEmpty()) delay(tickTime)
}
}
@@ -133,7 +192,7 @@ class World(val player: Player, val session: Session) {
}
}
var camera: Camera = Camera(Vector3d(0.0, 0.0, 0.0), 0f, 0f, 0f)
var camera: Camera = Camera(Vector3d(0.0, 0.0, 0.0), 0f, 0f, 0f, 90f)
private fun packChunkKey(x: Int, z: Int): Long =
x.toLong() and 0xFFFFFFFFL or ((z.toLong() and 0xFFFFFFFFL) shl 32)
@@ -184,7 +243,10 @@ class World(val player: Player, val session: Session) {
val blockLights = object : LightAccessor {
override operator fun get(x: Int, y: Int, z: Int): Int {
return getChunk(floorDiv(x, ChunkSection.LENGTH), floorDiv(z, ChunkSection.LENGTH))?.blockLights[floorMod(
return getChunk(
floorDiv(x, ChunkSection.LENGTH),
floorDiv(z, ChunkSection.LENGTH)
)?.blockLights[floorMod(
x,
ChunkSection.LENGTH
), y, floorMod(z, ChunkSection.LENGTH)] ?: 0
@@ -215,6 +277,8 @@ class World(val player: Player, val session: Session) {
camera = player.getCamera(alpha)
Game.updateFov(camera.fov)
val view = Matrix4f().identity().rotateX(-camera.pitch).rotateY(-camera.yaw) // copy the camera matrix
// Now upload
@@ -227,10 +291,46 @@ class World(val player: Player, val session: Session) {
(player.position.z / ChunkSection.LENGTH).toInt()
)
val renderDistance = Settings.get(SettingKey.RENDERDISTANCE) ?: 32
for (dx in -renderDistance - 1..renderDistance) {
for (dz in -renderDistance - 1..renderDistance) {
val chunk = getChunk(centerChunk.x + dx, centerChunk.y + dz)
chunk?.render(alpha)
// --- Unload distant chunks ---
val chunksToUnload = chunks.values.filter { chunk ->
val dx = chunk.coords.x - centerChunk.x
val dz = chunk.coords.y - centerChunk.y
val dist2 = dx * dx + dz * dz
dist2 > renderDistance * renderDistance
}
for (chunk in chunksToUnload) {
chunks.remove(packChunkKey(chunk.coords.x, chunk.coords.y))
chunk.unload() // optional cleanup
}
// --- Collect coordinates in a circle ---
val chunkCoords = mutableListOf<Pair<Long, Vector2i>>()
for (dx in -renderDistance..renderDistance) {
for (dz in -renderDistance..renderDistance) {
if (dx*dx + dz*dz > renderDistance*renderDistance) continue // circular radius
val chunkX = centerChunk.x + dx
val chunkZ = centerChunk.y + dz
val key = packChunkKey(chunkX, chunkZ)
chunkCoords.add(key to Vector2i(chunkX, chunkZ))
}
}
// --- Sort by distance squared (closest first) ---
chunkCoords.sortBy { (_, coords) ->
val dx = coords.x - centerChunk.x
val dz = coords.y - centerChunk.y
dx*dx + dz*dz
}
// --- Load/render chunks in order ---
for ((key, coords) in chunkCoords) {
val chunk = getChunk(coords.x, coords.y)
if (chunk == null) {
toLoadChunks[key] = coords
} else {
chunk.render(alpha)
}
}
}
@@ -20,6 +20,7 @@ import org.lwjgl.opengl.GL15.glGenBuffers
import org.lwjgl.opengl.GL20.glEnableVertexAttribArray
import org.lwjgl.opengl.GL20.glVertexAttribPointer
import org.lwjgl.opengl.GL30.*
import java.util.Collections
interface BlockAccessor {
@@ -41,7 +42,10 @@ class ChunkSection(val blockData: Array<Block?>, val chunk: Chunk, val index: In
val sunLightData = ByteArray(LENGTH * LENGTH * LENGTH / 2) { 0 }
val blockLightData = ByteArray(LENGTH * LENGTH * LENGTH / 2) { 0 }
var sunlightDataCalculated = false
fun calculateSunLight() {
if (sunlightDataCalculated) return
// 2. BFS propagation
val queue: ArrayDeque<Triple<Int, Int, Int>> = ArrayDeque() // store coordinates only
val propagation = arrayOf(
@@ -93,6 +97,7 @@ class ChunkSection(val blockData: Array<Block?>, val chunk: Chunk, val index: In
queue.addLast(Triple(nx, ny, nz))
}
}
sunlightDataCalculated = true
}
@@ -102,7 +107,7 @@ class ChunkSection(val blockData: Array<Block?>, val chunk: Chunk, val index: In
val byteIndex = index / 2
val high = index % 2 == 0
val v = value.coerceIn(0, 15)
val oldData = get(x,y,z)
val oldData = get(x, y, z)
if (high) {
sunLightData[byteIndex] = ((v shl 4) or (sunLightData[byteIndex].toInt() and 0x0F)).toByte()
} else {
@@ -112,10 +117,13 @@ class ChunkSection(val blockData: Array<Block?>, val chunk: Chunk, val index: In
chunk.world.getChunk(chunk.coords.x - 1, chunk.coords.y)?.getChunkSectionByIndex(index)?.updateMesh =
true
}
if (oldData != get(x,y,z)) {
if (oldData != get(x, y, z)) {
updateMesh = true
sunlightDataCalculated = false
if (z == 0) chunk.world.getChunk(chunk.coords.x, chunk.coords.y - 1)
?.getChunkSectionByIndex(index)?.updateMesh = true
if (x == 0) chunk.world.getChunk(chunk.coords.x - 1, chunk.coords.y)
?.getChunkSectionByIndex(index)?.updateMesh = true
if (x == LENGTH - 1) chunk.world.getChunk(chunk.coords.x + 1, chunk.coords.y)
?.getChunkSectionByIndex(index)?.updateMesh = true
if (z == LENGTH - 1) chunk.world.getChunk(chunk.coords.x, chunk.coords.y + 1)
@@ -201,29 +209,31 @@ class ChunkSection(val blockData: Array<Block?>, val chunk: Chunk, val index: In
var vbo: Int = 0
var timeMeshCreated = 0.0
var updateMesh: Boolean = true
var verticesForUpload: MutableList<Float>? = null
var verticesForUpload = Collections.synchronizedList(mutableListOf<Float>())
var verticesReadyForUpload = false
fun renderMesh(): Boolean {
if (!updateMesh) return false
verticesForUpload = mutableListOf<Float>()
setVertices(verticesForUpload!!)
verticesForUpload.clear()
setVertices(verticesForUpload)
updateMesh = false
verticesReadyForUpload = true
return true
}
fun uploadMesh() {
if (verticesForUpload == null) return
if (!verticesReadyForUpload) return
val newVertexCount = verticesForUpload!!.size
val newVertexCount = verticesForUpload.size
if (verticesForUpload!!.isEmpty()) {
if (verticesForUpload.isEmpty()) {
// If no vertices, clear mesh
if (meshExists) {
glDeleteBuffers(vbo)
glDeleteVertexArrays(vao)
}
meshExists = false
verticesForUpload = null
verticesReadyForUpload = false
timeMeshCreated = glfwGetTime()
vao = 0
vbo = 0
@@ -261,16 +271,21 @@ class ChunkSection(val blockData: Array<Block?>, val chunk: Chunk, val index: In
glBindBuffer(GL_ARRAY_BUFFER, vbo)
if (newVertexCount > vaoSize) {
// Reallocate if new data won't fit
glBufferData(GL_ARRAY_BUFFER, verticesForUpload!!.toFloatArray(), GL_DYNAMIC_DRAW)
synchronized(verticesForUpload) {
glBufferData(GL_ARRAY_BUFFER, verticesForUpload!!.toFloatArray(), GL_DYNAMIC_DRAW)
}
} else {
// Just update existing buffer
glBufferSubData(GL_ARRAY_BUFFER, 0, verticesForUpload!!.toFloatArray())
synchronized(verticesForUpload!!) {
glBufferSubData(GL_ARRAY_BUFFER, 0, verticesForUpload!!.toFloatArray())
}
}
glBindBuffer(GL_ARRAY_BUFFER, 0)
vaoSize = newVertexCount
updateMesh = false
verticesForUpload = null
verticesReadyForUpload = false
verticesForUpload.clear()
timeMeshCreated = glfwGetTime()
}
@@ -370,6 +385,15 @@ class ChunkSection(val blockData: Array<Block?>, val chunk: Chunk, val index: In
}
}
}
fun unload() {
if (meshExists) {
glDeleteBuffers(vbo)
glDeleteVertexArrays(vao)
meshExists = false
}
updateMesh = false
}
}
class Chunk(
@@ -377,73 +401,95 @@ class Chunk(
val coords: Vector2i,
val numberOfSections: Int,
val ySectionsOffset: Int,
val blockData: Array<Block?>
var blockData: Array<Block?>?,
val peak: Int
) {
var sections: Array<ChunkSection> = Array(numberOfSections) { sectionIndex ->
ChunkSection(Array(ChunkSection.SIZE) { i ->
val x = i % ChunkSection.LENGTH
val z = (i / ChunkSection.LENGTH) % ChunkSection.LENGTH
val y = (i / (ChunkSection.LENGTH * ChunkSection.LENGTH)) + sectionIndex * ChunkSection.LENGTH
blockData[x + z * ChunkSection.LENGTH + y * ChunkSection.LENGTH * ChunkSection.LENGTH]
val x = i % LENGTH
val z = (i / LENGTH) % LENGTH
val y = (i / (LENGTH * LENGTH)) + sectionIndex * LENGTH
blockData!![x + z * LENGTH + y * LENGTH * LENGTH]
}, this, sectionIndex)
}
init {
blockData = null
}
var loaded = true
var verticalSunCalculated = false
fun initVerticalSun() {
if (!loaded) return
val topY = (numberOfSections - ySectionsOffset) * LENGTH - 1
val bottomY = peak + 1
for (x in 0 until LENGTH) {
for (z in 0 until LENGTH) {
for (y in LENGTH - 1 downTo 0) {
if (blocks[x,y,z] != null) break
sunLights[x,y,z] = 15
for (y in topY downTo bottomY) {
sunLights[x, y, z] = 15
if (peak < y - LENGTH) getChunkSection(y)?.sunlightDataCalculated = true
}
}
}
verticalSunCalculated = true
}
fun getChunkSectionByIndex(index: Int): ChunkSection? {
if (!loaded) return null
if (index !in 0..<numberOfSections) return null
return sections[index]
}
fun getChunkSection(y: Int): ChunkSection? {
val sectionIndex = floorDiv((y + ySectionsOffset * ChunkSection.LENGTH), ChunkSection.LENGTH)
if (!loaded) return null
val sectionIndex = floorDiv((y + ySectionsOffset * LENGTH), LENGTH)
if (sectionIndex !in 0..<numberOfSections) return null
return sections[sectionIndex]
}
val blocks = object : BlockAccessor {
override operator fun get(x: Int, y: Int, z: Int): Block? {
return getChunkSection(y)?.blocks[x, floorMod(y, ChunkSection.LENGTH), z]
if (!loaded) return null
return getChunkSection(y)?.blocks[x, floorMod(y, LENGTH), z]
}
override operator fun set(x: Int, y: Int, z: Int, value: Block?) {
getChunkSection(y)?.blocks[x, floorMod(y, ChunkSection.LENGTH), z] = value
if (!loaded) return
getChunkSection(y)?.blocks[x, floorMod(y, LENGTH), z] = value
}
}
val sunLights = object : LightAccessor {
override operator fun set(x: Int, y: Int, z: Int, value: Int) {
getChunkSection(y)?.sunLights[x, floorMod(y, ChunkSection.LENGTH), z] = value
if (!loaded) return
getChunkSection(y)?.sunLights[x, floorMod(y, LENGTH), z] = value
}
override operator fun get(x: Int, y: Int, z: Int): Int {
return getChunkSection(y)?.sunLights[x, floorMod(y, ChunkSection.LENGTH), z] ?: if (y > 0) 15 else 0
if (!loaded) return if (y > 0) 15 else 0
return getChunkSection(y)?.sunLights[x, floorMod(y, LENGTH), z] ?: if (y > 0) 15 else 0
}
}
val blockLights = object : LightAccessor {
override operator fun set(x: Int, y: Int, z: Int, value: Int) {
getChunkSection(y)?.blockLights[x, floorMod(y, ChunkSection.LENGTH), z] = value
if (!loaded) return
getChunkSection(y)?.blockLights[x, floorMod(y, LENGTH), z] = value
}
override operator fun get(x: Int, y: Int, z: Int): Int {
return getChunkSection(y)?.blockLights[x, floorMod(y, ChunkSection.LENGTH), z] ?: 0
if (!loaded) return 0
return getChunkSection(y)?.blockLights[x, floorMod(y, LENGTH), z] ?: 0
}
}
var isQueuedForRender = false
fun toRenderMesh(): Boolean {
if (!loaded) return false
var toRender = false
for (i in 0..<numberOfSections) {
if (sections[i].updateMesh) toRender = true
@@ -452,6 +498,7 @@ class Chunk(
}
fun renderMesh(): Boolean {
if (!loaded) return false
initVerticalSun()
var hasRendered = false
for (i in (numberOfSections - 1) downTo 0) {
@@ -486,6 +533,7 @@ class Chunk(
}
fun uploadChunkMesh() {
if (!loaded) return
for (i in 0..<numberOfSections) {
sections[i].uploadMesh()
}
@@ -493,10 +541,14 @@ class Chunk(
fun render(alpha: Double) {
if (!loaded) return
for (i in 0..<numberOfSections) {
sections[i].render(alpha)
}
}
fun unload() {}
fun unload() {
sections.forEach { it.unload() }
loaded = false
}
}
@@ -3,6 +3,8 @@ package dev.wbell.buildtopia.app.game.session.world.player
import dev.wbell.buildtopia.app.game.Game
import dev.wbell.buildtopia.app.game.camera.Camera
import dev.wbell.buildtopia.app.game.session.world.World
import dev.wbell.buildtopia.app.game.settings.SettingKey
import dev.wbell.buildtopia.app.game.settings.Settings
import org.joml.Matrix4d
import org.joml.Vector2d
import org.joml.Vector2f
@@ -181,7 +183,7 @@ class Player(val position: Vector3d, var pitch: Float, var yaw: Float) {
lerp(lastPosition.x, position.x, alpha),
lerp(lastPosition.y, position.y, alpha) + 1,
lerp(lastPosition.z, position.z, alpha)
), pitch, yaw, 0f
), pitch, yaw, 0f, (Settings[SettingKey.FOV]?:90).toFloat()
)
}
@@ -10,12 +10,13 @@ sealed class SettingKey<T>(val key: String) {
object Settings {
private val data = mutableMapOf<SettingKey<*>, Any>()
fun <T : Any> set(key: SettingKey<T>, value: T) {
@Suppress("UNCHECKED_CAST")
operator fun <T : Any> set(key: SettingKey<T>, value: T) {
data[key] = value
}
@Suppress("UNCHECKED_CAST")
fun <T : Any> get(key: SettingKey<T>): T? {
operator fun <T : Any> get(key: SettingKey<T>): T? {
val value = data[key]
return value as T?
}