diff --git a/src/world/format/Chunk.php b/src/world/format/Chunk.php index f2a89b7ef..0010063e4 100644 --- a/src/world/format/Chunk.php +++ b/src/world/format/Chunk.php @@ -40,7 +40,6 @@ use function array_fill; use function array_filter; use function array_map; use function count; -use function max; use function str_repeat; class Chunk{ @@ -323,47 +322,6 @@ class Chunk{ return $y + 1; } - /** - * Performs basic sky light population on the chunk. - * This does not cater for adjacent sky light, this performs direct sky light population only. This may cause some strange visual artifacts - * if the chunk is light-populated after being terrain-populated. - * - * @param \SplFixedArray|int[] $lightFilters - * @phpstan-param \SplFixedArray $lightFilters - * - * TODO: fast adjacent light spread - */ - public function populateSkyLight(\SplFixedArray $lightFilters) : void{ - $highestHeightMap = max($this->heightMap->getValues()); - $lowestFullyLitSubChunk = ($highestHeightMap >> 4) + (($highestHeightMap & 0xf) !== 0 ? 1 : 0); - for($y = 0; $y < $lowestFullyLitSubChunk; $y++){ - $this->getWritableSubChunk($y)->setBlockSkyLightArray(LightArray::fill(0)); - } - for($y = $lowestFullyLitSubChunk, $yMax = $this->subChunks->count(); $y < $yMax; $y++){ - $this->getWritableSubChunk($y)->setBlockSkyLightArray(LightArray::fill(15)); - } - - for($x = 0; $x < 16; ++$x){ - for($z = 0; $z < 16; ++$z){ - $y = ($lowestFullyLitSubChunk * 16) - 1; - $heightMap = $this->getHeightMap($x, $z); - - for(; $y >= $heightMap; --$y){ - $this->setBlockSkyLight($x, $y, $z, 15); - } - - $light = 15; - for(; $y >= 0; --$y){ - $light -= $lightFilters[$this->getFullBlock($x, $y, $z)]; - if($light <= 0){ - break; - } - $this->setBlockSkyLight($x, $y, $z, $light); - } - } - } - } - /** * Returns the biome ID at the specified X/Z chunk block coordinates * diff --git a/src/world/light/BlockLightUpdate.php b/src/world/light/BlockLightUpdate.php index 6cd86e4d7..d97b87d53 100644 --- a/src/world/light/BlockLightUpdate.php +++ b/src/world/light/BlockLightUpdate.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\world\light; use pocketmine\world\format\LightArray; +use pocketmine\world\format\SubChunk; use pocketmine\world\utils\SubChunkExplorer; use pocketmine\world\utils\SubChunkExplorerStatus; use function max; @@ -57,4 +58,48 @@ class BlockLightUpdate extends LightUpdate{ $this->setAndUpdateLight($x, $y, $z, max($this->lightEmitters[$block], $this->getHighestAdjacentLight($x, $y, $z) - $this->lightFilters[$block])); } } + + public function recalculateChunk(int $chunkX, int $chunkZ) : int{ + if($this->subChunkExplorer->moveToChunk($chunkX, 0, $chunkZ, false) === SubChunkExplorerStatus::INVALID){ + throw new \InvalidArgumentException("Chunk $chunkX $chunkZ does not exist"); + } + $chunk = $this->subChunkExplorer->currentChunk; + + $lightSources = 0; + foreach($chunk->getSubChunks() as $subChunkY => $subChunk){ + $subChunk->setBlockLightArray(LightArray::fill(0)); + + foreach($subChunk->getBlockLayers() as $layer){ + foreach($layer->getPalette() as $state){ + if($this->lightEmitters[$state] > 0){ + $lightSources += $this->scanForLightEmittingBlocks($subChunk, $chunkX << 4, $subChunkY << 4, $chunkZ << 4); + break 2; + } + } + } + } + + return $lightSources; + } + + private function scanForLightEmittingBlocks(SubChunk $subChunk, int $baseX, int $baseY, int $baseZ) : int{ + $lightSources = 0; + for($x = 0; $x < 16; ++$x){ + for($z = 0; $z < 16; ++$z){ + for($y = 0; $y < 16; ++$y){ + $light = $this->lightEmitters[$subChunk->getFullBlock($x, $y, $z)]; + if($light > 0){ + $this->setAndUpdateLight( + $baseX + $x, + $baseY + $y, + $baseZ + $z, + $light + ); + $lightSources++; + } + } + } + } + return $lightSources; + } } diff --git a/src/world/light/LightPopulationTask.php b/src/world/light/LightPopulationTask.php index 61d839be7..896c7a645 100644 --- a/src/world/light/LightPopulationTask.php +++ b/src/world/light/LightPopulationTask.php @@ -28,6 +28,8 @@ use pocketmine\scheduler\AsyncTask; use pocketmine\world\format\Chunk; use pocketmine\world\format\io\FastChunkSerializer; use pocketmine\world\format\LightArray; +use pocketmine\world\SimpleChunkManager; +use pocketmine\world\utils\SubChunkExplorer; use pocketmine\world\World; use function igbinary_serialize; use function igbinary_unserialize; @@ -61,9 +63,18 @@ class LightPopulationTask extends AsyncTask{ /** @var Chunk $chunk */ $chunk = FastChunkSerializer::deserialize($this->chunk); + $manager = new SimpleChunkManager(); + $manager->setChunk($this->chunkX, $this->chunkZ, $chunk); + $blockFactory = BlockFactory::getInstance(); - $chunk->recalculateHeightMap($blockFactory->blocksDirectSkyLight); - $chunk->populateSkyLight($blockFactory->lightFilter); + foreach([ + "Block" => new BlockLightUpdate(new SubChunkExplorer($manager), $blockFactory->lightFilter, $blockFactory->light), + "Sky" => new SkyLightUpdate(new SubChunkExplorer($manager), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight), + ] as $name => $update){ + $update->recalculateChunk($this->chunkX, $this->chunkZ); + $update->execute(); + } + $chunk->setLightPopulated(); $this->resultHeightMap = igbinary_serialize($chunk->getHeightMapArray()); diff --git a/src/world/light/LightUpdate.php b/src/world/light/LightUpdate.php index 8fb5569e2..8fac2e378 100644 --- a/src/world/light/LightUpdate.php +++ b/src/world/light/LightUpdate.php @@ -61,6 +61,12 @@ abstract class LightUpdate{ abstract public function recalculateNode(int $x, int $y, int $z) : void; + /** + * Scans for all light sources in the target chunk and adds them to the propagation queue. + * This erases preexisting light in the chunk. + */ + abstract public function recalculateChunk(int $chunkX, int $chunkZ) : int; + protected function getEffectiveLight(int $x, int $y, int $z) : int{ if($this->subChunkExplorer->moveTo($x, $y, $z, false) !== SubChunkExplorerStatus::INVALID){ return $this->getCurrentLightArray()->get($x & 0xf, $y & 0xf, $z & 0xf); diff --git a/src/world/light/SkyLightUpdate.php b/src/world/light/SkyLightUpdate.php index daaa99239..9b7674646 100644 --- a/src/world/light/SkyLightUpdate.php +++ b/src/world/light/SkyLightUpdate.php @@ -96,4 +96,68 @@ class SkyLightUpdate extends LightUpdate{ $this->setAndUpdateLight($x, $y, $z, max(0, $this->getHighestAdjacentLight($x, $y, $z) - $this->lightFilters[$source])); } } + + public function recalculateChunk(int $chunkX, int $chunkZ) : int{ + if($this->subChunkExplorer->moveToChunk($chunkX, 0, $chunkZ, false) === SubChunkExplorerStatus::INVALID){ + throw new \InvalidArgumentException("Chunk $chunkX $chunkZ does not exist"); + } + $chunk = $this->subChunkExplorer->currentChunk; + + $chunk->recalculateHeightMap($this->directSkyLightBlockers); + + //setAndUpdateLight() won't bother propagating from nodes that are already what we want to change them to, so we + //have to avoid filling full light for any subchunk that contains a heightmap Y coordinate + $highestHeightMapPlusOne = max($chunk->getHeightMapArray()) + 1; + $lowestClearSubChunk = ($highestHeightMapPlusOne >> 4) + (($highestHeightMapPlusOne & 0xf) !== 0 ? 1 : 0); + $chunkHeight = $chunk->getSubChunks()->count(); + for($y = 0; $y < $lowestClearSubChunk && $y < $chunkHeight; $y++){ + $chunk->getWritableSubChunk($y)->setBlockSkyLightArray(LightArray::fill(0)); + } + for($y = $lowestClearSubChunk, $yMax = $chunkHeight; $y < $yMax; $y++){ + $chunk->getWritableSubChunk($y)->setBlockSkyLightArray(LightArray::fill(15)); + } + + $lightSources = 0; + + $baseX = $chunkX << 4; + $baseZ = $chunkZ << 4; + for($x = 0; $x < 16; ++$x){ + for($z = 0; $z < 16; ++$z){ + $currentHeight = $chunk->getHeightMap($x, $z); + $maxAdjacentHeight = 0; + if($x !== 0){ + $maxAdjacentHeight = max($maxAdjacentHeight, $chunk->getHeightMap($x - 1, $z)); + } + if($x !== 15){ + $maxAdjacentHeight = max($maxAdjacentHeight, $chunk->getHeightMap($x + 1, $z)); + } + if($z !== 0){ + $maxAdjacentHeight = max($maxAdjacentHeight, $chunk->getHeightMap($x, $z - 1)); + } + if($z !== 15){ + $maxAdjacentHeight = max($maxAdjacentHeight, $chunk->getHeightMap($x, $z + 1)); + } + + /* + * We skip the top two blocks between current height and max adjacent (if there's a difference) because: + * - the block next to the highest adjacent will do nothing during propagation (it's surrounded by 15s) + * - the block below that block will do the same as the node in the highest adjacent + * NOTE: If block opacity becomes direction-aware in the future, the second point will become invalid. + */ + $nodeColumnEnd = max($currentHeight, $maxAdjacentHeight - 2); + + for($y = $currentHeight; $y <= $nodeColumnEnd; $y++){ + $this->setAndUpdateLight($x + $baseX, $y, $z + $baseZ, 15); + $lightSources++; + } + for($y = $nodeColumnEnd + 1, $yMax = $lowestClearSubChunk * 16; $y < $yMax; $y++){ + if($this->subChunkExplorer->moveTo($x + $baseX, $y, $z + $baseZ, false) !== SubChunkExplorerStatus::INVALID){ + $this->getCurrentLightArray()->set($x, $y & 0xf, $z, 15); + } + } + } + } + + return $lightSources; + } }