mirror of
https://github.com/Ugric/Buildtopia.git
synced 2026-10-28 09:43:46 +00:00
add noise based terrain, with caves, and better mesh render times.
This commit is contained in:
1 parent
00f639451e
commit
c0586fd60a
12 files changed
+315
-132
No files matched your search
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="project">
|
||||
<words>
|
||||
<w>blocktopia</w>
|
||||
<w>coords</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
Generated
+3
@@ -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>
|
||||
Generated
+39
-15
@@ -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">{
|
||||
"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",
|
||||
"git-widget-placeholder": "master",
|
||||
"kotlin-language-version-configured": "true"
|
||||
<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>
|
||||
@@ -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() {
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ class Camera(
|
||||
val position: Vector3d,
|
||||
var pitch: Float,
|
||||
var yaw: Float,
|
||||
var roll: Float
|
||||
var roll: Float,
|
||||
var fov: Float
|
||||
) {
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
File renamed without changes.
File renamed without changes.
Reference in new issue
Block a user