Math: Kill BlockIterator, added a VoxelRayTrace class with level-independent generator functions (#1885)

This is a rather larger commit than I'm happy with, but oh well.

This kills off the enormously overcomplicated BlockIterator and replaces it with a VoxelRayTrace class containing ray tracing generator functions. These functions are independent of any Level. They yield Vector3 objects with current ray trace positions to allow implementations to handle the intercepted blocks in their own ways.

Living->getLineOfSight() now uses VoxelRayTrace instead of BlockIterator.
This commit is contained in:
Dylan K. Taylor 2018-01-10 20:14:36 +00:00 committed by GitHub
parent 24116ba846
commit 71d11c73f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 141 additions and 382 deletions

View File

@ -35,6 +35,7 @@ use pocketmine\event\Timings;
use pocketmine\item\Consumable;
use pocketmine\item\Item as ItemItem;
use pocketmine\math\Vector3;
use pocketmine\math\VoxelRayTrace;
use pocketmine\nbt\tag\ByteTag;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\FloatTag;
@ -44,7 +45,6 @@ use pocketmine\network\mcpe\protocol\EntityEventPacket;
use pocketmine\network\mcpe\protocol\MobEffectPacket;
use pocketmine\Player;
use pocketmine\utils\Binary;
use pocketmine\utils\BlockIterator;
use pocketmine\utils\Color;
abstract class Living extends Entity implements Damageable{
@ -689,11 +689,8 @@ abstract class Living extends Entity implements Damageable{
$blocks = [];
$nextIndex = 0;
$itr = new BlockIterator($this->level, $this->getPosition(), $this->getDirectionVector(), $this->getEyeHeight(), $maxDistance);
while($itr->valid()){
$itr->next();
$block = $itr->current();
foreach(VoxelRayTrace::inDirection($this->add(0, $this->eyeHeight, 0), $this->getDirectionVector(), $maxDistance) as $vector3){
$block = $this->level->getBlockAt($vector3->x, $vector3->y, $vector3->z);
$blocks[$nextIndex++] = $block;
if($maxLength !== 0 and count($blocks) > $maxLength){

View File

@ -1245,78 +1245,6 @@ class Level implements ChunkManager, Metadatable{
return $collides;
}
/*
public function rayTraceBlocks(Vector3 $pos1, Vector3 $pos2, $flag = false, $flag1 = false, $flag2 = false){
if(!is_nan($pos1->x) and !is_nan($pos1->y) and !is_nan($pos1->z)){
if(!is_nan($pos2->x) and !is_nan($pos2->y) and !is_nan($pos2->z)){
$x1 = (int) $pos1->x;
$y1 = (int) $pos1->y;
$z1 = (int) $pos1->z;
$x2 = (int) $pos2->x;
$y2 = (int) $pos2->y;
$z2 = (int) $pos2->z;
$block = $this->getBlock(Vector3::createVector($x1, $y1, $z1));
if(!$flag1 or $block->getBoundingBox() !== null){
$ob = $block->calculateIntercept($pos1, $pos2);
if($ob !== null){
return $ob;
}
}
$movingObjectPosition = null;
$k = 200;
while($k-- >= 0){
if(is_nan($pos1->x) or is_nan($pos1->y) or is_nan($pos1->z)){
return null;
}
if($x1 === $x2 and $y1 === $y2 and $z1 === $z2){
return $flag2 ? $movingObjectPosition : null;
}
$flag3 = true;
$flag4 = true;
$flag5 = true;
$i = 999;
$j = 999;
$k = 999;
if($x1 > $x2){
$i = $x2 + 1;
}elseif($x1 < $x2){
$i = $x2;
}else{
$flag3 = false;
}
if($y1 > $y2){
$j = $y2 + 1;
}elseif($y1 < $y2){
$j = $y2;
}else{
$flag4 = false;
}
if($z1 > $z2){
$k = $z2 + 1;
}elseif($z1 < $z2){
$k = $z2;
}else{
$flag5 = false;
}
//TODO
}
}
}
}
*/
public function getFullLight(Vector3 $pos) : int{
return $this->getFullLightAt($pos->x, $pos->y, $pos->z);
}

View File

@ -0,0 +1,138 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\math;
abstract class VoxelRayTrace{
/**
* Performs a ray trace from the start position in the given direction, for a distance of $maxDistance. This
* returns a Generator which yields Vector3s containing the coordinates of voxels it passes through.
*
* @param Vector3 $start
* @param Vector3 $directionVector
* @param float $maxDistance
*
* @return \Generator|Vector3[]
*/
public static function inDirection(Vector3 $start, Vector3 $directionVector, float $maxDistance) : \Generator{
return self::betweenPoints($start, $start->add($directionVector->multiply($maxDistance)));
}
/**
* Performs a ray trace between the start and end coordinates. This returns a Generator which yields Vector3s
* containing the coordinates of voxels it passes through.
*
* This is an implementation of the algorithm described in the link below.
* @link http://www.cse.yorku.ca/~amana/research/grid.pdf
*
* @param Vector3 $start
* @param Vector3 $end
*
* @return \Generator|Vector3[]
*/
public static function betweenPoints(Vector3 $start, Vector3 $end) : \Generator{
$currentBlock = $start->floor();
$directionVector = $end->subtract($start)->normalize();
if($directionVector->lengthSquared() <= 0){
throw new \InvalidArgumentException("Start and end points are the same, giving a zero direction vector");
}
$radius = $start->distance($end);
$stepX = $directionVector->x <=> 0;
$stepY = $directionVector->y <=> 0;
$stepZ = $directionVector->z <=> 0;
//Initialize the step accumulation variables depending how far into the current block the start position is. If
//the start position is on the corner of the block, these will be zero.
$tMaxX = self::rayTraceDistanceToBoundary($start->x, $directionVector->x);
$tMaxY = self::rayTraceDistanceToBoundary($start->y, $directionVector->y);
$tMaxZ = self::rayTraceDistanceToBoundary($start->z, $directionVector->z);
//The change in t on each axis when taking a step on that axis (always positive).
$tDeltaX = $directionVector->x == 0 ? 0 : $stepX / $directionVector->x;
$tDeltaY = $directionVector->y == 0 ? 0 : $stepY / $directionVector->y;
$tDeltaZ = $directionVector->z == 0 ? 0 : $stepZ / $directionVector->z;
while(true){
yield $currentBlock;
// tMaxX stores the t-value at which we cross a cube boundary along the
// X axis, and similarly for Y and Z. Therefore, choosing the least tMax
// chooses the closest cube boundary.
if($tMaxX < $tMaxY and $tMaxX < $tMaxZ){
if($tMaxX > $radius){
break;
}
$currentBlock->x += $stepX;
$tMaxX += $tDeltaX;
}elseif($tMaxY < $tMaxZ){
if($tMaxY > $radius){
break;
}
$currentBlock->y += $stepY;
$tMaxY += $tDeltaY;
}else{
if($tMaxZ > $radius){
break;
}
$currentBlock->z += $stepZ;
$tMaxZ += $tDeltaZ;
}
}
}
/**
* Returns the distance that must be travelled on an axis from the start point with the direction vector component to
* cross a block boundary.
*
* For example, given an X coordinate inside a block and the X component of a direction vector, will return the distance
* travelled by that direction component to reach a block with a different X coordinate.
*
* Find the smallest positive t such that s+t*ds is an integer.
*
* @param float $s Starting coordinate
* @param float $ds Direction vector component of the relevant axis
*
* @return float Distance along the ray trace that must be travelled to cross a boundary.
*/
private static function rayTraceDistanceToBoundary(float $s, float $ds) : float{
if($ds == 0){
return INF;
}
if($ds < 0){
$s = -$s;
$ds = -$ds;
if(floor($s) == $s){ //exactly at coordinate, will leave the coordinate immediately by moving negatively
return 0;
}
}
// problem is now s+t*ds = 1
return (1 - ($s - floor($s))) / $ds;
}
}

View File

@ -1,304 +0,0 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\utils;
use pocketmine\block\Block;
use pocketmine\level\Level;
use pocketmine\math\Vector3;
/**
* This class performs ray tracing and iterates along blocks on a line
*/
class BlockIterator implements \Iterator{
/** @var Level */
private $level;
private $maxDistance;
private static $gridSize = 16777216; //1 << 24
private $end = false;
/** @var \SplFixedArray<Block>[3] */
private $blockQueue;
private $currentBlock = 0;
/** @var Block */
private $currentBlockObject = null;
private $currentDistance = 0;
private $maxDistanceInt = 0;
private $secondError;
private $thirdError;
private $secondStep;
private $thirdStep;
private $mainFace;
private $secondFace;
private $thirdFace;
public function __construct(Level $level, Vector3 $start, Vector3 $direction, $yOffset = 0, $maxDistance = 0){
$this->level = $level;
$this->maxDistance = (int) $maxDistance;
$this->blockQueue = new \SplFixedArray(3);
$startClone = new Vector3($start->x, $start->y, $start->z);
$startClone->y += $yOffset;
$this->currentDistance = 0;
$mainDirection = 0;
$secondDirection = 0;
$thirdDirection = 0;
$mainPosition = 0;
$secondPosition = 0;
$thirdPosition = 0;
$pos = new Vector3($startClone->x, $startClone->y, $startClone->z);
$startBlock = $this->level->getBlock(new Vector3(floor($pos->x), floor($pos->y), floor($pos->z)));
if($this->getXLength($direction) > $mainDirection){
$this->mainFace = $this->getXFace($direction);
$mainDirection = $this->getXLength($direction);
$mainPosition = $this->getXPosition($direction, $startClone, $startBlock);
$this->secondFace = $this->getYFace($direction);
$secondDirection = $this->getYLength($direction);
$secondPosition = $this->getYPosition($direction, $startClone, $startBlock);
$this->thirdFace = $this->getZFace($direction);
$thirdDirection = $this->getZLength($direction);
$thirdPosition = $this->getZPosition($direction, $startClone, $startBlock);
}
if($this->getYLength($direction) > $mainDirection){
$this->mainFace = $this->getYFace($direction);
$mainDirection = $this->getYLength($direction);
$mainPosition = $this->getYPosition($direction, $startClone, $startBlock);
$this->secondFace = $this->getZFace($direction);
$secondDirection = $this->getZLength($direction);
$secondPosition = $this->getZPosition($direction, $startClone, $startBlock);
$this->thirdFace = $this->getXFace($direction);
$thirdDirection = $this->getXLength($direction);
$thirdPosition = $this->getXPosition($direction, $startClone, $startBlock);
}
if($this->getZLength($direction) > $mainDirection){
$this->mainFace = $this->getZFace($direction);
$mainDirection = $this->getZLength($direction);
$mainPosition = $this->getZPosition($direction, $startClone, $startBlock);
$this->secondFace = $this->getXFace($direction);
$secondDirection = $this->getXLength($direction);
$secondPosition = $this->getXPosition($direction, $startClone, $startBlock);
$this->thirdFace = $this->getYFace($direction);
$thirdDirection = $this->getYLength($direction);
$thirdPosition = $this->getYPosition($direction, $startClone, $startBlock);
}
$d = $mainPosition / $mainDirection;
$secondd = $secondPosition - $secondDirection * $d;
$thirdd = $thirdPosition - $thirdDirection * $d;
$this->secondError = floor($secondd * self::$gridSize);
$this->secondStep = round($secondDirection / $mainDirection * self::$gridSize);
$this->thirdError = floor($thirdd * self::$gridSize);
$this->thirdStep = round($thirdDirection / $mainDirection * self::$gridSize);
if($this->secondError + $this->secondStep <= 0){
$this->secondError = -$this->secondStep + 1;
}
if($this->thirdError + $this->thirdStep <= 0){
$this->thirdError = -$this->thirdStep + 1;
}
$lastBlock = $startBlock->getSide(Vector3::getOppositeSide($this->mainFace));
if($this->secondError < 0){
$this->secondError += self::$gridSize;
$lastBlock = $lastBlock->getSide(Vector3::getOppositeSide($this->secondFace));
}
if($this->thirdError < 0){
$this->thirdError += self::$gridSize;
$lastBlock = $lastBlock->getSide(Vector3::getOppositeSide($this->thirdFace));
}
$this->secondError -= self::$gridSize;
$this->thirdError -= self::$gridSize;
$this->blockQueue[0] = $lastBlock;
$this->currentBlock = -1;
$this->scan();
$startBlockFound = false;
for($cnt = $this->currentBlock; $cnt >= 0; --$cnt){
if($this->blockEquals($this->blockQueue[$cnt], $startBlock)){
$this->currentBlock = $cnt;
$startBlockFound = true;
break;
}
}
if(!$startBlockFound){
throw new \InvalidStateException("Start block missed in BlockIterator");
}
$this->maxDistanceInt = round($maxDistance / (sqrt($mainDirection ** 2 + $secondDirection ** 2 + $thirdDirection ** 2) / $mainDirection));
}
private function blockEquals(Block $a, Block $b){
return $a->x === $b->x and $a->y === $b->y and $a->z === $b->z;
}
private function getXFace(Vector3 $direction){
return ($direction->x > 0) ? Vector3::SIDE_EAST : Vector3::SIDE_WEST;
}
private function getYFace(Vector3 $direction){
return ($direction->y > 0) ? Vector3::SIDE_UP : Vector3::SIDE_DOWN;
}
private function getZFace(Vector3 $direction){
return ($direction->z > 0) ? Vector3::SIDE_SOUTH : Vector3::SIDE_NORTH;
}
private function getXLength(Vector3 $direction){
return abs($direction->x);
}
private function getYLength(Vector3 $direction){
return abs($direction->y);
}
private function getZLength(Vector3 $direction){
return abs($direction->z);
}
private function getPosition($direction, $position, $blockPosition){
return $direction > 0 ? ($position - $blockPosition) : ($blockPosition + 1 - $position);
}
private function getXPosition(Vector3 $direction, Vector3 $position, Block $block){
return $this->getPosition($direction->x, $position->x, $block->x);
}
private function getYPosition(Vector3 $direction, Vector3 $position, Block $block){
return $this->getPosition($direction->y, $position->y, $block->y);
}
private function getZPosition(Vector3 $direction, Vector3 $position, Block $block){
return $this->getPosition($direction->z, $position->z, $block->z);
}
public function next(){
$this->scan();
if($this->currentBlock <= -1){
throw new \OutOfBoundsException;
}else{
$this->currentBlockObject = $this->blockQueue[$this->currentBlock--];
}
}
/**
* @return Block
*
* @throws \OutOfBoundsException
*/
public function current(){
if($this->currentBlockObject === null){
throw new \OutOfBoundsException;
}
return $this->currentBlockObject;
}
public function rewind(){
throw new \InvalidStateException("BlockIterator doesn't support rewind()");
}
public function key(){
return $this->currentBlock - 1;
}
public function valid(){
$this->scan();
return $this->currentBlock !== -1;
}
private function scan(){
if($this->currentBlock >= 0){
return;
}
if($this->maxDistance !== 0 and $this->currentDistance > $this->maxDistanceInt){
$this->end = true;
return;
}
if($this->end){
return;
}
++$this->currentDistance;
$this->secondError += $this->secondStep;
$this->thirdError += $this->thirdStep;
if($this->secondError > 0 and $this->thirdError > 0){
$this->blockQueue[2] = $this->blockQueue[0]->getSide($this->mainFace);
if(($this->secondStep * $this->thirdError) < ($this->thirdStep * $this->secondError)){
$this->blockQueue[1] = $this->blockQueue[2]->getSide($this->secondFace);
$this->blockQueue[0] = $this->blockQueue[1]->getSide($this->thirdFace);
}else{
$this->blockQueue[1] = $this->blockQueue[2]->getSide($this->thirdFace);
$this->blockQueue[0] = $this->blockQueue[1]->getSide($this->secondFace);
}
$this->thirdError -= self::$gridSize;
$this->secondError -= self::$gridSize;
$this->currentBlock = 2;
}elseif($this->secondError > 0){
$this->blockQueue[1] = $this->blockQueue[0]->getSide($this->mainFace);
$this->blockQueue[0] = $this->blockQueue[1]->getSide($this->secondFace);
$this->secondError -= self::$gridSize;
$this->currentBlock = 1;
}elseif($this->thirdError > 0){
$this->blockQueue[1] = $this->blockQueue[0]->getSide($this->mainFace);
$this->blockQueue[0] = $this->blockQueue[1]->getSide($this->thirdFace);
$this->thirdError -= self::$gridSize;
$this->currentBlock = 1;
}else{
$this->blockQueue[0] = $this->blockQueue[0]->getSide($this->mainFace);
$this->currentBlock = 0;
}
}
}