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

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