<?phpnamespace App\Engine;use App\Engine\Map\Level;use App\Engine\Map\Line;use App\Engine\Map\Node;use App\Engine\Map\Seg;use App\Engine\Map\Sector;use App\Engine\Map\Subsector;/** * The software renderer, a PHP port of the original engine's (r_main.c, r_bsp.c, r_segs.c, r_plane.c, * r_things.c, r_draw.c), with floats instead of fixed point: * - the BSP tree is walked front to back; each seg is clipped against the columns already filled * (solid segs) and drawn column by column (walls), while marking the floor and ceiling spans above / below it; * - floors and ceilings are drawn afterwards as horizontal spans (visplanes); * - sprites are sorted back to front and clipped by the silhouettes of the walls in front of them; * - lighting = colormaps chosen by the sector light and the distance. * The picture is a string of palette indices, w x h (w = 320, or 160 in low detail). */final class SoftRenderer{ public const SKY = 'SKY'; private const UNSET = 100000; private const SIL_BOTTOM = 1; private const SIL_TOP = 2; private const MAXINT = 1e9; public string $fb = ''; public int $frameCount = 0; /** statistics of the last frame */ public array $stats = []; public int $extraLight = 0; /** the viewer: interpolation factor between the previous and the current tic */ public float $alpha = 1.0; private float $centerX; private float $centerY; private float $projX; private float $projY; private float $clipAngle; /** @var float[] */ private array $xToAngle = []; private array $xToCos = []; private float $vx = 0.0; private float $vy = 0.0; private float $vz = 0.0; private float $vang = 0.0; private float $vcos = 1.0; private float $vsin = 0.0; private Level $level; private array $ceilClip = []; private array $floorClip = []; private array $solid = []; private array $drawSegs = []; private array $planes = []; private array $sprites = []; private int $validCount = 0; private ?object $viewer = null; // the seg being drawn private Seg $seg; private Sector $front; private ?Sector $back = null; private ?int $floorPlane = null; private ?int $ceilPlane = null; public function __construct(private Art $art, public int $w = 320, public int $h = 168) { $this->setSize($w, $h); } public function setSize(int $w, int $h): void { $this->w = $w; $this->h = $h; $this->centerX = $w / 2; $this->centerY = $h / 2; $this->projX = $w / 2; $this->projY = 160.0; $this->clipAngle = atan($this->centerX / $this->projX); $this->xToAngle = []; $this->xToCos = []; for ($x = 0; $x < $w; $x++) { $a = atan(($this->centerX - ($x + 0.5)) / $this->projX); $this->xToAngle[$x] = $a; $this->xToCos[$x] = cos($a); } $this->fb = str_repeat("\0", $w * $h); } /** * Draws the level seen from (x, y, z) looking at $angle. $viewer: the thing to hide (the player). */ public function render(Level $level, float $x, float $y, float $z, float $angle, ?object $viewer = null): void { $this->level = $level; $this->viewer = $viewer; $this->vx = $x; $this->vy = $y; $this->vz = $z; $this->vang = self::norm($angle); $this->vcos = cos($this->vang); $this->vsin = sin($this->vang); $this->frameCount++; $this->validCount++; $this->ceilClip = array_fill(0, $this->w, -1); $this->floorClip = array_fill(0, $this->w, $this->h); $this->solid = [[-self::MAXINT, -1], [$this->w, self::MAXINT]]; $this->drawSegs = []; $this->planes = []; $this->sprites = []; $this->stats = ['segs' => 0, 'planes' => 0, 'sprites' => 0]; $this->renderNode($level->root); $this->drawPlanes(); $this->drawMasked(); $this->stats['planes'] = count($this->planes); $this->stats['sprites'] = count($this->sprites); } public static function norm(float $a): float { $a = fmod($a, 2 * M_PI); return $a < 0 ? $a + 2 * M_PI : $a; } /** -PI..PI */ private static function signed(float $a): float { $a = self::norm($a); return $a > M_PI ? $a - 2 * M_PI : $a; } private function angleToX(float $rel): int { $x = (int)round($this->centerX - tan($rel) * $this->projX); return $x < 0 ? 0 : ($x > $this->w ? $this->w : $x); } // ----------------------------------------------------------------------------------------- BSP (r_bsp.c) private function renderNode(int $n): void { if ($n < 0) { $this->subsector($this->level->subsectors[-1 - $n]); return; } $node = $this->level->nodes[$n]; $side = (($this->vx - $node->x) * $node->dy - ($this->vy - $node->y) * $node->dx) >= 0 ? 0 : 1; $this->renderNode($node->children[$side]); if ($this->checkBBox($node->bbox[$side ^ 1])) { $this->renderNode($node->children[$side ^ 1]); } } private const CHECKCOORD = [ [3, 0, 2, 1], [3, 0, 2, 0], [3, 1, 2, 0], [0, 0, 0, 0], [2, 0, 2, 1], [0, 0, 0, 0], [3, 1, 3, 0], [0, 0, 0, 0], [2, 0, 3, 1], [2, 1, 3, 1], [2, 1, 3, 0], ]; /** R_CheckBBox: can a part of this box be seen (not off screen, not behind solid walls)? */ private function checkBBox(array $box): bool { $bx = $this->vx <= $box[2] ? 0 : ($this->vx < $box[3] ? 1 : 2); $by = $this->vy >= $box[0] ? 0 : ($this->vy > $box[1] ? 1 : 2); $pos = ($by << 2) + $bx; if ($pos === 5) { return true; } $c = self::CHECKCOORD[$pos]; $x1 = $box[$c[0]]; $y1 = $box[$c[1]]; $x2 = $box[$c[2]]; $y2 = $box[$c[3]]; $a1 = self::norm(atan2($y1 - $this->vy, $x1 - $this->vx) - $this->vang); $a2 = self::norm(atan2($y2 - $this->vy, $x2 - $this->vx) - $this->vang); $span = self::norm($a1 - $a2); if ($span >= M_PI) { return true; } $clip = $this->clipAngle; $t = self::norm($a1 + $clip); if ($t > 2 * $clip) { $t -= 2 * $clip; if ($t >= $span) { return false; } $a1 = $clip; } $t = self::norm($clip - $a2); if ($t > 2 * $clip) { $t -= 2 * $clip; if ($t >= $span) { return false; } $a2 = -$clip; } $sx1 = $this->angleToX(self::signed($a1)); $sx2 = $this->angleToX(self::signed($a2)); if ($sx1 === $sx2) { return false; } $sx2--; $i = 0; while ($this->solid[$i][1] < $sx2) { $i++; } return !($sx1 >= $this->solid[$i][0] && $sx2 <= $this->solid[$i][1]); } private function subsector(Subsector $sub): void { $f = $sub->sector; $this->front = $f; $this->floorPlane = $f->floor < $this->vz ? $this->findPlane($f->floor, $f->floorPic, $f->light) : null; $this->ceilPlane = ($f->ceil > $this->vz || $f->ceilPic === self::SKY) ? $this->findPlane($f->ceil, $f->ceilPic, $f->light) : null; if ($f->validCount !== $this->validCount) { $f->validCount = $this->validCount; $this->addSprites($f); } foreach ($sub->segs as $seg) { $this->addLine($seg); } } private function addLine(Seg $seg): void { $a1 = atan2($seg->y1 - $this->vy, $seg->x1 - $this->vx); $a2 = atan2($seg->y2 - $this->vy, $seg->x2 - $this->vx); $span = self::norm($a1 - $a2); if ($span >= M_PI) { return; // back side } $a1 = self::norm($a1 - $this->vang); $a2 = self::norm($a2 - $this->vang); $clip = $this->clipAngle; $t = self::norm($a1 + $clip); if ($t > 2 * $clip) { $t -= 2 * $clip; if ($t >= $span) { return; } $a1 = $clip; } $t = self::norm($clip - $a2); if ($t > 2 * $clip) { $t -= 2 * $clip; if ($t >= $span) { return; } $a2 = -$clip; } $x1 = $this->angleToX(self::signed($a1)); $x2 = $this->angleToX(self::signed($a2)); if ($x1 >= $x2) { return; } $this->seg = $seg; $back = $seg->backSector; $this->back = $back; $front = $this->front; if ($back === null || $back->ceil <= $front->floor || $back->floor >= $front->ceil) { $this->clipSolid($x1, $x2 - 1); return; } if ($back->ceil !== $front->ceil || $back->floor !== $front->floor) { $this->clipPass($x1, $x2 - 1); return; } if ($back->ceilPic === $front->ceilPic && $back->floorPic === $front->floorPic && $back->light === $front->light && $seg->sidedef->mid === null) { return; } $this->clipPass($x1, $x2 - 1); } private function clipSolid(int $first, int $last): void { $s = &$this->solid; $start = 0; while ($s[$start][1] < $first - 1) { $start++; } if ($first < $s[$start][0]) { if ($last < $s[$start][0] - 1) { $this->storeWallRange($first, $last); array_splice($s, $start, 0, [[$first, $last]]); return; } $this->storeWallRange($first, $s[$start][0] - 1); $s[$start][0] = $first; } if ($last <= $s[$start][1]) { return; } $next = $start; while ($last >= $s[$next + 1][0] - 1) { $this->storeWallRange($s[$next][1] + 1, $s[$next + 1][0] - 1); $next++; if ($last <= $s[$next][1]) { $s[$start][1] = $s[$next][1]; if ($next !== $start) { array_splice($s, $start + 1, $next - $start); } return; } } $this->storeWallRange($s[$next][1] + 1, $last); $s[$start][1] = $last; if ($next !== $start) { array_splice($s, $start + 1, $next - $start); } } private function clipPass(int $first, int $last): void { $s = $this->solid; $start = 0; while ($s[$start][1] < $first - 1) { $start++; } if ($first < $s[$start][0]) { if ($last < $s[$start][0] - 1) { $this->storeWallRange($first, $last); return; } $this->storeWallRange($first, $s[$start][0] - 1); } if ($last <= $s[$start][1]) { return; } while ($last >= $s[$start + 1][0] - 1) { $this->storeWallRange($s[$start][1] + 1, $s[$start + 1][0] - 1); $start++; if ($last <= $s[$start][1]) { return; } } $this->storeWallRange($s[$start][1] + 1, $last); } // ------------------------------------------------------------------------------------ walls (r_segs.c) /** The colormap of a wall / sprite: sector light (with the fake contrast) and scale. */ private function lightLevel(int $light, float $scale, int $contrast = 0): int { $ln = ($light >> 4) + $this->extraLight + $contrast; $ln = $ln < 0 ? 0 : ($ln > 15 ? 15 : $ln); $j = (int)($scale * 16); if ($j > 47) { $j = 47; } $level = (15 - $ln) * 4 - ($j >> 1); return $level < 0 ? 0 : ($level > 31 ? 31 : $level); } private function storeWallRange(int $start, int $stop): void { if ($start > $stop) { return; } $this->stats['segs']++; $seg = $this->seg; $front = $this->front; $back = $this->back; $side = $seg->sidedef; $line = $seg->line; $line->mapped = true; $vz = $this->vz; $w = $this->w; $h = $this->h; $cy = $this->centerY; $sa = $seg->angle; $ux = cos($sa); $uy = sin($sa); $dist = ($this->vx - $seg->x1) * $uy - ($this->vy - $seg->y1) * $ux; if ($dist < 0.1) { $dist = 0.1; } $sF = ($this->vx - $seg->x1) * $ux + ($this->vy - $seg->y1) * $uy; $base = $this->vang - $sa - M_PI / 2; $scale1 = $this->scaleAt($start, $base, $dist); $scale2 = $stop > $start ? $this->scaleAt($stop, $base, $dist) : $scale1; $step = $stop > $start ? ($scale2 - $scale1) / ($stop - $start) : 0.0; $ds = [ 'x1' => $start, 'x2' => $stop, 'scale1' => $scale1, 'scale2' => $scale2, 'step' => $step, 'seg' => $seg, 'sil' => 0, 'bsil' => self::MAXINT, 'tsil' => -self::MAXINT, 'top' => null, 'bottom' => null, 'masked' => null, ]; $worldTop = $front->ceil - $vz; $worldBottom = $front->floor - $vz; $midTex = $topTex = $bottomTex = null; $midMid = $topMid = $bottomMid = 0.0; $masked = false; $worldHigh = $worldLow = 0.0; $markFloor = $markCeil = false; $art = $this->art; if ($back === null) { $midTex = $side->mid ?? 'TECH'; $markFloor = $markCeil = true; if ($line->flags & Line::UNPEG_BOTTOM) { $midMid = $front->floor + $art->walls[$midTex]['h'] - $vz; } else { $midMid = $worldTop; } $midMid += $side->yoff; $ds['sil'] = self::SIL_BOTH; $ds['top'] = 'full'; $ds['bottom'] = 'full'; } else { $sil = 0; if ($front->floor > $back->floor) { $sil |= self::SIL_BOTTOM; $ds['bsil'] = $front->floor; } elseif ($back->floor > $vz) { $sil |= self::SIL_BOTTOM; $ds['bsil'] = self::MAXINT; } if ($front->ceil < $back->ceil) { $sil |= self::SIL_TOP; $ds['tsil'] = $front->ceil; } elseif ($back->ceil < $vz) { $sil |= self::SIL_TOP; $ds['tsil'] = -self::MAXINT; } if ($back->ceil <= $front->floor) { $ds['bottom'] = 'full'; $ds['bsil'] = self::MAXINT; $sil |= self::SIL_BOTTOM; } if ($back->floor >= $front->ceil) { $ds['top'] = 'full'; $ds['tsil'] = -self::MAXINT; $sil |= self::SIL_TOP; } $ds['sil'] = $sil; $worldHigh = $back->ceil - $vz; $worldLow = $back->floor - $vz; if ($front->ceilPic === self::SKY && $back->ceilPic === self::SKY) { $worldTop = $worldHigh; } $markFloor = $worldLow !== $worldBottom || $back->floorPic !== $front->floorPic || $back->light !== $front->light; $markCeil = $worldHigh !== $worldTop || $back->ceilPic !== $front->ceilPic || $back->light !== $front->light; if ($back->ceil <= $front->floor || $back->floor >= $front->ceil) { $markCeil = $markFloor = true; } if ($worldHigh < $worldTop) { $topTex = $side->top ?? 'STEP'; if ($line->flags & Line::UNPEG_TOP) { $topMid = $worldTop; } else { $topMid = $back->ceil + $art->walls[$topTex]['h'] - $vz; } $topMid += $side->yoff; } if ($worldLow > $worldBottom) { $bottomTex = $side->bottom ?? 'STEP'; $bottomMid = (($line->flags & Line::UNPEG_BOTTOM) ? $worldTop : $worldLow) + $side->yoff; } if ($side->mid !== null) { $masked = true; $ds['masked'] = []; } } $textured = $midTex !== null || $topTex !== null || $bottomTex !== null || $masked; $offset = $seg->offset + $side->xoff + $sF; if ($front->floor >= $vz) { $markFloor = false; } if ($front->ceil <= $vz && $front->ceilPic !== self::SKY) { $markCeil = false; } $topFrac = $cy - $worldTop * $scale1; $topStep = -$step * $worldTop; $bottomFrac = $cy - $worldBottom * $scale1; $bottomStep = -$step * $worldBottom; $pixHigh = $pixHighStep = $pixLow = $pixLowStep = 0.0; if ($back !== null) { if ($worldHigh < $worldTop) { $pixHigh = $cy - $worldHigh * $scale1; $pixHighStep = -$step * $worldHigh; } if ($worldLow > $worldBottom) { $pixLow = $cy - $worldLow * $scale1; $pixLowStep = -$step * $worldLow; } } if ($markCeil && $this->ceilPlane !== null) { $this->ceilPlane = $this->checkPlane($this->ceilPlane, $start, $stop); } else { $markCeil = false; } if ($markFloor && $this->floorPlane !== null) { $this->floorPlane = $this->checkPlane($this->floorPlane, $start, $stop); } else { $markFloor = false; } // the fake contrast of the original: east-west walls darker, north-south walls brighter $contrast = $seg->y1 === $seg->y2 ? -1 : ($seg->x1 === $seg->x2 ? 1 : 0); $light = $front->light; $fb = &$this->fb; $ceilClip = &$this->ceilClip; $floorClip = &$this->floorClip; $cp = $markCeil ? $this->ceilPlane : null; $fp = $markFloor ? $this->floorPlane : null; $scale = $scale1; $midInfo = $midTex !== null ? $art->walls[$midTex] : null; $topInfo = $topTex !== null ? $art->walls[$topTex] : null; $botInfo = $bottomTex !== null ? $art->walls[$bottomTex] : null; $vang = $this->vang; for ($x = $start; $x <= $stop; $x++) { $yl = (int)ceil($topFrac); if ($yl < $ceilClip[$x] + 1) { $yl = $ceilClip[$x] + 1; } if ($cp !== null) { $top = $ceilClip[$x] + 1; $bottom = $yl - 1; if ($bottom >= $floorClip[$x]) { $bottom = $floorClip[$x] - 1; } if ($top <= $bottom) { $this->planes[$cp]['top'][$x] = $top; $this->planes[$cp]['bottom'][$x] = $bottom; } } $yh = (int)floor($bottomFrac); if ($yh >= $floorClip[$x]) { $yh = $floorClip[$x] - 1; } if ($fp !== null) { $top = $yh + 1; $bottom = $floorClip[$x] - 1; if ($top <= $ceilClip[$x]) { $top = $ceilClip[$x] + 1; } if ($top <= $bottom) { $this->planes[$fp]['top'][$x] = $top; $this->planes[$fp]['bottom'][$x] = $bottom; } } $texCol = 0; $level = 0; $iscale = 1.0; if ($textured) { $phi = $vang + $this->xToAngle[$x] - $sa - M_PI / 2; $texCol = (int)floor($offset - $dist * tan($phi)); $level = $this->lightLevel($light, $scale, $contrast); $iscale = 1.0 / $scale; } if ($midInfo !== null) { if ($yl <= $yh) { $this->column($fb, $art->wall($midTex, $level), $midInfo, $texCol, $x, $yl, $yh, $midMid, $iscale); } $ceilClip[$x] = $h; $floorClip[$x] = -1; } else { if ($topInfo !== null) { $mid = (int)floor($pixHigh); $pixHigh += $pixHighStep; if ($mid >= $floorClip[$x]) { $mid = $floorClip[$x] - 1; } if ($mid >= $yl) { $this->column($fb, $art->wall($topTex, $level), $topInfo, $texCol, $x, $yl, $mid, $topMid, $iscale); $ceilClip[$x] = $mid; } else { $ceilClip[$x] = $yl - 1; } } elseif ($markCeil) { $ceilClip[$x] = $yl - 1; } if ($botInfo !== null) { $mid = (int)ceil($pixLow); $pixLow += $pixLowStep; if ($mid <= $ceilClip[$x]) { $mid = $ceilClip[$x] + 1; } if ($mid <= $yh) { $this->column($fb, $art->wall($bottomTex, $level), $botInfo, $texCol, $x, $mid, $yh, $bottomMid, $iscale); $floorClip[$x] = $mid; } else { $floorClip[$x] = $yh + 1; } } elseif ($markFloor) { $floorClip[$x] = $yh + 1; } if ($masked) { $ds['masked'][$x] = $texCol; } } $scale += $step; $topFrac += $topStep; $bottomFrac += $bottomStep; } // silhouettes for the sprites if ((($ds['sil'] & self::SIL_TOP) || $masked) && $ds['top'] === null) { $ds['top'] = array_slice($this->ceilClip, $start, $stop - $start + 1); } if ((($ds['sil'] & self::SIL_BOTTOM) || $masked) && $ds['bottom'] === null) { $ds['bottom'] = array_slice($this->floorClip, $start, $stop - $start + 1); } if ($masked && !($ds['sil'] & self::SIL_TOP)) { $ds['sil'] |= self::SIL_TOP; $ds['tsil'] = -self::MAXINT; } if ($masked && !($ds['sil'] & self::SIL_BOTTOM)) { $ds['sil'] |= self::SIL_BOTTOM; $ds['bsil'] = self::MAXINT; } $this->drawSegs[] = $ds; } private const SIL_BOTH = 3; private function scaleAt(int $x, float $base, float $dist): float { $phi = $base + $this->xToAngle[$x]; $s = $this->projY * cos($phi) / ($dist * $this->xToCos[$x]); return $s < 0.004 ? 0.004 : ($s > 64.0 ? 64.0 : $s); } /** R_DrawColumn: one vertical strip of a wall texture (column-major pixels). */ private function column(string &$fb, string $px, array $info, int $u, int $x, int $yl, int $yh, float $texMid, float $iscale): void { $th = $info['h']; $hmask = $info['hmask']; $base = ($u & $info['wmask']) * $th; $frac = $texMid + ($yl - $this->centerY + 0.5) * $iscale + 8192.0; $w = $this->w; $o = $yl * $w + $x; for ($y = $yl; $y <= $yh; $y++) { $fb[$o] = $px[$base + (((int)$frac) & $hmask)]; $frac += $iscale; $o += $w; } } // ------------------------------------------------------------------------------ planes (r_plane.c) private function findPlane(float $height, string $pic, int $light): int { if ($pic === self::SKY) { $height = 0.0; $light = 0; } foreach ($this->planes as $i => $p) { if ($p['height'] === $height && $p['pic'] === $pic && $p['light'] === $light) { return $i; } } $this->planes[] = ['height' => $height, 'pic' => $pic, 'light' => $light, 'minx' => $this->w, 'maxx' => -1, 'top' => array_fill(0, $this->w, self::UNSET), 'bottom' => array_fill(0, $this->w, -1)]; return count($this->planes) - 1; } private function checkPlane(int $i, int $start, int $stop): int { $p = &$this->planes[$i]; if ($start < $p['minx']) { $intrl = $p['minx']; $unionl = $start; } else { $unionl = $p['minx']; $intrl = $start; } if ($stop > $p['maxx']) { $intrh = $p['maxx']; $unionh = $stop; } else { $unionh = $p['maxx']; $intrh = $stop; } $free = true; for ($x = $intrl; $x <= $intrh; $x++) { if ($p['top'][$x] !== self::UNSET) { $free = false; break; } } if ($free) { $p['minx'] = $unionl; $p['maxx'] = $unionh; return $i; } $this->planes[] = ['height' => $p['height'], 'pic' => $p['pic'], 'light' => $p['light'], 'minx' => $start, 'maxx' => $stop, 'top' => array_fill(0, $this->w, self::UNSET), 'bottom' => array_fill(0, $this->w, -1)]; unset($p); return count($this->planes) - 1; } private function drawPlanes(): void { $fb = &$this->fb; $w = $this->w; foreach ($this->planes as $p) { if ($p['minx'] > $p['maxx']) { continue; } if ($p['pic'] === self::SKY) { $this->drawSky($fb, $p); continue; } $top = $p['top']; $bottom = $p['bottom']; $minx = $p['minx']; $maxx = $p['maxx']; $top[$maxx + 1] = self::UNSET; $bottom[$maxx + 1] = -1; $top[$minx - 1] = self::UNSET; $bottom[$minx - 1] = -1; $planeH = abs($p['height'] - $this->vz); $ln = ($p['light'] >> 4) + $this->extraLight; $ln = $ln < 0 ? 0 : ($ln > 15 ? 15 : $ln); $startMap = (15 - $ln) * 4; $spanStart = []; for ($x = $minx; $x <= $maxx + 1; $x++) { $t1 = $top[$x - 1]; $b1 = $bottom[$x - 1]; $t2 = $top[$x]; $b2 = $bottom[$x]; while ($t1 < $t2 && $t1 <= $b1) { $this->mapPlane($fb, $p['pic'], $planeH, $startMap, $t1, $spanStart[$t1], $x - 1); $t1++; } while ($b1 > $b2 && $b1 >= $t1) { $this->mapPlane($fb, $p['pic'], $planeH, $startMap, $b1, $spanStart[$b1], $x - 1); $b1--; } while ($t2 < $t1 && $t2 <= $b2) { $spanStart[$t2] = $x; $t2++; } while ($b2 > $b1 && $b2 >= $t2) { $spanStart[$b2] = $x; $b2--; } } } } /** R_MapPlane: one horizontal span of a floor / ceiling. */ private function mapPlane(string &$fb, string $pic, float $planeH, int $startMap, int $y, int $x1, int $x2): void { $dy = abs($y - $this->centerY + 0.5); $distance = $planeH * $this->projY / ($dy < 0.5 ? 0.5 : $dy); $j = (int)($distance / 16); $level = $startMap - (int)(80 / ($j + 1)); $level = $level < 0 ? 0 : ($level > 31 ? 31 : $level); $flat = $this->art->flat($pic, $level); $lat = $distance / $this->projX; $xs = $this->vsin * $lat; $ys = -$this->vcos * $lat; $off = $x1 + 0.5 - $this->centerX; $xf = $this->vx + $this->vcos * $distance + $xs * $off + 65536.0; $yf = -($this->vy + $this->vsin * $distance + $ys * $off) + 65536.0; $ys = -$ys; $o = $y * $this->w + $x1; for ($x = $x1; $x <= $x2; $x++) { $fb[$o++] = $flat[((((int)$yf) & 63) << 6) | (((int)$xf) & 63)]; $xf += $xs; $yf += $ys; } } private function drawSky(string &$fb, array $p): void { $sky = $this->art->sky; $px = $sky['px']; $sh = $sky['h']; $w = $this->w; $scaleY = 168 / max(1, $this->h) * ($this->h / 168); for ($x = $p['minx']; $x <= $p['maxx']; $x++) { $t = $p['top'][$x]; $b = $p['bottom'][$x]; if ($t > $b) { continue; } $ang = self::norm($this->vang + $this->xToAngle[$x]); $col = ((int)(1024 - $ang / (2 * M_PI) * 1024)) & 255; $base = $col * $sh; $o = $t * $w + $x; for ($y = $t; $y <= $b; $y++) { $row = (int)(($y - $this->centerY) * $scaleY + 100); $fb[$o] = $px[$base + ($row < 0 ? 0 : ($row > 127 ? 127 : $row))]; $o += $w; } } } // ---------------------------------------------------------------------------- sprites (r_things.c) private function addSprites(Sector $sector): void { $a = $this->alpha; foreach ($sector->things as $t) { if ($t === $this->viewer || !$t->visible || $t->sprite === '') { continue; } $x = $t->px + ($t->x - $t->px) * $a; $y = $t->py + ($t->y - $t->py) * $a; $z = $t->pz + ($t->z - $t->pz) * $a; $trx = $x - $this->vx; $try = $y - $this->vy; $tz = $trx * $this->vcos + $try * $this->vsin; if ($tz < 4) { continue; } $tx = $trx * $this->vsin - $try * $this->vcos; // to the right $xscale = $this->projX / $tz; $yscale = $this->projY / $tz; if (abs($tx) > $tz * 2 + 64) { continue; } $rot = 0; $ang = atan2($this->vy - $y, $this->vx - $x); $rot = ((int)floor(self::norm($ang - $t->angle + M_PI / 8) / (M_PI / 4)) % 8) + 1; $frame = $this->art->spriteFrame($t->sprite, $t->frame, $rot); if ($frame === null) { continue; } [$pic, $key] = $frame; $sx = $this->centerX + $tx * $xscale; $x1 = (int)ceil($sx - $pic['left'] * $xscale - 0.5); $x2 = (int)ceil($sx + ($pic['w'] - $pic['left']) * $xscale - 0.5) - 1; if ($x1 >= $this->w || $x2 < 0) { continue; } $gzt = $z + $pic['top']; $level = $t->bright ? 0 : $this->lightLevel($sector->light, $yscale); $start = $x1 < 0 ? 0 : $x1; $this->sprites[] = [ 'x1' => $start, 'x2' => $x2 >= $this->w ? $this->w - 1 : $x2, 'scale' => $yscale, 'xiscale' => 1 / $xscale, 'startfrac' => ($start + 0.5 - ($sx - $pic['left'] * $xscale)) / $xscale, 'gx' => $x, 'gy' => $y, 'gz' => $z, 'gzt' => $gzt, 'mid' => $gzt - $this->vz, 'pic' => $pic, 'px' => $this->art->spritePixels($pic, $key, $level), ]; } } private function drawMasked(): void { usort($this->sprites, fn (array $a, array $b) => $a['scale'] <=> $b['scale']); foreach ($this->sprites as $s) { $this->drawSprite($s); } // the masked walls (bars) not drawn yet for ($i = count($this->drawSegs) - 1; $i >= 0; $i--) { if ($this->drawSegs[$i]['masked'] !== null) { $this->maskedRange($i, $this->drawSegs[$i]['x1'], $this->drawSegs[$i]['x2']); } } } private function drawSprite(array $spr): void { $x1 = $spr['x1']; $x2 = $spr['x2']; $clipBot = []; $clipTop = []; for ($x = $x1; $x <= $x2; $x++) { $clipBot[$x] = -2; $clipTop[$x] = -2; } for ($i = count($this->drawSegs) - 1; $i >= 0; $i--) { $ds = $this->drawSegs[$i]; if ($ds['x1'] > $x2 || $ds['x2'] < $x1 || (!$ds['sil'] && $ds['masked'] === null)) { continue; } $r1 = max($ds['x1'], $x1); $r2 = min($ds['x2'], $x2); $low = min($ds['scale1'], $ds['scale2']); $high = max($ds['scale1'], $ds['scale2']); $line = $ds['seg']; $onBack = (($spr['gx'] - $line->x1) * ($line->y2 - $line->y1) - ($spr['gy'] - $line->y1) * ($line->x2 - $line->x1)) < 0; if ($high < $spr['scale'] || ($low < $spr['scale'] && !$onBack)) { if ($ds['masked'] !== null) { $this->maskedRange($i, $r1, $r2); } continue; } $sil = $ds['sil']; if ($spr['gz'] >= $ds['bsil']) { $sil &= ~self::SIL_BOTTOM; } if ($spr['gzt'] <= $ds['tsil']) { $sil &= ~self::SIL_TOP; } if ($sil & self::SIL_BOTTOM) { for ($x = $r1; $x <= $r2; $x++) { if ($clipBot[$x] === -2) { $clipBot[$x] = $ds['bottom'] === 'full' ? -1 : $ds['bottom'][$x - $ds['x1']]; } } } if ($sil & self::SIL_TOP) { for ($x = $r1; $x <= $r2; $x++) { if ($clipTop[$x] === -2) { $clipTop[$x] = $ds['top'] === 'full' ? $this->h : $ds['top'][$x - $ds['x1']]; } } } } $fb = &$this->fb; $w = $this->w; $h = $this->h; $pic = $spr['pic']; $px = $spr['px']; $ph = $pic['h']; $pw = $pic['w']; $scale = $spr['scale']; $iscale = 1 / $scale; $topScreen = $this->centerY - $spr['mid'] * $scale; $frac = $spr['startfrac']; $xi = $spr['xiscale']; for ($x = $x1; $x <= $x2; $x++, $frac += $xi) { $col = (int)$frac; if ($col < 0 || $col >= $pw) { continue; } [$first, $last] = $pic['ranges'][$col]; if ($first < 0) { continue; } $cb = $clipBot[$x] === -2 ? $h : $clipBot[$x]; $ct = $clipTop[$x] === -2 ? -1 : $clipTop[$x]; $yl = (int)ceil($topScreen + $first * $scale); $yh = (int)ceil($topScreen + ($last + 1) * $scale) - 1; if ($yl <= $ct) { $yl = $ct + 1; } if ($yh >= $cb) { $yh = $cb - 1; } if ($yl > $yh) { continue; } $base = $col * $ph; $row = ($yl + 0.5 - $topScreen) * $iscale; $o = $yl * $w + $x; for ($y = $yl; $y <= $yh; $y++) { $r = (int)$row; if ($r > $last) { $r = $last; } $c = $px[$base + $r]; if ($c !== "\xFF") { $fb[$o] = $c; } $row += $iscale; $o += $w; } } } /** R_RenderMaskedSegRange: bars / grates on two-sided lines, once per column. */ private function maskedRange(int $i, int $x1, int $x2): void { $ds = &$this->drawSegs[$i]; $seg = $ds['seg']; $side = $seg->sidedef; $tex = $side->mid; $info = $this->art->walls[$tex] ?? null; if ($info === null) { return; } $front = $seg->frontSector; $back = $seg->backSector; $th = $info['h']; if ($seg->line->flags & Line::UNPEG_BOTTOM) { $mid = max($front->floor, $back->floor) + $th - $this->vz; } else { $mid = min($front->ceil, $back->ceil) - $this->vz; } $mid += $side->yoff; $contrast = $seg->y1 === $seg->y2 ? -1 : ($seg->x1 === $seg->x2 ? 1 : 0); $fb = &$this->fb; $w = $this->w; for ($x = $x1; $x <= $x2; $x++) { if (!isset($ds['masked'][$x])) { continue; } $u = $ds['masked'][$x]; unset($ds['masked'][$x]); $scale = $ds['scale1'] + ($x - $ds['x1']) * $ds['step']; $level = $this->lightLevel($front->light, $scale, $contrast); $px = $this->art->wall($tex, $level); $top = $this->centerY - $mid * $scale; $yl = (int)ceil($top); $yh = (int)ceil($top + $th * $scale) - 1; $ct = is_array($ds['top']) ? $ds['top'][$x - $ds['x1']] : -1; $cb = is_array($ds['bottom']) ? $ds['bottom'][$x - $ds['x1']] : $this->h; if ($yl <= $ct) { $yl = $ct + 1; } if ($yh >= $cb) { $yh = $cb - 1; } $iscale = 1 / $scale; $base = ($u & $info['wmask']) * $th; $row = ($yl + 0.5 - $top) * $iscale; $o = $yl * $w + $x; for ($y = $yl; $y <= $yh; $y++) { $r = (int)$row; $c = $px[$base + ($r >= $th ? $th - 1 : $r)]; if ($c !== "\xFF") { $fb[$o] = $c; } $row += $iscale; $o += $w; } } } /** The weapon in the hands (r_things.c's psprites), drawn over everything. */ public function drawWeapon(array $pic, float $ox, float $oy, int $level): void { $fb = &$this->fb; $w = $this->w; $xs = $w / 320; // weapon pictures are made for 320 columns $ys = $this->h / 168; $px = Palette::darken($pic['px'], $level); $left = 160 - $pic['left'] + $ox; $top = 84 - $pic['top'] + $oy; $x1 = (int)ceil($left * $xs); $x2 = (int)ceil(($left + $pic['w']) * $xs) - 1; for ($x = max(0, $x1); $x <= min($w - 1, $x2); $x++) { $col = (int)(($x + 0.5) / $xs - $left); if ($col < 0 || $col >= $pic['w']) { continue; } [$first, $last] = $pic['ranges'][$col]; if ($first < 0) { continue; } $yl = max(0, (int)ceil(($top + $first) * $ys)); $yh = min($this->h - 1, (int)ceil(($top + $last + 1) * $ys) - 1); $base = $col * $pic['h']; for ($y = $yl; $y <= $yh; $y++) { $r = (int)(($y + 0.5) / $ys - $top); if ($r < 0 || $r >= $pic['h']) { continue; } $c = $px[$base + $r]; if ($c !== "\xFF") { $fb[$y * $w + $x] = $c; } } } } /** A row-major palette picture (logo, status pictures) into the frame, 255 = transparent. */ public function blit(array $pic, int $x, int $y): void { $fb = &$this->fb; $w = $pic['w']; for ($j = 0; $j < $pic['h']; $j++) { $yy = $y + $j; if ($yy < 0 || $yy >= $this->h) { continue; } for ($i = 0; $i < $w; $i++) { $c = $pic['px'][$j * $w + $i]; $xx = $x + $i; if ($c !== "\xFF" && $xx >= 0 && $xx < $this->w) { $fb[$yy * $this->w + $xx] = $c; } } } } /** The level's lines seen so far are on the automap: Line::$mapped (set while drawing). */ public function viewAngle(): float { return $this->vang; }}