diff --git a/README b/README.md similarity index 66% rename from README rename to README.md index ec42510..1366cba 100644 --- a/README +++ b/README.md @@ -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. \ No newline at end of file +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 ;-). \ No newline at end of file diff --git a/src/Primitives/Tuple.php b/src/Primitives/Tuple.php new file mode 100644 index 0000000..a1c98ea --- /dev/null +++ b/src/Primitives/Tuple.php @@ -0,0 +1,253 @@ +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); + } +} \ No newline at end of file diff --git a/src/virtual_cannon.php b/src/virtual_cannon.php new file mode 100644 index 0000000..9ae25ea --- /dev/null +++ b/src/virtual_cannon.php @@ -0,0 +1,61 @@ +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); +} \ No newline at end of file diff --git a/tests/Primitives/TupleTest.php b/tests/Primitives/TupleTest.php new file mode 100644 index 0000000..5056717 --- /dev/null +++ b/tests/Primitives/TupleTest.php @@ -0,0 +1,454 @@ +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), + ], + ]; + } +}