implemented first chapter (Tuples, Points, and Vectors)

This commit is contained in:
Mirko Janssen 2019-09-21 18:33:40 +02:00
parent 6749d00ebf
commit b243d2b9e6
4 changed files with 772 additions and 1 deletions

View file

@ -3,4 +3,7 @@
This project is based on the book [The Ray Tracer Challenge](https://www.amazon.de/Ray-Tracer-Challenge-Test-Driven-Renderer/dp/1680502719)
by [Jamis Buck](https://twitter.com/jamis) and will hopefully contain my own 3D renderer at some point.
I think I'm going to tag the states after each chapter, so anyone who is interested can see the progress from chapter to chapter.
I think I'm going to tag the states after each chapter, so anyone who is interested can see the progress from chapter to chapter.
**Disclaimer:** You will probably notice, that I will change name of the tests or implementing things differently from the book.
I do this in hope, that I will not regret it ... I guess we'll see ;-).

253
src/Primitives/Tuple.php Normal file
View file

@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
namespace ItsAMirko\RayTracer\Primitives;
use InvalidArgumentException;
use RuntimeException;
use function pow;
use function sqrt;
/**
* Class Tuple
*
* @package ItsAMirko\RayTracer\Primitives
*/
class Tuple
{
/**
* @var float
*/
protected $x;
/**
* @var float
*/
protected $y;
/**
* @var float
*/
protected $z;
/**
* @var float
*/
protected $w;
/**
* Tuple constructor.
*
* @param float $x
* @param float $y
* @param float $z
* @param float $w
*/
public function __construct(float $x, float $y, float $z, float $w)
{
$this->x = $x;
$this->y = $y;
$this->z = $z;
$this->w = $w;
}
/**
* @param float $x
* @param float $y
* @param float $z
*
* @return Tuple
*/
public static function createPoint(float $x, float $y, float $z): Tuple
{
return new self($x, $y, $z, 1.0);
}
/**
* @param float $x
* @param float $y
* @param float $z
*
* @return Tuple
*/
public static function createVector(float $x, float $y, float $z): Tuple
{
return new self($x, $y, $z, 0.0);
}
/**
* @return float
*/
public function x(): float
{
return $this->x;
}
/**
* @return float
*/
public function y(): float
{
return $this->y;
}
/**
* @return float
*/
public function z(): float
{
return $this->z;
}
/**
* @return float
*/
public function w(): float
{
return $this->w;
}
/**
* @return bool
*/
public function isPoint(): bool
{
return $this->w === 1.0;
}
/**
* @return bool
*/
public function isVector(): bool
{
return $this->w === 0.0;
}
/**
* @param Tuple $other
*
* @return Tuple
*/
public function plus(Tuple $other): Tuple
{
if ($this->isPoint() && $other->isPoint()) {
throw new InvalidArgumentException('Two point can not be added together');
}
return new self($this->x + $other->x(), $this->y + $other->y(), $this->z + $other->z(), $this->w + $other->w());
}
/**
* @param Tuple $other
*
* @return Tuple
*/
public function minus(Tuple $other): Tuple
{
if ($this->isVector() && $other->isPoint()) {
throw new InvalidArgumentException('Can not subtract a point from a vector');
}
return new self($this->x - $other->x(), $this->y - $other->y(), $this->z - $other->z(), $this->w - $other->w());
}
/**
* @return Tuple
*/
public function negate(): Tuple
{
return new self(-1 * $this->x, -1 * $this->y, -1 * $this->z, -1 * $this->w);
}
/**
* @param float $multiplier
*
* @return Tuple
*/
public function multiplyWith(float $multiplier): Tuple
{
return new self($multiplier * $this->x, $multiplier * $this->y, $multiplier * $this->z, $multiplier * $this->w);
}
/**
* @param float $divider
*
* @return Tuple
*/
public function divideWith(float $divider): Tuple
{
if ($divider === 0.0) {
throw new InvalidArgumentException('Can not divided with zero');
}
return new self($this->x / $divider, $this->y / $divider, $this->z / $divider, $this->w / $divider);
}
/**
* @return float
*/
public function magnitude(): float
{
if ($this->isVector() === false) {
throw new RuntimeException('This tuple needs to be a vector');
}
return sqrt(pow($this->x, 2) + pow($this->y, 2) + pow($this->z, 2));
}
/**
* @return Tuple
*/
public function normalize(): Tuple
{
return $this->divideWith($this->magnitude());
}
/**
* @param Tuple $other
*
* @return float
*/
public function dot(Tuple $other): float
{
if ($this->isVector() === false || $other->isVector() === false) {
throw new RuntimeException('Dot product can only be build for vectors');
}
return $this->x * $other->x() + $this->y * $other->y() + $this->z * $other->z;
}
/**
* @param Tuple $other
*
* @return Tuple
*/
public function cross(Tuple $other): Tuple
{
if ($this->isVector() === false || $other->isVector() === false) {
throw new RuntimeException('Cross product can only be build for vectors');
}
return new self($this->y * $other->z() - $this->z * $other->y, $this->z * $other->x() - $this->x * $other->z,
$this->x * $other->y() - $this->y * $other->x, 0.0);
}
}

61
src/virtual_cannon.php Normal file
View file

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
include __DIR__ . '/../vendor/autoload.php';
use ItsAMirko\RayTracer\Primitives\Tuple;
/**
* Class Projectile
*/
class Projectile
{
/**
* @var Tuple
*/
public $position;
/**
* @var Tuple
*/
public $velocity;
}
/**
* Class Environment
*/
class Environment
{
/**
* @var Tuple
*/
public $gravity;
/**
* @var Tuple
*/
public $wind;
}
function tick(Environment $environment, Projectile $projectile): Projectile
{
$projectile->position = $projectile->position->plus($projectile->velocity);
$projectile->velocity = $projectile->velocity->plus($environment->gravity)->plus($environment->wind);
return $projectile;
}
$environment = new Environment();
$environment->gravity = Tuple::createVector(0.0, -0.1, 0.0);
$environment->wind = Tuple::createVector(-0.01, 0.0, 0.0);
$projectile = new Projectile();
$projectile->position = Tuple::createPoint(0.0, 1.0, 0.0);
$projectile->velocity = Tuple::createVector(1.0, 1.0, 0.0)->normalize();
while ($projectile->position->y() > 0) {
echo $projectile->position->x() . ' | ' . $projectile->position->y() . PHP_EOL;
tick($environment, $projectile);
}

View file

@ -0,0 +1,454 @@
<?php
declare(strict_types=1);
namespace ItsAMirko\RayTracer\Primitives;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use function sqrt;
class TupleTest extends TestCase
{
/**
* @testdox A tuple with w=1.0 is a point
*/
public function testCanBeAPoint(): void
{
$tuple = new Tuple(4.3, -4.2, 3.1, 1.0);
$this->assertSame(4.3, $tuple->x());
$this->assertSame(-4.2, $tuple->y());
$this->assertSame(3.1, $tuple->z());
$this->assertSame(1.0, $tuple->w());
$this->assertTrue($tuple->isPoint());
$this->assertFalse($tuple->isVector());
}
/**
* @testdox A tuple with w=0.0 is a vector
*/
public function testCanBeAVector(): void
{
$tuple = new Tuple(4.3, -4.2, 3.1, 0.0);
$this->assertSame(4.3, $tuple->x());
$this->assertSame(-4.2, $tuple->y());
$this->assertSame(3.1, $tuple->z());
$this->assertSame(0.0, $tuple->w());
$this->assertFalse($tuple->isPoint());
$this->assertTrue($tuple->isVector());
}
/**
* @testdox creates tuples with w=1
*/
public function testCanBeCreatedAsPoint(): void
{
$point = Tuple::createPoint(4.0, -4.0, 3.0);
$this->assertInstanceOf(Tuple::class, $point);
$this->assertSame(4.0, $point->x());
$this->assertSame(-4.0, $point->y());
$this->assertSame(3.0, $point->z());
$this->assertSame(1.0, $point->w());
}
/**
* @testdox creates tuples with w=0
*/
public function testCanBeCreatedAsVector(): void
{
$vector = Tuple::createVector(4.0, -4.0, 3.0);
$this->assertInstanceOf(Tuple::class, $vector);
$this->assertSame(4.0, $vector->x());
$this->assertSame(-4.0, $vector->y());
$this->assertSame(3.0, $vector->z());
$this->assertSame(0.0, $vector->w());
}
/**
* @testdox Adding two tuples
* @dataProvider parametersForTupleAddition
*
* @param Tuple $tuple1
* @param Tuple $tuple2
* @param Tuple $expectedTuple
*/
public function testCanBeAddedTogether(Tuple $tuple1, Tuple $tuple2, Tuple $expectedTuple): void
{
$actualTuple = $tuple1->plus($tuple2);
$this->assertSame($expectedTuple->x(), $actualTuple->x());
$this->assertSame($expectedTuple->y(), $actualTuple->y());
$this->assertSame($expectedTuple->z(), $actualTuple->z());
$this->assertSame($expectedTuple->w(), $actualTuple->w());
}
/**
* @return array
*/
public function parametersForTupleAddition(): array
{
return [
[
new Tuple(3.0, -2.0, 5.0, 1.0),
new Tuple(-2.0, 3.0, 1.0, 0.0),
new Tuple(1.0, 1.0, 6.0, 1.0),
],
[
new Tuple(-2.0, 3.0, 1.0, 0.0),
new Tuple(3.0, -2.0, 5.0, 1.0),
new Tuple(1.0, 1.0, 6.0, 1.0),
],
[
new Tuple(1.1, 2.2, 3.3, 0.0),
new Tuple(0.9, 0.8, 0.7, 0.0),
new Tuple(2.0, 3.0, 4.0, 0.0),
],
[
new Tuple(0.9, 0.8, 0.7, 0.0),
new Tuple(1.1, 2.2, 3.3, 0.0),
new Tuple(2.0, 3.0, 4.0, 0.0),
],
];
}
/**
* @testdox Throw exception if two point should be added together
*/
public function testCanNotAddPointToAnotherPoint(): void
{
$this->expectException(InvalidArgumentException::class);
$point1 = new Tuple(1.0, 1.0, 1.0, 1.0);
$point2 = new Tuple(1.0, 1.0, 1.0, 1.0);
$point1->plus($point2);
}
/**
* @testdox Subtracting two tuples
* @dataProvider parametersForTupleSubtraction
*
* @param Tuple $tuple1
* @param Tuple $tuple2
* @param Tuple $expectedTuple
*/
public function testCanBeSubtractedFromEachOther(Tuple $tuple1, Tuple $tuple2, Tuple $expectedTuple): void
{
$actualTuple = $tuple1->minus($tuple2);
$this->assertSame($expectedTuple->x(), $actualTuple->x());
$this->assertSame($expectedTuple->y(), $actualTuple->y());
$this->assertSame($expectedTuple->z(), $actualTuple->z());
$this->assertSame($expectedTuple->w(), $actualTuple->w());
}
/**
* @return array
*/
public function parametersForTupleSubtraction(): array
{
return [
[
new Tuple(3.0, -2.0, 5.0, 1.0),
new Tuple(1.0, 1.0, 6.0, 1.0),
new Tuple(2.0, -3.0, -1.0, 0.0),
],
[
new Tuple(1.0, 1.0, 6.0, 1.0),
new Tuple(3.0, -2.0, 5.0, 1.0),
new Tuple(-2.0, 3.0, 1.0, 0.0),
],
[
new Tuple(1.1, 2.2, 3.3, 1.0),
new Tuple(2.0, 3.0, 4.0, 0.0),
new Tuple(-0.9, -0.8, -0.7, 1.0),
],
[
new Tuple(0.0, 0.0, 0.0, 0.0),
new Tuple(2.0, 3.0, 4.0, 0.0),
new Tuple(-2.0, -3.0, -4.0, 0.0),
],
[
new Tuple(2.0, 3.0, 4.0, 0.0),
new Tuple(0.9, 0.8, 0.7, 0.0),
new Tuple(1.1, 2.2, 3.3, 0.0),
],
];
}
/**
* @testdox Throw exception if a point should be subtracted from a vector
*/
public function testCanNotSubtractPointFromAVector(): void
{
$this->expectException(InvalidArgumentException::class);
$point1 = new Tuple(1.0, 1.0, 1.0, 0.0);
$point2 = new Tuple(1.0, 1.0, 1.0, 1.0);
$point1->minus($point2);
}
/**
* @testdox Negating a tuple
* @dataProvider parametersForTupleNegation
*
* @param Tuple $tuple1
* @param Tuple $expectedTuple
*/
public function testCanBeNegated(Tuple $tuple1, Tuple $expectedTuple): void
{
$actualTuple = $tuple1->negate();
$this->assertSame($expectedTuple->x(), $actualTuple->x());
$this->assertSame($expectedTuple->y(), $actualTuple->y());
$this->assertSame($expectedTuple->z(), $actualTuple->z());
$this->assertSame($expectedTuple->w(), $actualTuple->w());
}
/**
* @return array
*/
public function parametersForTupleNegation(): array
{
return [
[
new Tuple(3.0, -2.0, 5.0, 1.0),
new Tuple(-3.0, 2.0, -5.0, -1.0),
],
[
new Tuple(-3.0, 2.0, -5.0, -1.0),
new Tuple(3.0, -2.0, 5.0, 1.0),
],
[
new Tuple(1.1, 2.2, 3.3, 0.0),
new Tuple(-1.1, -2.2, -3.3, 0.0),
],
[
new Tuple(-1.1, -2.2, -3.3, 0.0),
new Tuple(1.1, 2.2, 3.3, 0.0),
],
];
}
/**
* @testdox Multiplying a tuple
* @dataProvider parametersForTupleMultiplication
*
* @param Tuple $tuple1
* @param float $multiplier
* @param Tuple $expectedTuple
*/
public function testCanBeMultiplied(Tuple $tuple1, float $multiplier, Tuple $expectedTuple): void
{
$actualTuple = $tuple1->multiplyWith($multiplier);
$this->assertSame($expectedTuple->x(), $actualTuple->x());
$this->assertSame($expectedTuple->y(), $actualTuple->y());
$this->assertSame($expectedTuple->z(), $actualTuple->z());
$this->assertSame($expectedTuple->w(), $actualTuple->w());
}
/**
* @return array
*/
public function parametersForTupleMultiplication(): array
{
return [
[
new Tuple(1.0, -2.0, 3.0, -4.0),
3.5,
new Tuple(3.5, -7.0, 10.5, -14),
],
[
new Tuple(1.0, -2.0, 3.0, -4.0),
0.5,
new Tuple(0.5, -1.0, 1.5, -2.0),
],
[
new Tuple(1.0, -2.0, 3.0, -4.0),
-1,
new Tuple(-1.0, 2.0, -3.0, 4.0),
],
];
}
/**
* @testdox Dividing a tuple
* @dataProvider parametersForTupleDivision
*
* @param Tuple $tuple1
* @param float $divider
* @param Tuple $expectedTuple
*/
public function testCanBeDivided(Tuple $tuple1, float $divider, Tuple $expectedTuple): void
{
$actualTuple = $tuple1->divideWith($divider);
$this->assertSame($expectedTuple->x(), $actualTuple->x());
$this->assertSame($expectedTuple->y(), $actualTuple->y());
$this->assertSame($expectedTuple->z(), $actualTuple->z());
$this->assertSame($expectedTuple->w(), $actualTuple->w());
}
/**
* @return array
*/
public function parametersForTupleDivision(): array
{
return [
[
new Tuple(1.0, -2.0, 3.0, -4.0),
2,
new Tuple(0.5, -1.0, 1.5, -2.0),
],
[
new Tuple(1.0, -2.0, 3.0, -4.0),
0.5,
new Tuple(2.0, -4.0, 6.0, -8.0),
],
];
}
/**
* @testdox Computing the magnitude of a vector
*/
public function testHasAMagnitude(): void
{
$this->assertSame(1.0, (new Tuple(1.0, 0.0, 0.0, 0.0))->magnitude());
$this->assertSame(1.0, (new Tuple(0.0, 1.0, 0.0, 0.0))->magnitude());
$this->assertSame(1.0, (new Tuple(0.0, 0.0, 1.0, 0.0))->magnitude());
$this->assertSame(sqrt(14), (new Tuple(1.0, 2.0, 3.0, 0.0))->magnitude());
$this->assertSame(sqrt(14), (new Tuple(-1.0, -2.0, -3.0, 0.0))->magnitude());
}
/**
* @testdox Throws an exception if the magnitude should be computed for a point
*/
public function testCanNotComputeMagnitudeForAPoint(): void
{
$this->expectException(RuntimeException::class);
Tuple::createPoint(1.0, 0.0, 0.0)->magnitude();
}
/**
* @testdox Normalizing vector
* @dataProvider parametersForVectorNormalization
*
* @param Tuple $vector
* @param Tuple $expectedVector
*/
public function testCanBeNormalized(Tuple $vector, Tuple $expectedVector): void
{
$normalizedVector = $vector->normalize();
$this->assertSame($expectedVector->x(), $normalizedVector->x());
$this->assertSame($expectedVector->y(), $normalizedVector->y());
$this->assertSame($expectedVector->z(), $normalizedVector->z());
$this->assertSame(1.0, $normalizedVector->magnitude());
}
/**
* @return array
*/
public function parametersForVectorNormalization(): array
{
return [
[
Tuple::createVector(4.0, 0.0, 0.0),
Tuple::createVector(1.0, 0.0, 0.0),
],
[
Tuple::createVector(1.0, 2.0, 3.0),
Tuple::createVector(1 / sqrt(14), 2 / sqrt(14), 3 / sqrt(14)),
],
];
}
/**
* @testdox Throws an exception if a point should be normalized
*/
public function testCanNotNormalizeAPoint(): void
{
$this->expectException(RuntimeException::class);
Tuple::createPoint(1.0, 0.0, 0.0)->normalize();
}
/**
* @testdox The dot product of two vectors
*/
public function testCanBuildDotProduct(): void
{
$vector1 = new Tuple(1.0, 2.0, 3.0, 0.0);
$vector2 = new Tuple(2.0, 3.0, 4.0, 0.0);
$this->assertSame(20.0, $vector1->dot($vector2));
}
/**
* @testdox The cross product of two vectors
* @dataProvider parametersForVectorCrossProduct
*
* @param Tuple $vector1
* @param Tuple $vector2
* @param Tuple $expectedVector
*/
public function testCanBuildCrossProduct(Tuple $vector1, Tuple $vector2, Tuple $expectedVector): void
{
$actualTuple = $vector1->cross($vector2);
$this->assertSame($expectedVector->x(), $actualTuple->x());
$this->assertSame($expectedVector->y(), $actualTuple->y());
$this->assertSame($expectedVector->z(), $actualTuple->z());
$this->assertSame($expectedVector->w(), $actualTuple->w());
}
/**
* @return array
*/
public function parametersForVectorCrossProduct(): array
{
return [
[
new Tuple(1.0, 2.0, 3.0, 0.0),
new Tuple(2.0, 3.0, 4.0, 0.0),
new Tuple(-1.0, 2.0, -1.0, 0.0),
],
[
new Tuple(2.0, 3.0, 4.0, 0.0),
new Tuple(1.0, 2.0, 3.0, 0.0),
new Tuple(1.0, -2.0, 1.0, 0.0),
],
];
}
}