messy state

This commit is contained in:
2026-03-12 19:25:03 +03:30
commit a47f1f9818
30 changed files with 2791 additions and 0 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
rustflags = ["-Ctarget-cpu=native"]

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
.idea

228
Cargo.lock generated Normal file
View File

@@ -0,0 +1,228 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "fastnoise-lite"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5f3c3cc9081e5d0e18bcd50e80cd33cba47fc22f88a9da9c33ecd1c87ea5c0"
dependencies = [
"num-traits",
]
[[package]]
name = "fastvoxel"
version = "0.1.0"
dependencies = [
"fastnoise-lite",
"godot",
]
[[package]]
name = "gdextension-api"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e25d88dabe9fdb2e064cb545312178ec4025eefb62d9a3bbce1769088e2c381d"
[[package]]
name = "glam"
version = "0.30.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46"
[[package]]
name = "godot"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a9b5fac6f08345fe81f840353b433c3e8cd07a105cb23ea080251173c149625"
dependencies = [
"godot-core",
"godot-macros",
]
[[package]]
name = "godot-bindings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96920225203f1ccee3d1fd4513402a31a4d6567e437367c704682f7eee43eda3"
dependencies = [
"gdextension-api",
]
[[package]]
name = "godot-cell"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb34522bce7b863ef4dbf154be53392f23e32747877b4c7e759a0ec43876b3f"
[[package]]
name = "godot-codegen"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396303aaca6c212cf85442e78afcf28844b06b564bbc23f37c40ec57885f4395"
dependencies = [
"godot-bindings",
"heck",
"nanoserde",
"proc-macro2",
"quote",
"regex",
]
[[package]]
name = "godot-core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be623c121c8cd24a0e173771d73a0cbf8180840abd4389f4c78e698018d729ae"
dependencies = [
"glam",
"godot-bindings",
"godot-cell",
"godot-codegen",
"godot-ffi",
]
[[package]]
name = "godot-ffi"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a8358c83761a174b33640944e72ec2fabf0274f516520b68ab8a140ac25d97"
dependencies = [
"godot-bindings",
"godot-codegen",
"godot-macros",
"libc",
]
[[package]]
name = "godot-macros"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96066983ab510b0f89ea2f5b3e1dc47eaf9126f0f9e595da6cba4e7bf49a84dd"
dependencies = [
"godot-bindings",
"proc-macro2",
"quote",
"venial",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "nanoserde"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a36fb3a748a4c9736ed7aeb5f2dfc99665247f1ce306abbddb2bf0ba2ac530a4"
dependencies = [
"nanoserde-derive",
]
[[package]]
name = "nanoserde-derive"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a846cbc04412cf509efcd8f3694b114fc700a035fb5a37f21517f9fb019f1ebc"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "venial"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a42528baceab6c7784446df2a10f4185078c39bf73dc614f154353f1a6b1229"
dependencies = [
"proc-macro2",
"quote",
]

21
Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "fastvoxel"
version = "0.1.0"
edition = "2024"
[dependencies]
godot = "0.4.2"
fastnoise-lite = "*"
[lib]
crate-type = ["cdylib"]
[profile.all]
inherits = "release"
opt-level = 3
debug = false
strip = true
lto = true
codegen-units = 1
incremental = false
panic = "abort"

28
build.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
# Exit on error
set -e
# Configuration
GODOT_PROJECT="../minekoloft" # relative path to Godot project
LIB_NAME="fastvoxel" # your library name
DEST_LINUX="$GODOT_PROJECT/addons/fastvoxel/bin/linux"
DEST_WINDOWS="$GODOT_PROJECT/addons/fastvoxel/bin/windows"
# Build for Linux (native)
echo "Building for Linux (native)..."
cargo build --release
# Copy the shared library
mkdir -p "$DEST_LINUX"
cp "target/release/lib${LIB_NAME}.so" "$DEST_LINUX/"
echo "Copied to $DEST_LINUX"
# Optional: Crosscompile for Windows (if needed)
# Uncomment the following lines if you have the Windows target installed
echo "Building for Windows (cross-compile)..."
cargo build --release --target x86_64-pc-windows-gnu
mkdir -p "$DEST_WINDOWS"
cp "target/x86_64-pc-windows-gnu/release/${LIB_NAME}.dll" "$DEST_WINDOWS/"
echo "Copied to $DEST_WINDOWS"
echo "Done."

124
logo-cropped.svg Normal file
View File

@@ -0,0 +1,124 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="358.52 293.35 387.37 447.28">
<defs>
<style>
.cls-1 {
fill: #e83045;
}
.cls-2 {
fill: #196781;
}
.cls-3 {
stroke: #196781;
}
.cls-3, .cls-4, .cls-5 {
fill: none;
stroke-miterlimit: 10;
stroke-width: 4px;
}
.cls-6 {
fill: #09c5e8;
}
.cls-7 {
fill: #08c5e8;
}
.cls-8 {
fill: #ff364f;
}
.cls-9 {
fill: #042b41;
}
.cls-10 {
fill: #052940;
}
.cls-11 {
fill: #09536d;
}
.cls-12 {
opacity: .44;
}
.cls-4 {
stroke: #ff364f;
}
.cls-5 {
stroke: #09c5e8;
}
</style>
</defs>
<g>
<polygon class="cls-9" points="358.54 405.17 358.54 628.8 552.21 740.61 745.88 628.8 745.88 405.17 552.21 293.36 358.54 405.17"/>
<g>
<g>
<polygon class="cls-11" points="377.75 618.29 377.75 421.86 547.03 520.23 547.01 715.64 377.75 618.29"/>
<path class="cls-7" d="M382.75,430.55l159.27,92.56v183.9s-159.27-91.6-159.27-91.6v-184.85M372.75,413.17v208.01l179.26,103.11v-206.94s-179.26-104.18-179.26-104.18h0Z"/>
</g>
<g>
<polygon class="cls-1" points="382.73 413.18 552.03 315.46 721.67 413.18 552.03 511.57 382.73 413.18"/>
<path class="cls-8" d="M552.03,321.23l159.65,91.97-159.65,92.59-159.33-92.59,159.33-91.97M552.03,309.69l-179.27,103.48,179.27,104.18,179.63-104.18-179.63-103.48h0Z"/>
</g>
<g>
<polygon class="cls-10" points="557.03 520.23 726.66 421.85 726.66 618.28 557.03 715.64 557.03 520.23"/>
<path class="cls-2" d="M721.66,430.53v184.86l-159.63,91.62v-183.9s159.63-92.58,159.63-92.58M731.66,413.17l-179.63,104.18v206.92s179.63-103.1,179.63-103.1v-208.01h0Z"/>
</g>
<g>
<line class="cls-4" x1="470.8" y1="460.08" x2="633.26" y2="366.27"/>
<line class="cls-8" x1="470.8" y1="460.08" x2="633.26" y2="366.27"/>
</g>
<g>
<line class="cls-4" x1="633.26" y1="460.08" x2="470.79" y2="366.27"/>
<line class="cls-8" x1="633.26" y1="460.08" x2="470.79" y2="366.27"/>
</g>
<g>
<line class="cls-5" x1="381.26" y1="521.89" x2="543.52" y2="615.57"/>
<line class="cls-6" x1="381.26" y1="521.89" x2="543.52" y2="615.57"/>
</g>
<g>
<line class="cls-5" x1="462.39" y1="663.31" x2="462.39" y2="474.15"/>
<line class="cls-6" x1="462.39" y1="663.31" x2="462.39" y2="474.15"/>
</g>
<g>
<line class="cls-3" x1="722.98" y1="521.88" x2="560.71" y2="615.56"/>
<line class="cls-2" x1="722.98" y1="521.88" x2="560.71" y2="615.56"/>
</g>
<g>
<line class="cls-3" x1="641.85" y1="663.3" x2="641.85" y2="474.14"/>
<line class="cls-2" x1="641.85" y1="663.3" x2="641.85" y2="474.14"/>
</g>
</g>
</g>
<g class="cls-12">
<path class="cls-7" d="M428.36,525.2c0-.46-7.46-4.7-7.61-5.14l-.03-39.29-.32-.45-5.66-1.61c-.19-.05-.52-.52-.52-.63v-7.33l.84-.29c3.36,1.24,7.01,1.69,10.39,2.83.62.21,2.87,1.25,3.02,1.77l-.11,50.14h0Z"/>
</g>
<g class="cls-12">
<path class="cls-7" d="M510.23,666.82c0-.46-7.46-4.7-7.61-5.14l-.03-39.29-.32-.45-5.66-1.61c-.19-.05-.52-.52-.52-.63v-7.33l.84-.29c3.36,1.24,7.01,1.69,10.39,2.83.62.21,2.87,1.25,3.02,1.77l-.11,50.14h0Z"/>
</g>
<g class="cls-12">
<path class="cls-7" d="M428.93,572.8c8.06,8.23,12.88,19.99,12.83,31.59-.07,17.75-11.58,21.25-24.63,10.78-8.62-6.92-13.72-17.36-15.59-28.15-.8-4.62-.93-8.43.23-13.03,4.19-16.62,18.68-9.84,27.16-1.18ZM417.82,607.26c5.06,4.82,12.6,7.3,15.18-1.41,2.47-8.32-.82-20.22-6.46-26.7-5.43-6.24-14.57-9.82-17.04,1.05-2.05,9.04,1.67,20.74,8.32,27.07Z"/>
</g>
<g class="cls-12">
<path class="cls-7" d="M510.8,525.54c8.06,8.23,12.88,19.99,12.83,31.59-.07,17.75-11.58,21.25-24.63,10.78-8.62-6.92-13.72-17.36-15.59-28.15-.8-4.62-.93-8.43.23-13.03,4.19-16.62,18.68-9.84,27.16-1.18ZM499.7,559.99c5.06,4.82,12.6,7.3,15.18-1.41,2.47-8.32-.82-20.22-6.46-26.7-5.43-6.24-14.57-9.82-17.04,1.05-2.05,9.04,1.67,20.74,8.32,27.07Z"/>
</g>
<g class="cls-12">
<path class="cls-2" d="M620.49,621.02c1.16,4.6,1.03,8.41.23,13.03-1.88,10.8-6.97,21.24-15.59,28.15-13.05,10.47-24.56,6.97-24.63-10.78-.04-11.6,4.77-23.36,12.83-31.59,8.48-8.66,22.97-15.43,27.16,1.18ZM612.76,627.22c-2.47-10.87-11.61-7.29-17.04-1.05s-8.93,18.38-6.46,26.7c2.59,8.72,10.13,6.23,15.18,1.41,6.64-6.33,10.37-18.03,8.32-27.07Z"/>
</g>
<g class="cls-12">
<path class="cls-2" d="M702.27,479.52c1.16,4.6,1.03,8.41.23,13.03-1.88,10.8-6.97,21.24-15.59,28.15-13.05,10.47-24.56,6.97-24.63-10.78-.04-11.6,4.77-23.36,12.83-31.59,8.48-8.66,22.97-15.43,27.16,1.18ZM694.53,485.72c-2.47-10.87-11.61-7.29-17.04-1.05s-8.93,18.38-6.46,26.7c2.59,8.72,10.13,6.23,15.18,1.41,6.64-6.33,10.37-18.03,8.32-27.07Z"/>
</g>
<g class="cls-12">
<path class="cls-2" d="M608.18,517.65v50.34s-7.55,4.71-7.55,4.71l-.39-.21v-39.78c-.74-.16-.79.53-1.18.86-1.67,1.43-3.42,2.81-5.01,4.33l-.39-.2v-7.62c0-.24.92-1.13,1.16-1.37,2.85-2.8,6.41-5.84,9.47-8.42.95-.8,1.93-1.97,3.13-2.51.26-.12.49-.13.77-.11Z"/>
</g>
<g class="cls-12">
<path class="cls-2" d="M689.91,564.76v50.34s-7.55,4.71-7.55,4.71l-.39-.21v-39.78c-.74-.16-.79.53-1.18.86-1.67,1.43-3.42,2.81-5.01,4.33l-.39-.2v-7.62c0-.24.92-1.13,1.16-1.37,2.85-2.8,6.41-5.84,9.47-8.42.95-.8,1.93-1.97,3.13-2.51.26-.12.49-.13.77-.11Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

125
logo.svg Normal file
View File

@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 1080">
<defs>
<style>
.cls-1 {
fill: #e83045;
}
.cls-2 {
fill: #196781;
}
.cls-3 {
stroke: #196781;
}
.cls-3, .cls-4, .cls-5 {
fill: none;
stroke-miterlimit: 10;
stroke-width: 4px;
}
.cls-6 {
fill: #09c5e8;
}
.cls-7 {
fill: #08c5e8;
}
.cls-8 {
fill: #ff364f;
}
.cls-9 {
fill: #042b41;
}
.cls-10 {
fill: #052940;
}
.cls-11 {
fill: #09536d;
}
.cls-12 {
opacity: .44;
}
.cls-4 {
stroke: #ff364f;
}
.cls-5 {
stroke: #09c5e8;
}
</style>
</defs>
<g>
<polygon class="cls-9" points="358.54 405.17 358.54 628.8 552.21 740.61 745.88 628.8 745.88 405.17 552.21 293.36 358.54 405.17"/>
<g>
<g>
<polygon class="cls-11" points="377.75 618.29 377.75 421.86 547.03 520.23 547.01 715.64 377.75 618.29"/>
<path class="cls-7" d="M382.75,430.55l159.27,92.56v183.9s-159.27-91.6-159.27-91.6v-184.85M372.75,413.17v208.01l179.26,103.11v-206.94s-179.26-104.18-179.26-104.18h0Z"/>
</g>
<g>
<polygon class="cls-1" points="382.73 413.18 552.03 315.46 721.67 413.18 552.03 511.57 382.73 413.18"/>
<path class="cls-8" d="M552.03,321.23l159.65,91.97-159.65,92.59-159.33-92.59,159.33-91.97M552.03,309.69l-179.27,103.48,179.27,104.18,179.63-104.18-179.63-103.48h0Z"/>
</g>
<g>
<polygon class="cls-10" points="557.03 520.23 726.66 421.85 726.66 618.28 557.03 715.64 557.03 520.23"/>
<path class="cls-2" d="M721.66,430.53v184.86l-159.63,91.62v-183.9s159.63-92.58,159.63-92.58M731.66,413.17l-179.63,104.18v206.92s179.63-103.1,179.63-103.1v-208.01h0Z"/>
</g>
<g>
<line class="cls-4" x1="470.8" y1="460.08" x2="633.26" y2="366.27"/>
<line class="cls-8" x1="470.8" y1="460.08" x2="633.26" y2="366.27"/>
</g>
<g>
<line class="cls-4" x1="633.26" y1="460.08" x2="470.79" y2="366.27"/>
<line class="cls-8" x1="633.26" y1="460.08" x2="470.79" y2="366.27"/>
</g>
<g>
<line class="cls-5" x1="381.26" y1="521.89" x2="543.52" y2="615.57"/>
<line class="cls-6" x1="381.26" y1="521.89" x2="543.52" y2="615.57"/>
</g>
<g>
<line class="cls-5" x1="462.39" y1="663.31" x2="462.39" y2="474.15"/>
<line class="cls-6" x1="462.39" y1="663.31" x2="462.39" y2="474.15"/>
</g>
<g>
<line class="cls-3" x1="722.98" y1="521.88" x2="560.71" y2="615.56"/>
<line class="cls-2" x1="722.98" y1="521.88" x2="560.71" y2="615.56"/>
</g>
<g>
<line class="cls-3" x1="641.85" y1="663.3" x2="641.85" y2="474.14"/>
<line class="cls-2" x1="641.85" y1="663.3" x2="641.85" y2="474.14"/>
</g>
</g>
</g>
<g class="cls-12">
<path class="cls-7" d="M428.36,525.2c0-.46-7.46-4.7-7.61-5.14l-.03-39.29-.32-.45-5.66-1.61c-.19-.05-.52-.52-.52-.63v-7.33l.84-.29c3.36,1.24,7.01,1.69,10.39,2.83.62.21,2.87,1.25,3.02,1.77l-.11,50.14h0Z"/>
</g>
<g class="cls-12">
<path class="cls-7" d="M510.23,666.82c0-.46-7.46-4.7-7.61-5.14l-.03-39.29-.32-.45-5.66-1.61c-.19-.05-.52-.52-.52-.63v-7.33l.84-.29c3.36,1.24,7.01,1.69,10.39,2.83.62.21,2.87,1.25,3.02,1.77l-.11,50.14h0Z"/>
</g>
<g class="cls-12">
<path class="cls-7" d="M428.93,572.8c8.06,8.23,12.88,19.99,12.83,31.59-.07,17.75-11.58,21.25-24.63,10.78-8.62-6.92-13.72-17.36-15.59-28.15-.8-4.62-.93-8.43.23-13.03,4.19-16.62,18.68-9.84,27.16-1.18ZM417.82,607.26c5.06,4.82,12.6,7.3,15.18-1.41,2.47-8.32-.82-20.22-6.46-26.7-5.43-6.24-14.57-9.82-17.04,1.05-2.05,9.04,1.67,20.74,8.32,27.07Z"/>
</g>
<g class="cls-12">
<path class="cls-7" d="M510.8,525.54c8.06,8.23,12.88,19.99,12.83,31.59-.07,17.75-11.58,21.25-24.63,10.78-8.62-6.92-13.72-17.36-15.59-28.15-.8-4.62-.93-8.43.23-13.03,4.19-16.62,18.68-9.84,27.16-1.18ZM499.7,559.99c5.06,4.82,12.6,7.3,15.18-1.41,2.47-8.32-.82-20.22-6.46-26.7-5.43-6.24-14.57-9.82-17.04,1.05-2.05,9.04,1.67,20.74,8.32,27.07Z"/>
</g>
<g class="cls-12">
<path class="cls-2" d="M620.49,621.02c1.16,4.6,1.03,8.41.23,13.03-1.88,10.8-6.97,21.24-15.59,28.15-13.05,10.47-24.56,6.97-24.63-10.78-.04-11.6,4.77-23.36,12.83-31.59,8.48-8.66,22.97-15.43,27.16,1.18ZM612.76,627.22c-2.47-10.87-11.61-7.29-17.04-1.05s-8.93,18.38-6.46,26.7c2.59,8.72,10.13,6.23,15.18,1.41,6.64-6.33,10.37-18.03,8.32-27.07Z"/>
</g>
<g class="cls-12">
<path class="cls-2" d="M702.27,479.52c1.16,4.6,1.03,8.41.23,13.03-1.88,10.8-6.97,21.24-15.59,28.15-13.05,10.47-24.56,6.97-24.63-10.78-.04-11.6,4.77-23.36,12.83-31.59,8.48-8.66,22.97-15.43,27.16,1.18ZM694.53,485.72c-2.47-10.87-11.61-7.29-17.04-1.05s-8.93,18.38-6.46,26.7c2.59,8.72,10.13,6.23,15.18,1.41,6.64-6.33,10.37-18.03,8.32-27.07Z"/>
</g>
<g class="cls-12">
<path class="cls-2" d="M608.18,517.65v50.34s-7.55,4.71-7.55,4.71l-.39-.21v-39.78c-.74-.16-.79.53-1.18.86-1.67,1.43-3.42,2.81-5.01,4.33l-.39-.2v-7.62c0-.24.92-1.13,1.16-1.37,2.85-2.8,6.41-5.84,9.47-8.42.95-.8,1.93-1.97,3.13-2.51.26-.12.49-.13.77-.11Z"/>
</g>
<g class="cls-12">
<path class="cls-2" d="M689.91,564.76v50.34s-7.55,4.71-7.55,4.71l-.39-.21v-39.78c-.74-.16-.79.53-1.18.86-1.67,1.43-3.42,2.81-5.01,4.33l-.39-.2v-7.62c0-.24.92-1.13,1.16-1.37,2.85-2.8,6.41-5.84,9.47-8.42.95-.8,1.93-1.97,3.13-2.51.26-.12.49-.13.77-.11Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

108
src/chunk/chunk.rs Normal file
View File

@@ -0,0 +1,108 @@
use godot::prelude::*;
pub const CHUNK_SIZE: i32 = 32;
pub const ARR_SIZE: usize = CHUNK_SIZE.pow(3) as usize;
// 32x32x32 = 32768 bits, stored in u32s (32 bits each) = 1024 u32s
pub const BITPACKED_SIZE: usize = ARR_SIZE / 32;
#[derive(Debug)]
pub struct Chunk {
// Bitpacked voxel data: 0 = air, 1 = solid
pub voxels: Vec<u32>,
// WORLD COORDINATES
pub world_position: (f64, f64, f64),
modified: bool,
}
impl Chunk {
/// pos_z and pos_x are `world` coordinates of the chunk, inside the world
pub fn new(world_pos_x: f64, world_pos_y: f64, world_pos_z: f64, size: Vector3i) -> Self {
Chunk {
voxels: vec![0u32; BITPACKED_SIZE],
world_position: (world_pos_x, world_pos_y, world_pos_z),
modified: false,
}
}
#[inline]
pub fn get_voxel_index(&self, local_pos: Vector3i, chunk_size: Vector3i) -> usize {
((local_pos.x * chunk_size.y * chunk_size.z) + (local_pos.y * chunk_size.x) + local_pos.z)
as usize
}
#[inline]
pub fn is_voxel_within_bounds(&self, local_pos: Vector3i, chunk_size: Vector3i) -> bool {
local_pos.x >= 0
&& local_pos.x < chunk_size.x
&& local_pos.y >= 0
&& local_pos.y < chunk_size.y
&& local_pos.z >= 0
&& local_pos.z < chunk_size.z
}
#[inline]
pub fn get_voxel(&self, local_pos: Vector3i, chunk_size: Vector3i) -> bool {
debug_assert!(
self.is_voxel_within_bounds(local_pos, chunk_size),
"Voxel position {:?} out of chunk bounds",
local_pos
);
let index = self.get_voxel_index(local_pos, chunk_size);
debug_assert!(
index < ARR_SIZE,
"Voxel index {:?} out of chunk bounds",
index
);
// Get the u32 containing this bit and the bit position within it
let word_index = index / chunk_size.x as usize;
let bit_index = index % chunk_size.x as usize;
(self.voxels[word_index] & (1 << bit_index)) != 0
}
pub fn set_voxel(&mut self, voxel_pos: Vector3i, is_solid: bool, chunk_size: Vector3i) {
debug_assert!(
self.is_voxel_within_bounds(voxel_pos, chunk_size),
"Voxel position {:?} out of chunk bounds",
voxel_pos
);
let index = self.get_voxel_index(voxel_pos, chunk_size);
let word_index = index / chunk_size.x as usize;
let bit_index = index % chunk_size.x as usize;
if is_solid {
self.voxels[word_index] |= 1 << bit_index;
} else {
self.voxels[word_index] &= !(1 << bit_index);
}
self.modified = true;
}
pub fn fill(&mut self, is_solid: bool) {
let fill_value = if is_solid { u32::MAX } else { 0 };
self.voxels.fill(fill_value);
self.modified = true;
}
pub fn get_position(&self) -> (f64, f64, f64) {
self.world_position
}
pub fn get_world_bounds(&self, chunk_size: Vector3i) -> (Vector3, Vector3) {
let (x, y, z) = self.world_position;
let min = Vector3::new(
(x * chunk_size.x as f64) as f32,
(y * chunk_size.y as f64) as f32,
(z * chunk_size.z as f64) as f32,
);
let max = Vector3::new(
((x + 1.0) * chunk_size.x as f64) as f32,
((y + 1.0) * chunk_size.y as f64) as f32,
((z + 1.0) * chunk_size.z as f64) as f32,
);
(min, max)
}
}

229
src/chunk/chunk_manager.rs Normal file
View File

@@ -0,0 +1,229 @@
use godot::classes::MeshInstance3D;
use godot::classes::class_macros::private::virtuals::Os::{Vector3, Vector3i};
use godot::global::godot_print;
use godot::obj::Gd;
use crate::chunk::{Chunk, ChunkColumn, ChunkMesh};
use crate::editor::voxel_registry::VoxelRegistry;
use crate::generation::WorldGenerator;
use crate::meshing::Mesher;
use crate::rendering::Renderer;
use std::collections::HashMap;
use std::time::Instant;
pub struct ChunkManager {
pub chunk_columns: HashMap<(i32, i32), ChunkColumn>,
pub renderer: Renderer,
pub last_player_chunk: (i32, i32),
// terrain parameteres
pub chunk_size: Vector3i,
pub terrain_height: u32,
// Track which mesh instances need to be added to scene inefficient as but who cares
// the mesher is efficient enough
pub pending_mesh_instances: Vec<Gd<MeshInstance3D>>,
}
impl ChunkManager {
pub fn new() -> Self {
Self {
chunk_columns: HashMap::new(),
renderer: Renderer::new(),
last_player_chunk: (0, 0),
chunk_size: Vector3i::ZERO,
terrain_height: 0,
pending_mesh_instances: Vec::new(),
}
}
pub fn set_chunk_size(&mut self, chunk_size: Vector3i) {
self.chunk_size = chunk_size
}
pub fn update_around_player(
&mut self,
player_world_pos: Vector3,
render_distance: i32,
registry: &VoxelRegistry,
generator: &dyn WorldGenerator,
mesher: &dyn Mesher,
) -> bool {
let player_chunk_x = player_world_pos.x as i32 / self.chunk_size.x;
let player_chunk_z = player_world_pos.z as i32 / self.chunk_size.z;
let current_chunk = (player_chunk_x, player_chunk_z);
// Early return if player is still in the same chunk
if current_chunk == self.last_player_chunk {
return false;
}
let now = Instant::now();
godot_print!("Player moved to chunk: {:?}", current_chunk);
self.last_player_chunk = current_chunk;
// Clear pending instances from previous updates
self.pending_mesh_instances.clear();
// Unload distant chunks
self.unload_distant_chunks(player_chunk_x, player_chunk_z, render_distance);
let after_unload = now.elapsed().as_micros();
// Load new chunks
self.load_chunks_around(
player_chunk_x,
player_chunk_z,
render_distance,
registry,
generator,
mesher,
);
godot_print!(
"Loading {}ms || Unloading {}us, ",
(now.elapsed().as_micros() - after_unload) / 1000,
after_unload
);
true
}
fn unload_distant_chunks(&mut self, center_x: i32, center_z: i32, distance: i32) {
let mut to_remove = Vec::new();
for (&(x, z), _) in &self.chunk_columns {
let dx = (x - center_x).abs();
let dz = (z - center_z).abs();
if dx > distance || dz > distance {
to_remove.push((x, z));
}
}
for (x, z) in to_remove {
if let Some(_) = self.chunk_columns.remove(&(x, z)) {
// Remove all chunk meshes in this column
for y in 0..8 {
self.renderer.remove_chunk((x, y, z));
}
godot_print!("Unloaded chunk column ({}, {})", x, z);
}
}
}
fn load_chunks_around(
&mut self,
center_x: i32,
center_z: i32,
distance: i32,
registry: &VoxelRegistry,
generator: &dyn WorldGenerator,
mesher: &dyn Mesher,
) {
let mut chunks_loaded = 0;
for x in (center_x - distance)..=(center_x + distance) {
for z in (center_z - distance)..=(center_z + distance) {
if !self.chunk_columns.contains_key(&(x, z)) {
self.generate_chunk_column(x, z, registry, generator, mesher);
chunks_loaded += 1;
godot_print!("Loaded chunk column ({}, {})", x, z)
}
}
}
if chunks_loaded > 0 {
godot_print!("Loaded {} new chunk columns", chunks_loaded);
}
}
pub fn generate_chunk_column(
&mut self,
x: i32,
z: i32,
registry: &VoxelRegistry,
generator: &dyn WorldGenerator,
mesher: &dyn Mesher,
) {
let mut column = ChunkColumn::new(x, z, self.chunk_size);
let start = Instant::now();
// Generate all chunks first
for i in 0..self.terrain_height {
let chunk = column.get_or_create_chunk(i as i32);
generator.generate_chunk(chunk);
}
let generate_elapsed = start.elapsed().as_micros();
godot_print!("Generation took: {}μs", generate_elapsed);
// Mesh and render each chunk
for i in 0..self.terrain_height {
if let Some(chunk) = column.get_chunk(i as usize) {
self.mesh_and_render_chunk(chunk, mesher, registry);
}
}
// Insert the column only after we're completely done with it
self.chunk_columns.insert((x, z), column);
}
fn mesh_and_render_chunk(
&mut self,
chunk: &Chunk,
mesher: &dyn Mesher,
registry: &VoxelRegistry,
) {
let mut mesh = ChunkMesh::new();
let start = Instant::now();
mesher.generate_mesh_with_registry(chunk, registry, &mut mesh);
if mesh.is_empty() {
// Silent - empty chunks are normal
return;
}
godot_print!("Meshing Took: {}μs", start.elapsed().as_micros());
let start = Instant::now();
// Use render_chunk (with colors) instead of render_chunk_with_uvs
let mesh_instance = self.renderer.render_chunk(chunk, &mesh);
godot_print!("Rendering Took: {}μs", start.elapsed().as_micros());
self.pending_mesh_instances.push(mesh_instance);
}
pub fn clear(&mut self) {
// Remove all mesh instances from renderer
self.renderer.clear();
self.chunk_columns.clear();
self.pending_mesh_instances.clear();
// self.last_player_chunk = (0, 0);
}
pub fn get_voxel_at(&self, world_pos: Vector3) -> bool {
// Convert world position to chunk coordinates
let chunk_x = (world_pos.x / 32.0).floor() as i32;
let chunk_z = (world_pos.z / 32.0).floor() as i32;
if let Some(column) = self.chunk_columns.get(&(chunk_x, chunk_z)) {
// Pass the world position directly - let column handle the conversion
column.get_voxel(Vector3i::new(
world_pos.x as i32, // Use actual world coordinates
world_pos.y as i32,
world_pos.z as i32,
))
} else {
false // Air by default
}
}
// Get pending mesh instances and clear the list
pub fn take_pending_mesh_instances(&mut self) -> Vec<Gd<MeshInstance3D>> {
std::mem::take(&mut self.pending_mesh_instances)
}
}

89
src/chunk/column.rs Normal file
View File

@@ -0,0 +1,89 @@
use godot::classes::class_macros::private::virtuals::Os::Vector3i;
use super::chunk::{CHUNK_SIZE, Chunk};
pub const CHUNKS_PER_COLUMN: usize = 8;
pub struct ChunkColumn {
// from bottom to top
// TODO: replace with vec![]
pub chunks: [Option<Chunk>; CHUNKS_PER_COLUMN],
pub world_position: (i32, i32), // XZ
pub chunk_size: Vector3i,
}
impl ChunkColumn {
pub fn new(x: i32, z: i32, chunk_size: Vector3i) -> Self {
ChunkColumn {
chunks: [None, None, None, None, None, None, None, None],
world_position: (x, z),
chunk_size,
}
}
#[inline]
pub fn get_chunk_index(chunk_size_y: i32, world_y: i32) -> usize {
(world_y / chunk_size_y) as usize
}
#[inline]
pub fn get_local_y(chunk_size_y: i32, world_y: i32) -> i32 {
world_y % chunk_size_y
}
#[inline]
pub fn get_world_y(chunk_size_y: i32, chunk_index: i32, local_y: i32) -> i32 {
(chunk_index * chunk_size_y) + local_y
}
pub fn get_or_create_chunk(&mut self, chunk_y_index: i32) -> &mut Chunk {
if self.chunks[chunk_y_index as usize].is_none() {
let (world_x, world_z) = self.world_position;
self.chunks[chunk_y_index as usize] = Some(Chunk::new(
world_x as f64,
chunk_y_index as f64,
world_z as f64,
self.chunk_size,
));
}
self.chunks[chunk_y_index as usize].as_mut().unwrap()
}
pub fn get_chunk(&self, chunk_y_index: usize) -> Option<&Chunk> {
self.chunks[chunk_y_index].as_ref()
}
pub fn get_chunk_mut(&mut self, chunk_y_index: usize) -> Option<&mut Chunk> {
self.chunks[chunk_y_index].as_mut()
}
pub fn set_voxel(&mut self, world_pos: Vector3i, is_solid: bool) -> bool {
let chunk_index = Self::get_chunk_index(self.chunk_size.y, world_pos.y);
if chunk_index < CHUNKS_PER_COLUMN {
let size = self.chunk_size.clone();
let chunk = self.get_or_create_chunk(chunk_index as i32);
let local_x = world_pos.x.rem_euclid(size.x);
let local_y = Self::get_local_y(size.y, world_pos.y);
let local_z = world_pos.z.rem_euclid(size.z);
chunk.set_voxel(Vector3i::new(local_x, local_y, local_z), is_solid, size);
true
} else {
false
}
}
pub fn get_voxel(&self, world_pos: Vector3i) -> bool {
let chunk_index = Self::get_chunk_index(self.chunk_size.y, world_pos.y);
if let Some(chunk) = self.get_chunk(chunk_index) {
let local_x = world_pos.x.rem_euclid(CHUNK_SIZE);
let local_y = Self::get_local_y(self.chunk_size.y, world_pos.y);
let local_z = world_pos.z.rem_euclid(CHUNK_SIZE);
chunk.get_voxel(Vector3i::new(local_x, local_y, local_z), self.chunk_size)
} else {
false // Air by default
}
}
}

40
src/chunk/mesh.rs Normal file
View File

@@ -0,0 +1,40 @@
use godot::prelude::*;
#[derive(Debug)]
pub struct ChunkMesh {
pub vertices: Vec<Vector3>,
pub normals: Vec<Vector3>,
pub colors: Vec<Color>,
pub uvs: Vec<Vector2>,
pub indices: Vec<i32>,
}
impl Default for ChunkMesh {
fn default() -> Self {
Self::new()
}
}
impl ChunkMesh {
pub fn new() -> Self {
Self {
vertices: Vec::new(),
normals: Vec::new(),
colors: Vec::new(),
uvs: Vec::new(),
indices: Vec::new(),
}
}
pub fn clear(&mut self) {
self.vertices.clear();
self.normals.clear();
self.colors.clear();
self.uvs.clear();
self.indices.clear();
}
pub fn is_empty(&self) -> bool {
self.vertices.is_empty()
}
}

9
src/chunk/mod.rs Normal file
View File

@@ -0,0 +1,9 @@
pub mod chunk;
pub mod chunk_manager;
pub mod column;
pub mod mesh;
pub use chunk::Chunk;
pub use chunk_manager::ChunkManager;
pub use column::ChunkColumn;
pub use mesh::ChunkMesh;

3
src/editor/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod voxel_registry;
pub use voxel_registry::VoxelRegistry;

View File

@@ -0,0 +1,95 @@
use godot::{
classes::{
Resource,
class_macros::private::virtuals::Os::{Array, Vector2i},
},
obj::{Base, Gd},
prelude::{Export, GodotClass, GodotConvert, Var},
};
#[derive(GodotConvert, Var, Export, Debug, Clone, Copy)]
#[godot(via = i64)]
pub enum VoxelType {
Empty,
Cube,
Mesh,
Fluid,
}
impl Default for VoxelType {
fn default() -> Self {
Self::Empty
}
}
#[derive(GodotClass, Debug)]
#[class(init, base=Resource, tool)]
pub struct VoxelRegistry {
#[export]
pub models: Array<Gd<VoxelModel>>,
base: Base<Resource>,
}
#[derive(GodotClass, Debug, Clone, Copy)]
#[class(init, base=Resource, tool)]
pub struct VoxelModel {
#[export]
voxel_type: VoxelType,
#[export]
atlas_size_in_tiles: Vector2i,
#[export_group(name = "Cube Tiles")]
#[export]
tile_left: Vector2i,
#[export]
tile_right: Vector2i,
#[export]
tile_bottom: Vector2i,
#[export]
tile_top: Vector2i,
#[export]
tile_back: Vector2i,
#[export]
tile_front: Vector2i,
}
impl VoxelModel {
#[inline]
pub fn is_solid(&self) -> bool {
match self.voxel_type {
VoxelType::Empty => false,
_ => true,
}
}
#[inline]
pub fn is_empty(&self) -> bool {
match self.voxel_type {
VoxelType::Empty => true,
_ => false,
}
}
#[inline]
pub fn is_cube(&self) -> bool {
match self.voxel_type {
VoxelType::Cube => true,
_ => false,
}
}
#[inline]
pub fn get_tile_for_face(&self, face: i32) -> Vector2i {
match face {
0 => self.tile_left, // Left
1 => self.tile_right, // Right
2 => self.tile_bottom, // Bottom
3 => self.tile_top, // Top
4 => self.tile_back, // Back
5 => self.tile_front, // Front
_ => Vector2i::new(0, 0),
}
}
}

View File

@@ -0,0 +1,8 @@
use crate::chunk::Chunk;
pub trait WorldGenerator: Send + Sync {
fn generate_chunk(&self, chunk: &mut Chunk);
fn get_base_height(&self, x: f32, z: f32) -> f32;
fn should_be_solid(&self, density: f32, relative_height: f32) -> bool;
fn get_name(&self) -> &str;
}

202
src/generation/heightmap.rs Normal file
View File

@@ -0,0 +1,202 @@
use super::generator::WorldGenerator;
use crate::chunk::Chunk;
use crate::chunk::chunk::CHUNK_SIZE;
use crate::voxel::Voxel;
use fastnoise_lite::*;
use godot::classes::class_macros::private::virtuals::Os::{Vector2i, Vector3i};
pub struct HeightmapGenerator {
noise: FastNoiseLite,
noise_detail: FastNoiseLite,
noise_caves: FastNoiseLite,
terrain_height: i32,
chunk_size: Vector3i,
}
impl HeightmapGenerator {
pub fn new(seed: i64, frequency: f32, terrain_height: i32) -> Self {
// Main terrain noise
let mut noise = FastNoiseLite::new();
noise.set_seed(Some(seed as i32));
noise.set_frequency(Some(frequency * 0.5)); // Lower frequency for larger features
noise.set_noise_type(Some(fastnoise_lite::NoiseType::OpenSimplex2S));
noise.set_fractal_type(Some(fastnoise_lite::FractalType::FBm));
noise.set_fractal_octaves(Some(4));
noise.set_fractal_lacunarity(Some(2.0));
noise.set_fractal_gain(Some(0.5));
// Detail noise for small features
let mut noise_detail = FastNoiseLite::new();
noise_detail.set_seed(Some(seed as i32 + 1));
noise_detail.set_frequency(Some(frequency * 2.0));
noise_detail.set_noise_type(Some(fastnoise_lite::NoiseType::OpenSimplex2S));
// Cave noise
let mut noise_caves = FastNoiseLite::new();
noise_caves.set_seed(Some(seed as i32 + 2));
noise_caves.set_frequency(Some(frequency * 1.5));
noise_caves.set_noise_type(Some(fastnoise_lite::NoiseType::Perlin));
Self {
noise,
noise_detail,
noise_caves,
terrain_height,
chunk_size: Vector3i::new(32, 32, 32),
}
}
}
impl WorldGenerator for HeightmapGenerator {
// fn generate_chunk(&self, chunk: &mut Chunk) {
// let (chunk_x, chunk_y, chunk_z) = chunk.world_position;
// for local_x in 0..CHUNK_SIZE {
// for local_z in 0..CHUNK_SIZE {
// let world_x = chunk_x * CHUNK_SIZE as f64 + local_x as f64;
// let world_z = chunk_z * CHUNK_SIZE as f64 + local_z as f64;
// let mut noise_value = self.noise.get_noise_2d(world_x as f32, world_z as f32);
// noise_value *= 2f32;
// let height = ((noise_value + 1.0) * 0.5 * self.terrain_height as f32) as i32;
// for local_y in 0..=height.min(CHUNK_SIZE - 1) {
// let world_y = chunk_y as f64 * CHUNK_SIZE as f64 + local_y as f64;
// let voxel = if world_y < height as f64 - 3.0 {
// Voxel::Stone
// } else if world_y < height as f64 - 1.0 {
// Voxel::Dirt
// } else if world_y <= height as f64 {
// Voxel::Grass
// } else if world_y < height as f64 + 2.0 {
// Voxel::Water
// } else {
// Voxel::Grass
// };
// chunk.set_voxel(
// Vector3i::new(local_x, local_y, local_z),
// true,
// Vector3i {
// x: 32,
// y: 32,
// z: 32,
// },
// );
// }
// }
// }
// }
//
fn generate_chunk(&self, chunk: &mut Chunk) {
let (chunk_x, chunk_y, chunk_z) = chunk.world_position;
let chunk_world_y = chunk_y * self.chunk_size.y as f64;
// Early exit: check if entire chunk is above or below terrain
let chunk_min_y = chunk_world_y as f32;
let chunk_max_y = (chunk_world_y + self.chunk_size.y as f64) as f32;
// Pre-calculate all base heights for this chunk (32x32 = 1024 calculations instead of 32768)
let mut height_cache = [[0.0f32; 32]; 32];
for local_x in 0..self.chunk_size.x {
let world_x = (chunk_x * self.chunk_size.x as f64 + local_x as f64) as f32;
for local_z in 0..self.chunk_size.z {
let world_z = (chunk_z * self.chunk_size.z as f64 + local_z as f64) as f32;
height_cache[local_x as usize][local_z as usize] =
self.get_base_height(world_x, world_z);
}
}
// Check if we can fill entire chunk
let max_height = height_cache
.iter()
.flat_map(|row| row.iter())
.cloned()
.fold(f32::MIN, f32::max);
if chunk_max_y < max_height - 10.0 {
// Entire chunk is underground - fill it
chunk.fill(true);
return;
}
let min_height = height_cache
.iter()
.flat_map(|row| row.iter())
.cloned()
.fold(f32::MAX, f32::min);
if chunk_min_y > min_height + 10.0 {
// Entire chunk is above terrain - leave as air
return;
}
// Generate voxels with cached heights
for local_x in 0..self.chunk_size.x {
let world_x = (chunk_x * self.chunk_size.x as f64 + local_x as f64) as f32;
for local_z in 0..self.chunk_size.z {
let world_z = (chunk_z * self.chunk_size.z as f64 + local_z as f64) as f32;
let base_height = height_cache[local_x as usize][local_z as usize];
for local_y in 0..self.chunk_size.y {
let world_y = (chunk_world_y + local_y as f64) as f32;
let relative_height = world_y - base_height;
// Use 3D noise for density only when needed
let is_solid = if relative_height < -3.0 {
// Deep underground - check for caves
let cave_noise = self.noise_caves.get_noise_3d(world_x, world_y, world_z);
cave_noise < 0.6 // Creates cave systems
} else if relative_height < 0.0 {
true // Just below surface - always solid
} else if relative_height < 15.0 {
let density_value = self.noise.get_noise_3d(world_x, world_y, world_z);
self.should_be_solid(density_value, relative_height)
} else {
false // Above terrain
};
if is_solid {
chunk.set_voxel(
Vector3i::new(local_x, local_y, local_z),
true,
self.chunk_size,
);
}
}
}
}
}
fn get_base_height(&self, x: f32, z: f32) -> f32 {
// Main terrain with multiple octaves (FBm already applied)
let height_value = self.noise.get_noise_2d(x, z);
// Add detail noise for small features
let detail = self.noise_detail.get_noise_2d(x, z) * 0.15;
// Combine and scale - creates hills and valleys like Minecraft
let combined = height_value + detail;
let height = (combined + 1.0) * 0.5 * self.terrain_height as f32;
// Add some exponential scaling for more dramatic terrain
height.powf(1.3)
}
fn should_be_solid(&self, density: f32, relative_height: f32) -> bool {
// Base terrain is solid below surface
if relative_height < 0.0 {
return true;
}
// Create overhangs and floating islands
let cave_threshold = 0.35; // Higher = fewer caves
if relative_height < 15.0 && density > cave_threshold {
return true;
}
false
}
fn get_name(&self) -> &str {
"HeightmapGenerator"
}
}

View File

@@ -0,0 +1,39 @@
use fastnoise_lite::FastNoiseLite;
// HIGHTLY EXPERIMENTAL!!!
pub struct LayeredWorldGenerator {
base_noise: FastNoiseLite, // Large scale terrain
detail_noise: FastNoiseLite, // Medium details
cave_noise: FastNoiseLite, // Cave systems
biome_noise: FastNoiseLite, // Biome distribution
terrain_height: i32,
}
impl LayeredWorldGenerator {
pub fn new(seed: i64) -> Self {
let mut base_noise = FastNoiseLite::new();
base_noise.set_seed(Some(seed as i32));
base_noise.set_frequency(Some(0.001)); // Very low frequency for continents
let mut detail_noise = FastNoiseLite::new();
detail_noise.set_seed(Some(seed as i32 + 1));
detail_noise.set_frequency(Some(0.01)); // Medium frequency for hills
let mut cave_noise = FastNoiseLite::new();
cave_noise.set_seed(Some(seed as i32 + 2));
cave_noise.set_frequency(Some(0.05)); // High frequency for small caves
let mut biome_noise = FastNoiseLite::new();
biome_noise.set_seed(Some(seed as i32 + 3));
biome_noise.set_frequency(Some(0.0005)); // Very low for large biomes
Self {
base_noise,
detail_noise,
cave_noise,
biome_noise,
terrain_height: 64,
}
}
}

7
src/generation/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod generator;
pub mod heightmap;
pub mod layered_generator;
pub use generator::WorldGenerator;
pub use heightmap::HeightmapGenerator;
pub use layered_generator::LayeredWorldGenerator;

15
src/lib.rs Normal file
View File

@@ -0,0 +1,15 @@
use godot::prelude::*;
mod voxel;
mod world;
mod chunk;
mod editor;
mod generation;
mod meshing;
mod rendering;
struct FastVoxel;
#[gdextension]
unsafe impl ExtensionLibrary for FastVoxel {}

View File

@@ -0,0 +1,317 @@
use crate::chunk::chunk::CHUNK_SIZE;
use crate::chunk::{Chunk, ChunkMesh};
use crate::editor::voxel_registry::VoxelRegistry;
use crate::meshing::Mesher;
use godot::prelude::*;
#[derive(Debug, Clone, Copy)]
pub struct BinaryGreedyMesher;
#[derive(Debug, Clone, Copy)]
struct GreedyQuad {
x: u32,
y: u32,
w: u32,
h: u32,
}
impl BinaryGreedyMesher {
pub fn new() -> Self {
Self
}
pub fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) {
mesh.vertices.clear();
mesh.normals.clear();
mesh.indices.clear();
// Generate mesh for each of the 6 faces
self.mesh_face(chunk, mesh, 0); // -X (left)
self.mesh_face(chunk, mesh, 1); // +X (right)
self.mesh_face(chunk, mesh, 2); // -Y (down)
self.mesh_face(chunk, mesh, 3); // +Y (up)
self.mesh_face(chunk, mesh, 4); // -Z (back)
self.mesh_face(chunk, mesh, 5); // +Z (forward)
}
fn mesh_face(&self, chunk: &Chunk, mesh: &mut ChunkMesh, face_dir: usize) {
let size = CHUNK_SIZE as usize;
// For each slice perpendicular to the face direction
for axis in 0..size {
// Generate binary mask for this slice
let mut face_mask = [0u32; 32];
for row in 0..size {
for col in 0..size {
let (x, y, z) = self.get_coords_for_face(face_dir, axis, row, col);
if x >= size || y >= size || z >= size {
continue;
}
let pos = Vector3i::new(x as i32, y as i32, z as i32);
let is_solid = chunk.get_voxel(
pos,
Vector3i {
x: 32,
y: 32,
z: 32,
},
);
// Check if neighbor in face direction is air
let (nx, ny, nz) = self.get_neighbor_coords(face_dir, x, y, z);
let neighbor_is_air = if nx < 0
|| nx >= size as i32
|| ny < 0
|| ny >= size as i32
|| nz < 0
|| nz >= size as i32
{
true // Outside chunk = air
} else {
!chunk.get_voxel(
Vector3i::new(nx, ny, nz),
Vector3i {
x: 32,
y: 32,
z: 32,
},
)
};
// Face is visible if this voxel is solid and neighbor is air
if is_solid && neighbor_is_air {
face_mask[row] |= 1 << col;
}
}
}
// Greedy mesh the binary mask
let quads = self.greedy_mesh_binary_plane(face_mask);
// Add quads to mesh
for quad in quads {
self.add_quad_to_mesh(mesh, quad, face_dir, axis, chunk);
}
}
}
#[inline]
fn get_coords_for_face(
&self,
face_dir: usize,
axis: usize,
row: usize,
col: usize,
) -> (usize, usize, usize) {
match face_dir {
0 => (axis, col, row), // -X: axis=x, row=z, col=y
1 => (axis, col, row), // +X: axis=x, row=z, col=y
2 => (row, axis, col), // -Y: axis=y, row=x, col=z
3 => (row, axis, col), // +Y: axis=y, row=x, col=z
4 => (row, col, axis), // -Z: axis=z, row=x, col=y
5 => (row, col, axis), // +Z: axis=z, row=x, col=y
_ => unreachable!(),
}
}
#[inline]
fn get_neighbor_coords(
&self,
face_dir: usize,
x: usize,
y: usize,
z: usize,
) -> (i32, i32, i32) {
let (x, y, z) = (x as i32, y as i32, z as i32);
match face_dir {
0 => (x - 1, y, z), // -X
1 => (x + 1, y, z), // +X
2 => (x, y - 1, z), // -Y
3 => (x, y + 1, z), // +Y
4 => (x, y, z - 1), // -Z
5 => (x, y, z + 1), // +Z
_ => unreachable!(),
}
}
fn greedy_mesh_binary_plane(&self, mut data: [u32; 32]) -> Vec<GreedyQuad> {
let mut quads = Vec::new();
let size = CHUNK_SIZE as u32;
for row in 0..data.len() {
let mut y = 0;
while y < size {
// Find first solid bit (skip zeros)
y += (data[row] >> y).trailing_zeros();
if y >= size {
break; // Reached end of row
}
// Count consecutive solid bits (height)
let h = (data[row] >> y).trailing_ones();
// Create a mask for these bits
let h_as_mask = if h >= 32 { u32::MAX } else { (1u32 << h) - 1 };
let mask = h_as_mask << y;
// Try to expand horizontally
let mut w = 1;
while row + w < size as usize {
// Check if the next row has the same pattern at this position
let next_row_bits = (data[row + w] >> y) & h_as_mask;
if next_row_bits != h_as_mask {
break; // Can't expand further
}
// Clear the bits we're consuming
data[row + w] &= !mask;
w += 1;
}
quads.push(GreedyQuad {
x: row as u32,
y,
w: w as u32,
h,
});
y += h;
}
}
quads
}
fn add_quad_to_mesh(
&self,
mesh: &mut ChunkMesh,
quad: GreedyQuad,
face_dir: usize,
axis: usize,
chunk: &Chunk,
) {
let base_idx = mesh.vertices.len() as i32;
// Convert quad coordinates back to 3D positions
let vertices = self.get_quad_vertices(quad, face_dir, axis);
let normal = self.get_face_normal(face_dir);
// Base color based on face direction with simple directional shading (no per-vertex AO)
let (r, g, b) = match face_dir {
0 => (0.55, 0.55, 0.55), // -X (left) - darker gray
1 => (0.65, 0.65, 0.65), // +X (right) - lighter gray
2 => (0.35, 0.35, 0.35), // -Y (down) - dark gray
3 => (0.5, 0.8, 0.3), // +Y (up) - grass green (brightest)
4 => (0.50, 0.50, 0.50), // -Z (back) - medium gray
5 => (0.60, 0.60, 0.60), // +Z (forward) - lighter gray
_ => (1.0, 1.0, 1.0),
};
let color = Color::from_rgb(r, g, b);
// Add vertices with consistent color (no per-vertex variation)
for vertex in &vertices {
mesh.vertices.push(*vertex);
mesh.normals.push(normal);
mesh.colors.push(color);
}
// Add indices for two triangles
mesh.indices.push(base_idx);
mesh.indices.push(base_idx + 1);
mesh.indices.push(base_idx + 2);
mesh.indices.push(base_idx);
mesh.indices.push(base_idx + 2);
mesh.indices.push(base_idx + 3);
}
fn get_quad_vertices(&self, quad: GreedyQuad, face_dir: usize, axis: usize) -> [Vector3; 4] {
let x = quad.x as f32;
let y = quad.y as f32;
let w = quad.w as f32;
let h = quad.h as f32;
let a = axis as f32;
match face_dir {
0 => [
// -X (left)
Vector3::new(a, y, x),
Vector3::new(a, y + h, x),
Vector3::new(a, y + h, x + w),
Vector3::new(a, y, x + w),
],
1 => [
// +X (right)
Vector3::new(a + 1.0, y, x),
Vector3::new(a + 1.0, y, x + w),
Vector3::new(a + 1.0, y + h, x + w),
Vector3::new(a + 1.0, y + h, x),
],
2 => [
// -Y (down)
Vector3::new(x, a, y),
Vector3::new(x, a, y + h),
Vector3::new(x + w, a, y + h),
Vector3::new(x + w, a, y),
],
3 => [
// +Y (up)
Vector3::new(x, a + 1.0, y),
Vector3::new(x + w, a + 1.0, y),
Vector3::new(x + w, a + 1.0, y + h),
Vector3::new(x, a + 1.0, y + h),
],
4 => [
// -Z (back)
Vector3::new(x, y, a),
Vector3::new(x + w, y, a),
Vector3::new(x + w, y + h, a),
Vector3::new(x, y + h, a),
],
5 => [
// +Z (forward)
Vector3::new(x, y, a + 1.0),
Vector3::new(x, y + h, a + 1.0),
Vector3::new(x + w, y + h, a + 1.0),
Vector3::new(x + w, y, a + 1.0),
],
_ => unreachable!(),
}
}
fn get_face_normal(&self, face_dir: usize) -> Vector3 {
match face_dir {
0 => Vector3::new(-1.0, 0.0, 0.0), // -X
1 => Vector3::new(1.0, 0.0, 0.0), // +X
2 => Vector3::new(0.0, -1.0, 0.0), // -Y
3 => Vector3::new(0.0, 1.0, 0.0), // +Y
4 => Vector3::new(0.0, 0.0, -1.0), // -Z
5 => Vector3::new(0.0, 0.0, 1.0), // +Z
_ => unreachable!(),
}
}
}
impl Mesher for BinaryGreedyMesher {
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) {
self.generate_mesh(chunk, mesh);
}
fn generate_mesh_with_registry(
&self,
chunk: &Chunk,
_registry: &VoxelRegistry,
mesh: &mut ChunkMesh,
) {
// Binary greedy mesher doesn't use registry (only solid/air)
self.generate_mesh(chunk, mesh);
}
fn get_name(&self) -> &str {
"BinaryGreedyMesher"
}
}

207
src/meshing/mesher.rs Normal file
View File

@@ -0,0 +1,207 @@
use crate::chunk::{Chunk, ChunkMesh};
use crate::editor::voxel_registry::VoxelRegistry;
use godot::prelude::*;
pub trait Mesher: Send + Sync {
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh);
fn generate_mesh_with_registry(
&self,
chunk: &Chunk,
registry: &VoxelRegistry,
mesh: &mut ChunkMesh,
);
fn get_name(&self) -> &str;
}
pub struct CullingMesher;
impl CullingMesher {
pub fn new() -> Self {
Self
}
}
impl Mesher for CullingMesher {
fn generate_mesh_with_registry(
&self,
chunk: &Chunk,
registry: &VoxelRegistry,
mesh: &mut ChunkMesh,
) {
// panic!("Not implemented for CullingMesher")
self.generate_mesh(chunk, mesh);
}
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) {
mesh.clear();
// Simple CullingMesher - generate a quad for each solid voxel face
// This is very basic and should be optimized later with greedy meshing
for x in 0..16 {
for y in 0..16 {
for z in 0..16 {
let voxel = chunk.get_voxel(
Vector3i::new(x, y, z),
Vector3i {
x: 32,
y: 32,
z: 32,
},
);
if !voxel {
continue;
}
self.add_voxel_to_mesh(x, y, z, voxel, mesh, chunk);
}
}
}
}
fn get_name(&self) -> &str {
"CullingMesher"
}
}
impl CullingMesher {
fn add_voxel_to_mesh(
&self,
x: i32,
y: i32,
z: i32,
voxel: bool,
mesh: &mut ChunkMesh,
chunk: &Chunk,
) {
let color = Color {
r: 255.0,
g: 255.0,
b: 255.0,
a: 255.0,
};
// Check each face and add it if the neighbor is air
let directions = [
(Vector3i::new(1, 0, 0), Vector3::RIGHT),
(Vector3i::new(-1, 0, 0), Vector3::LEFT),
(Vector3i::new(0, 1, 0), Vector3::UP),
(Vector3i::new(0, -1, 0), Vector3::DOWN),
(Vector3i::new(0, 0, 1), Vector3::BACK),
(Vector3i::new(0, 0, -1), Vector3::FORWARD),
];
for (offset, normal) in directions.iter() {
let neighbor_pos = Vector3i::new(x + offset.x, y + offset.y, z + offset.z);
// If neighbor is out of bounds or air, create a face
if !chunk.is_voxel_within_bounds(
neighbor_pos,
Vector3i {
x: 32,
y: 32,
z: 32,
},
) || !chunk.get_voxel(
neighbor_pos,
Vector3i {
x: 32,
y: 32,
z: 32,
},
) {
self.add_face(x, y, z, *normal, color, mesh);
}
}
}
fn add_face(
&self,
x: i32,
y: i32,
z: i32,
normal: Vector3,
color: Color,
mesh: &mut ChunkMesh,
) {
let base_index = mesh.vertices.len() as i32;
let pos = Vector3::new(x as f32, y as f32, z as f32);
// Define the 4 vertices of the quad based on face direction
let (v0, v1, v2, v3) = Self::get_face_vertices(normal, pos);
// Add vertices
mesh.vertices.extend_from_slice(&[v0, v1, v2, v3]);
// Add normals (all same for the face)
for _ in 0..4 {
mesh.normals.push(normal);
}
// Add colors
for _ in 0..4 {
mesh.colors.push(color);
}
// Add indices (two triangles)
mesh.indices.extend_from_slice(&[
base_index,
base_index + 1,
base_index + 2,
base_index + 2,
base_index + 3,
base_index,
]);
}
fn get_face_vertices(normal: Vector3, pos: Vector3) -> (Vector3, Vector3, Vector3, Vector3) {
let half = Vector3::new(0.5, 0.5, 0.5);
let center = pos + half;
match (normal.x as i32, normal.y as i32, normal.z as i32) {
(1, 0, 0) => (
// RIGHT
center + Vector3::new(0.5, -0.5, -0.5),
center + Vector3::new(0.5, -0.5, 0.5),
center + Vector3::new(0.5, 0.5, 0.5),
center + Vector3::new(0.5, 0.5, -0.5),
),
(-1, 0, 0) => (
// LEFT
center + Vector3::new(-0.5, -0.5, 0.5),
center + Vector3::new(-0.5, -0.5, -0.5),
center + Vector3::new(-0.5, 0.5, -0.5),
center + Vector3::new(-0.5, 0.5, 0.5),
),
(0, 1, 0) => (
// UP
center + Vector3::new(-0.5, 0.5, -0.5),
center + Vector3::new(0.5, 0.5, -0.5),
center + Vector3::new(0.5, 0.5, 0.5),
center + Vector3::new(-0.5, 0.5, 0.5),
),
(0, -1, 0) => (
// DOWN
center + Vector3::new(-0.5, -0.5, 0.5),
center + Vector3::new(0.5, -0.5, 0.5),
center + Vector3::new(0.5, -0.5, -0.5),
center + Vector3::new(-0.5, -0.5, -0.5),
),
(0, 0, 1) => (
// BACK
center + Vector3::new(0.5, -0.5, 0.5),
center + Vector3::new(-0.5, -0.5, 0.5),
center + Vector3::new(-0.5, 0.5, 0.5),
center + Vector3::new(0.5, 0.5, 0.5),
),
(0, 0, -1) => (
// FORWARD
center + Vector3::new(-0.5, -0.5, -0.5),
center + Vector3::new(0.5, -0.5, -0.5),
center + Vector3::new(0.5, 0.5, -0.5),
center + Vector3::new(-0.5, 0.5, -0.5),
),
_ => panic!("wrong face"), // fallback, shouldn't happen
}
}
}

7
src/meshing/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod binary_greedy_mesher;
pub mod mesher;
pub mod textured_mesher;
pub use binary_greedy_mesher::BinaryGreedyMesher;
pub use mesher::Mesher;
pub use textured_mesher::TexturedMesher;

View File

@@ -0,0 +1,410 @@
use crate::chunk::chunk::CHUNK_SIZE;
use crate::chunk::{self, Chunk, ChunkMesh};
use crate::editor::voxel_registry::{VoxelModel, VoxelRegistry};
use crate::meshing::Mesher;
use godot::prelude::*;
pub struct TexturedMesher {
face_vertices: [[Vector3; 4]; 6],
face_normals: [Vector3; 6],
}
impl TexturedMesher {
pub fn new() -> Self {
let face_vertices = Self::generate_face_vertices();
let face_normals = [
Vector3::LEFT, // 0
Vector3::RIGHT, // 1
Vector3::DOWN, // 2
Vector3::UP, // 3
Vector3::BACK, // 4
Vector3::FORWARD, // 5
];
Self {
face_vertices,
face_normals,
}
}
fn generate_face_vertices() -> [[Vector3; 4]; 6] {
[
// LEFT (X-)
[
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 1.0, 1.0),
],
// RIGHT (X+)
[
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 1.0),
Vector3::new(1.0, 1.0, 1.0),
Vector3::new(1.0, 1.0, 0.0),
],
// DOWN (Y-)
[
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 0.0),
],
// UP (Y+)
[
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(1.0, 1.0, 0.0),
Vector3::new(1.0, 1.0, 1.0),
Vector3::new(0.0, 1.0, 1.0),
],
// BACK (Z+)
[
Vector3::new(1.0, 0.0, 1.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.0, 1.0, 1.0),
Vector3::new(1.0, 1.0, 1.0),
],
// FRONT (Z-)
[
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(1.0, 1.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
],
]
}
#[inline(always)]
fn is_face_visible(chunk: &Chunk, x: usize, y: usize, z: usize, face: usize) -> bool {
// Convert to i32 for neighbor calculation
let (nx, ny, nz) = match face {
0 => (x as i32 - 1, y as i32, z as i32), // LEFT
1 => (x as i32 + 1, y as i32, z as i32), // RIGHT
2 => (x as i32, y as i32 - 1, z as i32), // DOWN
3 => (x as i32, y as i32 + 1, z as i32), // UP
4 => (x as i32, y as i32, z as i32 + 1), // BACK
5 => (x as i32, y as i32, z as i32 - 1), // FRONT
_ => return false,
};
// Check if neighbor is out of bounds or empty
if nx < 0 || nx >= CHUNK_SIZE || ny < 0 || ny >= CHUNK_SIZE || nz < 0 || nz >= CHUNK_SIZE {
return true;
}
!chunk.get_voxel(
Vector3i::new(nx, ny, nz),
Vector3i {
x: 32,
y: 32,
z: 32,
},
) // Visible if neighbor is air (false)
}
fn add_cube_voxel_to_mesh_fast(
&self,
x: usize,
y: usize,
z: usize,
model: &VoxelModel, // Already borrowed, no binding needed
mesh: &mut ChunkMesh,
chunk: &Chunk,
) {
let pos = Vector3::new(x as f32, y as f32, z as f32);
for face in 0..6 {
if !Self::is_face_visible(chunk, x, y, z, face) {
continue;
}
let base_index = mesh.vertices.len() as i32;
// Add precomputed vertices
for vertex in &self.face_vertices[face] {
mesh.vertices.push(pos + *vertex);
}
// Add normals (all same for this face)
let normal = self.face_normals[face];
for _ in 0..4 {
mesh.normals.push(normal);
}
// Get UVs - precompute this if possible!
let tile_coord = model.get_tile_for_face(face as i32);
let atlas_size = Vector2i::new(16, 16); // Should come from model
let uvs = self.generate_face_uvs_fast(tile_coord, atlas_size);
mesh.uvs.extend_from_slice(&uvs);
// Add indices
mesh.indices.extend_from_slice(&[
base_index,
base_index + 1,
base_index + 2,
base_index + 2,
base_index + 3,
base_index,
]);
}
}
#[inline(always)]
fn generate_face_uvs_fast(&self, tile_coord: Vector2i, atlas_size: Vector2i) -> [Vector2; 4] {
let inv_atlas_x = 1.0 / atlas_size.x as f32;
let inv_atlas_y = 1.0 / atlas_size.y as f32;
let u_min = tile_coord.x as f32 * inv_atlas_x;
let u_max = (tile_coord.x + 1) as f32 * inv_atlas_x;
let v_min = tile_coord.y as f32 * inv_atlas_y;
let v_max = (tile_coord.y + 1) as f32 * inv_atlas_y;
[
Vector2::new(u_min, v_max), // bottom-left
Vector2::new(u_max, v_max), // bottom-right
Vector2::new(u_max, v_min), // top-right
Vector2::new(u_min, v_min), // top-left
]
}
fn add_voxel_to_mesh(
&self,
x: i32,
y: i32,
z: i32,
model_index: i32,
mesh: &mut ChunkMesh,
chunk: &Chunk,
registry: &VoxelRegistry,
) {
if let Some(model_gd) = registry.models.get(model_index as usize) {
let model = model_gd.bind();
// Skip empty models
if model.is_empty() {
return;
}
// Only handle cube models for now
if model.is_cube() {
self.add_cube_voxel_to_mesh(x, y, z, &model, mesh, chunk);
} else {
godot_print!("No model found for index: {}", model_index);
}
}
}
fn add_cube_voxel_to_mesh(
&self,
x: i32,
y: i32,
z: i32,
model: &VoxelModel,
mesh: &mut ChunkMesh,
chunk: &Chunk,
) {
let directions = [
(Vector3i::new(1, 0, 0), Vector3::RIGHT, 1), // Right face, index 1
(Vector3i::new(-1, 0, 0), Vector3::LEFT, 0), // Left face, index 0
(Vector3i::new(0, 1, 0), Vector3::UP, 3), // Top face, index 3
(Vector3i::new(0, -1, 0), Vector3::DOWN, 2), // Bottom face, index 2
(Vector3i::new(0, 0, 1), Vector3::BACK, 4), // Back face, index 4
(Vector3i::new(0, 0, -1), Vector3::FORWARD, 5), // Front face, index 5
];
for (offset, normal, face_index) in directions.iter() {
let neighbor_pos = Vector3i::new(x + offset.x, y + offset.y, z + offset.z);
// If neighbor is out of bounds or not solid, create a face
if !chunk.is_voxel_within_bounds(
neighbor_pos,
Vector3i {
x: 32,
y: 32,
z: 32,
},
) || !chunk.get_voxel(
neighbor_pos,
Vector3i {
x: 32,
y: 32,
z: 32,
},
) {
self.add_textured_face(x, y, z, *normal, *face_index, model, mesh);
}
}
}
fn add_textured_face(
&self,
x: i32,
y: i32,
z: i32,
normal: Vector3,
face_index: i32,
model: &VoxelModel,
mesh: &mut ChunkMesh,
) {
let base_index = mesh.vertices.len() as i32;
let pos = Vector3::new(x as f32, y as f32, z as f32);
// Get face vertices (same as before)
let (v0, v1, v2, v3) = self.get_face_vertices(normal, pos);
// Add vertices
mesh.vertices.extend_from_slice(&[v0, v1, v2, v3]);
// Add normals
let normal_vec = Vector3::new(normal.x as f32, normal.y as f32, normal.z as f32);
for _ in 0..4 {
mesh.normals.push(normal_vec);
}
// Get texture coordinates for this face
let tile_coord = model.get_tile_for_face(face_index);
// Get atlas size from the model
let atlas_size = Vector2i::new(16, 16);
// Generate UV coordinates for this face
let uvs = self.generate_face_uvs(tile_coord, atlas_size);
mesh.uvs.extend_from_slice(&uvs);
// Add indices (two triangles)
mesh.indices.extend_from_slice(&[
base_index,
base_index + 1,
base_index + 2,
base_index + 2,
base_index + 3,
base_index,
]);
}
fn generate_face_uvs(&self, tile_coord: Vector2i, atlas_size: Vector2i) -> [Vector2; 4] {
let tile_size = Vector2::new(1.0 / atlas_size.x as f32, 1.0 / atlas_size.y as f32);
let u_min = tile_coord.x as f32 * tile_size.x;
let u_max = (tile_coord.x + 1) as f32 * tile_size.x;
let v_min = tile_coord.y as f32 * tile_size.y;
let v_max = (tile_coord.y + 1) as f32 * tile_size.y;
// Standard quad UV mapping
// Adjust the order based on your vertex winding
[
Vector2::new(u_min, v_max), // bottom-left
Vector2::new(u_max, v_max), // bottom-right
Vector2::new(u_max, v_min), // top-right
Vector2::new(u_min, v_min), // top-left
]
}
fn get_face_vertices(
&self,
normal: Vector3,
pos: Vector3,
) -> (Vector3, Vector3, Vector3, Vector3) {
let half = Vector3::new(0.5, 0.5, 0.5);
let center = pos + half;
match (normal.x as i32, normal.y as i32, normal.z as i32) {
(1, 0, 0) => (
// RIGHT
center + Vector3::new(0.5, -0.5, -0.5),
center + Vector3::new(0.5, -0.5, 0.5),
center + Vector3::new(0.5, 0.5, 0.5),
center + Vector3::new(0.5, 0.5, -0.5),
),
(-1, 0, 0) => (
// LEFT
center + Vector3::new(-0.5, -0.5, 0.5),
center + Vector3::new(-0.5, -0.5, -0.5),
center + Vector3::new(-0.5, 0.5, -0.5),
center + Vector3::new(-0.5, 0.5, 0.5),
),
(0, 1, 0) => (
// UP
center + Vector3::new(-0.5, 0.5, -0.5),
center + Vector3::new(0.5, 0.5, -0.5),
center + Vector3::new(0.5, 0.5, 0.5),
center + Vector3::new(-0.5, 0.5, 0.5),
),
(0, -1, 0) => (
// DOWN
center + Vector3::new(-0.5, -0.5, 0.5),
center + Vector3::new(0.5, -0.5, 0.5),
center + Vector3::new(0.5, -0.5, -0.5),
center + Vector3::new(-0.5, -0.5, -0.5),
),
(0, 0, 1) => (
// BACK
center + Vector3::new(0.5, -0.5, 0.5),
center + Vector3::new(-0.5, -0.5, 0.5),
center + Vector3::new(-0.5, 0.5, 0.5),
center + Vector3::new(0.5, 0.5, 0.5),
),
(0, 0, -1) => (
// FORWARD
center + Vector3::new(-0.5, -0.5, -0.5),
center + Vector3::new(0.5, -0.5, -0.5),
center + Vector3::new(0.5, 0.5, -0.5),
center + Vector3::new(-0.5, 0.5, -0.5),
),
_ => panic!("wrong face"), // fallback, shouldn't happen
}
}
}
impl Mesher for TexturedMesher {
fn generate_mesh(&self, chunk: &Chunk, mesh: &mut ChunkMesh) {
panic!("TexturedMesher requires a VoxelRegistry. Use generate_mesh_with_registry instead.");
}
fn generate_mesh_with_registry(
&self,
chunk: &Chunk,
registry: &VoxelRegistry,
mesh: &mut ChunkMesh,
) {
mesh.clear();
let estimated_faces = 4096 / 4 * 3; // ~3000 faces
mesh.vertices.reserve(estimated_faces * 4);
mesh.normals.reserve(estimated_faces * 4);
mesh.uvs.reserve(estimated_faces * 4);
mesh.indices.reserve(estimated_faces * 6);
for x in 0..32 {
for y in 0..32 {
for z in 0..32 {
let is_solid = chunk.get_voxel(
Vector3i::new(x, y, z),
Vector3i {
x: 32,
y: 32,
z: 32,
},
);
if !is_solid {
continue;
}
// For now, use model index 1 for all solid voxels
// In the future, you might want to store material types separately
let model = registry.models.at(1);
self.add_cube_voxel_to_mesh_fast(
x as usize,
y as usize,
z as usize,
&model.bind(),
mesh,
chunk,
);
}
}
}
}
fn get_name(&self) -> &str {
"TexturedMesher"
}
}

3
src/rendering/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod renderer;
pub use renderer::Renderer;

220
src/rendering/renderer.rs Normal file
View File

@@ -0,0 +1,220 @@
use std::{collections::HashMap, time::Instant};
use crate::chunk::{self, Chunk, ChunkMesh};
use godot::{
classes::{
ArrayMesh, Material, MeshInstance3D, ResourceLoader, RenderingServer, StandardMaterial3D,
base_material_3d::{CullMode, Flags, ShadingMode, Transparency},
geometry_instance_3d::ShadowCastingSetting,
mesh::{ArrayType, PrimitiveType},
rendering_server::InstanceType,
},
obj::IndexEnum,
prelude::*,
};
pub struct Renderer {
mesh_instances: HashMap<(i32, i32, i32), Gd<MeshInstance3D>>,
terrain_material: Option<Gd<Material>>,
// Cache for reusable mesh instances
mesh_instance_pool: Vec<Gd<MeshInstance3D>>,
}
impl Renderer {
pub fn new() -> Self {
let terrain_material = Self::load_terrain_material();
Self {
mesh_instances: HashMap::new(),
terrain_material,
mesh_instance_pool: Vec::with_capacity(100),
}
}
#[inline]
fn load_terrain_material() -> Option<Gd<Material>> {
let mut resource_loader = ResourceLoader::singleton();
let path: GString = "res://materials/terrain_material.tres".into();
resource_loader
.load(&path)
.and_then(|resource| resource.try_cast::<Material>().ok())
}
pub fn render_chunk(&mut self, chunk: &Chunk, mesh: &ChunkMesh) -> Gd<MeshInstance3D> {
let chunk_key = chunk.world_position;
let chunk_key_i32 = (chunk_key.0 as i32, chunk_key.1 as i32, chunk_key.2 as i32);
if let Some(old_instance) = self.mesh_instances.remove(&chunk_key_i32) {
old_instance.free();
godot_print!("Renderer: Replaced mesh instance at {:?}", chunk_key);
}
// Skip empty meshes
if mesh.is_empty() {
return MeshInstance3D::new_alloc();
}
// Create Godot arrays for mesh data - use new() instead of new_gd()
let mut array_mesh = ArrayMesh::new_gd();
array_mesh.clear_surfaces();
let mut surface_array: VariantArray = VariantArray::new();
// Convert our mesh data to Godot arrays
surface_array.resize(ArrayType::MAX.to_index(), &Variant::nil()); // Mesh::ARRAY_MAX
// Vertices - push directly without to_variant()
let mut vertices = PackedVector3Array::new();
for vertex in &mesh.vertices {
vertices.push(*vertex);
}
surface_array.set(ArrayType::VERTEX.to_index(), &Variant::from(vertices)); // Remove .to_variant()
// Normals
let mut normals = PackedVector3Array::new();
for normal in &mesh.normals {
normals.push(*normal);
}
surface_array.set(ArrayType::NORMAL.to_index(), &Variant::from(normals)); // Remove .to_variant()
// Colors
let mut colors = PackedColorArray::new();
for color in &mesh.colors {
colors.push(*color);
}
surface_array.set(ArrayType::COLOR.to_index(), &Variant::from(colors)); // Remove .to_variant()
// Indices
let mut indices = PackedInt32Array::new();
for index in &mesh.indices {
indices.push(*index);
}
surface_array.set(ArrayType::INDEX.to_index(), &Variant::from(indices)); // Remove .to_variant()
// Create the mesh - use correct arguments
array_mesh.add_surface_from_arrays(PrimitiveType::TRIANGLES, &surface_array);
// Create mesh instance using new_alloc() for manually managed nodes
let mut mesh_instance = MeshInstance3D::new_alloc();
mesh_instance.set_mesh(&array_mesh);
// Position the mesh instance at the chunk's world position
let (x, y, z) = chunk.world_position;
mesh_instance.set_position(Vector3::new(
(x * 16 as f64) as f32,
(y * 16 as f64) as f32,
(z * 16 as f64) as f32,
));
// Create a proper material that supports shadows
let mut material = StandardMaterial3D::new_gd();
// Enable shadows
material.set_flag(Flags::ALBEDO_FROM_VERTEX_COLOR, true);
material.set_shading_mode(ShadingMode::PER_PIXEL);
material.set_transparency(Transparency::DISABLED);
material.set_cull_mode(CullMode::BACK);
// You can also add some basic properties
material.set_roughness(0.8);
material.set_metallic(0.0);
// Create mesh instance
mesh_instance.set_material_override(&material);
// Enable shadows for the mesh instance
mesh_instance.set_cast_shadows_setting(ShadowCastingSetting::ON);
self.mesh_instances
.insert(chunk_key_i32, mesh_instance.clone());
mesh_instance
}
pub fn render_chunk_with_uvs(&mut self, chunk: &Chunk, mesh: &ChunkMesh) -> Gd<MeshInstance3D> {
let chunk_key = chunk.world_position;
let chunk_key_i32 = (chunk_key.0 as i32, chunk_key.1 as i32, chunk_key.2 as i32);
// Reuse or remove old instance
if let Some(old_instance) = self.mesh_instances.remove(&chunk_key_i32) {
// Return to pool instead of freeing
self.mesh_instance_pool.push(old_instance);
}
if mesh.is_empty() {
return MeshInstance3D::new_alloc();
}
// Create mesh arrays more efficiently
let mut array_mesh = ArrayMesh::new_gd();
let mut surface_array: VariantArray = VariantArray::new();
surface_array.resize(ArrayType::MAX.to_index(), &Variant::nil());
// Use push instead of set for better performance
let mut vertices = PackedVector3Array::new();
for vertex in &mesh.vertices {
vertices.push(*vertex);
}
surface_array.set(ArrayType::VERTEX.to_index(), &Variant::from(vertices));
let mut normals = PackedVector3Array::new();
for normal in &mesh.normals {
normals.push(*normal);
}
surface_array.set(ArrayType::NORMAL.to_index(), &Variant::from(normals));
let mut uvs = PackedVector2Array::new();
for uv in &mesh.uvs {
uvs.push(*uv);
}
surface_array.set(ArrayType::TEX_UV.to_index(), &Variant::from(uvs));
let mut indices = PackedInt32Array::new();
for index in &mesh.indices {
indices.push(*index);
}
surface_array.set(ArrayType::INDEX.to_index(), &Variant::from(indices));
array_mesh.add_surface_from_arrays(PrimitiveType::TRIANGLES, &surface_array);
// Reuse mesh instance from pool or create new
let mut mesh_instance = self.mesh_instance_pool.pop().unwrap_or_else(|| {
let mut instance = MeshInstance3D::new_alloc();
instance.set_cast_shadows_setting(ShadowCastingSetting::ON);
instance
});
mesh_instance.set_mesh(&array_mesh);
// Position the mesh instance (chunk size is now 32)
let (x, y, z) = chunk.world_position;
mesh_instance.set_position(Vector3::new(
(x * 32.0) as f32,
(y * 32.0) as f32,
(z * 32.0) as f32,
));
// Apply material if available (do this without timing)
if let Some(mat) = &self.terrain_material {
mesh_instance.set_material_override(mat);
}
self.mesh_instances
.insert(chunk_key_i32, mesh_instance.clone());
mesh_instance
}
pub fn clear(&mut self) {
for (_, instance) in self.mesh_instances.drain() {
self.mesh_instance_pool.push(instance);
}
self.mesh_instances.clear();
}
pub fn remove_chunk(&mut self, chunk_position: (i32, i32, i32)) {
if let Some(instance) = self.mesh_instances.remove(&chunk_position) {
self.mesh_instance_pool.push(instance);
}
}
}

7
src/voxel/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod model;
pub mod registry;
pub mod voxel;
pub use model::*;
pub use registry::VoxelRegistry;
pub use voxel::Voxel;

24
src/voxel/model.rs Normal file
View File

@@ -0,0 +1,24 @@
use godot::classes::class_macros::private::virtuals::Os::Vector2i;
#[derive(Debug, PartialEq, Clone, Copy)]
#[repr(u8)]
pub enum VoxelModelType {
Empty = 0_u8,
Cube,
Mesh,
Fluid,
}
#[derive(Debug, Clone, Copy)]
pub struct VoxelModel {
voxel_type: VoxelModelType,
atlas_size_in_tiles: Vector2i,
tile_left: Vector2i,
tile_right: Vector2i,
tile_bottom: Vector2i,
tile_top: Vector2i,
tile_back: Vector2i,
tile_front: Vector2i,
}

13
src/voxel/registry.rs Normal file
View File

@@ -0,0 +1,13 @@
use crate::voxel::VoxelModel;
pub struct VoxelRegistry {
pub models: Vec<VoxelModel>,
}
impl VoxelRegistry {
pub fn new(size: usize) -> Self {
VoxelRegistry {
models: Vec::with_capacity(size),
}
}
}

33
src/voxel/voxel.rs Normal file
View File

@@ -0,0 +1,33 @@
use godot::classes::class_macros::private::virtuals::Os::Color;
#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub enum Voxel {
#[default]
Air = 0,
Grass,
Dirt,
Stone,
Sand,
Water,
}
impl Voxel {
pub fn get_color(&self) -> Color {
match self {
Voxel::Air => Color::from_rgba(0.0, 0.0, 0.0, 0.0), // Transparent
Voxel::Grass => Color::from_rgb(0.2, 0.8, 0.2),
Voxel::Dirt => Color::from_rgb(0.5, 0.3, 0.1),
Voxel::Stone => Color::from_rgb(0.4, 0.4, 0.4),
Voxel::Sand => Color::from_rgb(0.9, 0.8, 0.5),
Voxel::Water => Color::from_rgba(0.2, 0.4, 0.8, 0.7),
}
}
pub fn is_solid(&self) -> bool {
match self {
Voxel::Air => false,
Voxel::Water => false, // Or true if you want solid water
_ => true,
}
}
}

176
src/world.rs Normal file
View File

@@ -0,0 +1,176 @@
use godot::classes::{INode3D, VoxelGi};
use godot::prelude::*;
use crate::chunk::ChunkManager;
use crate::chunk::chunk::CHUNK_SIZE;
use crate::editor::VoxelRegistry;
use crate::generation::{HeightmapGenerator, WorldGenerator};
use crate::meshing::mesher::CullingMesher;
use crate::meshing::{BinaryGreedyMesher, Mesher};
#[derive(GodotClass)]
#[class(base=Node3D,tool)]
struct World {
base: Base<Node3D>,
// ===== RENDERING GROUP =====
#[export_group(name = "Rendering")]
#[export]
chunk_size: Vector3i,
#[export(range = (2.0,64.0))]
render_distance: u8,
// ===== GENERATION GROUP =====
#[export_group(name = "Generation")]
#[export]
world_seed: i32,
#[export(range = (0.01, 4.0, 0.01))]
noise_frequency: f32,
#[export(range = (1.0, 10.0))]
terrain_height: u32,
#[export(range = (1.0, 64.0, 1.0))]
workder_threads: u8,
#[export_group(name = "Voxel Registry")]
#[export]
registry: Option<Gd<VoxelRegistry>>,
#[export_group(name = "GI")]
#[export]
voxelgi_node: Option<Gd<VoxelGi>>,
// World data (temporary)
chunk_manager: ChunkManager,
generator: Box<dyn WorldGenerator>,
mesher: Box<dyn Mesher>,
}
#[godot_api]
impl INode3D for World {
fn init(base: Base<Node3D>) -> Self {
godot_print!("🧊 Hello from FastVoxel");
let generator = Box::new(HeightmapGenerator::new(1234, 0.03, 5));
let mesher = Box::new(CullingMesher::new());
let chunk_manager = ChunkManager::new();
Self {
render_distance: 8,
chunk_size: Vector3i::new(16, 16, 16),
world_seed: 1234,
noise_frequency: 0.03,
terrain_height: 5,
base,
generator,
mesher,
chunk_manager,
workder_threads: 4,
registry: None,
voxelgi_node: None,
}
}
fn process(&mut self, _delta: f64) {
self.add_pending_mesh_instances_to_scene();
}
fn ready(&mut self) {
if self.registry.is_none() {
godot_error!(
"VoxelRegistry not assigned! Please assign a VoxelRegistry resource in the editor."
);
return;
}
if self.voxelgi_node.is_none() {
godot_error!("VoxelGI Node is not selected.");
return;
}
godot_print!("ChunkSize is {0}", self.chunk_size);
self.generator = Box::new(HeightmapGenerator::new(
self.get_world_seed() as i64,
self.noise_frequency,
self.terrain_height as i32,
));
self.chunk_manager.chunk_size = self.chunk_size;
self.chunk_manager.terrain_height = self.terrain_height;
self.regenerate_terrain();
godot_print!("World ready! building GI...");
}
}
#[godot_api]
impl World {
#[func]
fn generate_terrain(&mut self) {
match &self.registry {
None => {
godot_print!("No Voxel registry is provided to the world.")
}
Some(registry) => {
let distance = self.render_distance as i32;
let library_ref = registry.bind();
for x in -distance..=distance {
for z in -distance..=distance {
self.chunk_manager.generate_chunk_column(
x,
z,
&library_ref,
self.generator.as_ref(),
self.mesher.as_ref(),
);
}
}
godot_print!("Textured terrain regenerated!");
}
}
}
fn regenerate_terrain(&mut self) {
godot_print!("Regenerating terrain...");
self.chunk_manager.clear();
// Create textured mesher
self.mesher = Box::new(CullingMesher::new());
self.generate_terrain();
}
#[func]
fn update_from_gdscript(&mut self, player_world_pos: Vector3) {
match &self.registry {
None => return,
Some(registry) => {
let registry_ref = registry.bind();
self.chunk_manager.update_around_player(
player_world_pos,
self.render_distance as i32,
&registry_ref,
self.generator.as_ref(),
self.mesher.as_ref(),
);
}
}
}
fn add_pending_mesh_instances_to_scene(&mut self) {
let mesh_instances = self.chunk_manager.take_pending_mesh_instances();
if mesh_instances.is_empty() {
return;
}
let mut base = self.base_mut();
for mesh_instance in mesh_instances {
base.add_child(&mesh_instance.upcast::<Node3D>());
}
}
}