Add dynamic chunk lighting engine (vertical sky light + emissive blocks)

This commit is contained in:
Master290 2026-06-27 21:21:31 +03:00
parent 09a694daf5
commit e33370a049
5 changed files with 212 additions and 27 deletions

68
cmd/genlight/main.go Normal file
View file

@ -0,0 +1,68 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
)
func main() {
f, err := os.Open("../../generated/reports/blocks.json")
if err != nil {
panic(err)
}
var data map[string]struct {
States []struct {
ID int `json:"id"`
} `json:"states"`
}
if err := json.NewDecoder(f).Decode(&data); err != nil {
panic(err)
}
opacity := make([]byte, 30000)
emission := make([]byte, 30000)
for name, block := range data {
op := byte(15)
em := byte(0)
if strings.Contains(name, "air") || strings.Contains(name, "glass") || strings.Contains(name, "leaves") || strings.Contains(name, "sapling") || strings.Contains(name, "flower") || strings.Contains(name, "grass") || strings.Contains(name, "fern") || strings.Contains(name, "torch") || strings.Contains(name, "lantern") || strings.Contains(name, "button") || strings.Contains(name, "pressure_plate") || strings.Contains(name, "rail") || strings.Contains(name, "ladder") || strings.Contains(name, "vine") || strings.Contains(name, "door") || strings.Contains(name, "trapdoor") || strings.Contains(name, "fence") || strings.Contains(name, "wall") || strings.Contains(name, "slab") || strings.Contains(name, "water") || strings.Contains(name, "ice") || strings.Contains(name, "snow") || strings.Contains(name, "carpet") || strings.Contains(name, "sign") || strings.Contains(name, "banner") || strings.Contains(name, "bed") || strings.Contains(name, "chest") || strings.Contains(name, "bell") || strings.Contains(name, "campfire") || strings.Contains(name, "chain") || strings.Contains(name, "coral") || strings.Contains(name, "end_rod") || strings.Contains(name, "glowstone") || strings.Contains(name, "lava") || strings.Contains(name, "magma") || strings.Contains(name, "sea_lantern") || strings.Contains(name, "shroomlight") || strings.Contains(name, "slime") || strings.Contains(name, "honey") {
op = 0
}
if strings.Contains(name, "water") || strings.Contains(name, "ice") {
op = 1
}
if strings.Contains(name, "leaves") {
op = 1
}
if name == "minecraft:glowstone" || name == "minecraft:sea_lantern" || name == "minecraft:shroomlight" || name == "minecraft:beacon" || name == "minecraft:end_rod" || strings.Contains(name, "lava") {
em = 15
}
if strings.Contains(name, "torch") || strings.Contains(name, "campfire") || strings.Contains(name, "lantern") || strings.Contains(name, "fire") {
em = 14
}
for _, state := range block.States {
if state.ID < len(opacity) {
opacity[state.ID] = op
emission[state.ID] = em
}
}
}
out, _ := os.Create("../../internal/world/light_data.go")
fmt.Fprintln(out, "package world")
fmt.Fprintln(out, "var blockOpacity = [29873]byte{")
for i := 0; i < 29873; i++ {
fmt.Fprintf(out, "%d,", opacity[i])
}
fmt.Fprintln(out, "}")
fmt.Fprintln(out, "var blockEmission = [29873]byte{")
for i := 0; i < 29873; i++ {
fmt.Fprintf(out, "%d,", emission[i])
}
fmt.Fprintln(out, "}")
out.Close()
}

View file

@ -40,17 +40,25 @@ func parsePalettedContainer(t *testing.T, r *protocol.Reader, maxBits, entryCoun
}
}
func skipBitSet(t *testing.T, r *protocol.Reader) {
func parseBitSet(t *testing.T, r *protocol.Reader) int {
t.Helper()
n, err := r.VarInt()
if err != nil || n < 0 {
t.Fatalf("bitset len: %v", err)
}
bits := 0
for i := int32(0); i < n; i++ {
if _, err := r.Int64(); err != nil {
val, err := r.Int64()
if err != nil {
t.Fatalf("bitset long: %v", err)
}
// Count set bits
for val > 0 {
bits += int(val & 1)
val >>= 1
}
}
return bits
}
// TestFlatChunkEncodesCleanly fully parses an encoded flat chunk and asserts
@ -117,13 +125,13 @@ func TestFlatChunkEncodesCleanly(t *testing.T) {
}
// Light: four bitsets, then sky arrays, then block arrays.
skipBitSet(t, r) // sky mask
skipBitSet(t, r) // block mask
skipBitSet(t, r) // empty sky mask
skipBitSet(t, r) // empty block mask
expectedSkyArrays := parseBitSet(t, r) // sky mask
expectedBlockArrays := parseBitSet(t, r) // block mask
parseBitSet(t, r) // empty sky mask
parseBitSet(t, r) // empty block mask
skyArrays, err := r.VarInt()
if err != nil || skyArrays != lightSections {
t.Fatalf("sky arrays = %d (err %v), want %d", skyArrays, err, lightSections)
if err != nil || skyArrays != int32(expectedSkyArrays) {
t.Fatalf("sky arrays = %d (err %v), want %d", skyArrays, err, expectedSkyArrays)
}
for i := int32(0); i < skyArrays; i++ {
n, err := r.VarInt()
@ -136,8 +144,20 @@ func TestFlatChunkEncodesCleanly(t *testing.T) {
}
}
}
if blockArrays, err := r.VarInt(); err != nil || blockArrays != 0 {
t.Fatalf("block arrays = %d (err %v), want 0", blockArrays, err)
blockArrays, err := r.VarInt()
if err != nil || blockArrays != int32(expectedBlockArrays) {
t.Fatalf("block arrays = %d (err %v), want %d", blockArrays, err, expectedBlockArrays)
}
for i := int32(0); i < blockArrays; i++ {
n, err := r.VarInt()
if err != nil || n != 2048 {
t.Fatalf("block array len = %d (err %v), want 2048", n, err)
}
for j := int32(0); j < n; j++ {
if _, err := r.ReadByte(); err != nil {
t.Fatalf("block byte: %v", err)
}
}
}
if rem := r.Remaining(); rem != 0 {

View file

@ -6,29 +6,104 @@ import "regionio/internal/protocol"
// above, plus one per block section.
const lightSections = SectionCount + 2
// writeLight emits a fully-lit sky: every light section carries sky light 15,
// and block light is reported as uniformly empty. This avoids a black world
// without implementing real light propagation (deferred).
// writeLight computes and emits simple lighting data. It does a vertical pass
// for sky light (sunlight propagating downward) and a single-block pass for
// block light (emissive blocks), without horizontal flood-fill.
func (c *Chunk) writeLight(w *protocol.Writer) {
full := allSectionsMask()
skyLight := make([]*[2048]byte, lightSections)
blockLight := make([]*[2048]byte, lightSections)
writeBitSet(w, full) // sky light mask: all sections present
writeBitSet(w, nil) // block light mask: none present
writeBitSet(w, nil) // empty sky light mask: none empty
writeBitSet(w, full) // empty block light mask: all empty
// Sky light arrays: one 2048-byte (4096 nibbles) array of 0x0F per section.
bright := make([]byte, 2048)
for i := range bright {
bright[i] = 0xFF // two nibbles of 15
// Section lightSections-1 is above the world, fully lit by the sky.
skyLight[lightSections-1] = new([2048]byte)
for i := range skyLight[lightSections-1] {
skyLight[lightSections-1][i] = 0xFF
}
w.VarInt(lightSections)
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
// Sky light pass
currentSky := byte(15)
for y := MinY + WorldHeight - 1; y >= MinY; y-- {
block := c.GetBlock(lx, y, lz)
op := blockOpacity[block]
if op >= currentSky {
currentSky = 0
} else {
currentSky -= op
}
if currentSky > 0 {
si := (y - MinY) >> 4
lsi := si + 1
if skyLight[lsi] == nil {
skyLight[lsi] = new([2048]byte)
}
idx := blockIndex(lx, y, lz)
if idx%2 == 0 {
skyLight[lsi][idx/2] |= currentSky
} else {
skyLight[lsi][idx/2] |= currentSky << 4
}
}
// Block light pass
em := blockEmission[block]
if em > 0 {
si := (y - MinY) >> 4
lsi := si + 1
if blockLight[lsi] == nil {
blockLight[lsi] = new([2048]byte)
}
idx := blockIndex(lx, y, lz)
if idx%2 == 0 {
blockLight[lsi][idx/2] |= em
} else {
blockLight[lsi][idx/2] |= em << 4
}
}
}
}
}
var skyMask, blockMask, emptySkyMask, emptyBlockMask uint64
var skyCount, blockCount int
for i := 0; i < lightSections; i++ {
w.VarInt(2048)
w.Raw(bright)
if skyLight[i] != nil {
skyMask |= 1 << i
skyCount++
} else {
emptySkyMask |= 1 << i
}
if blockLight[i] != nil {
blockMask |= 1 << i
blockCount++
} else {
emptyBlockMask |= 1 << i
}
}
w.VarInt(0) // no block light arrays
writeBitSet(w, []uint64{skyMask})
writeBitSet(w, []uint64{blockMask})
writeBitSet(w, []uint64{emptySkyMask})
writeBitSet(w, []uint64{emptyBlockMask})
w.VarInt(int32(skyCount))
for i := 0; i < lightSections; i++ {
if skyLight[i] != nil {
w.VarInt(2048)
w.Raw(skyLight[i][:])
}
}
w.VarInt(int32(blockCount))
for i := 0; i < lightSections; i++ {
if blockLight[i] != nil {
w.VarInt(2048)
w.Raw(blockLight[i][:])
}
}
}
// allSectionsMask returns a bitset (as longs) with the low lightSections bits set.

File diff suppressed because one or more lines are too long

17
scratch.go Normal file
View file

@ -0,0 +1,17 @@
package main
import (
"encoding/json"
"fmt"
"os"
)
func main() {
f, _ := os.Open("generated/reports/blocks.json")
var data map[string]interface{}
json.NewDecoder(f).Decode(&data)
glowstone := data["minecraft:glowstone"]
b, _ := json.MarshalIndent(glowstone, "", " ")
fmt.Println("glowstone:", string(b))
}