Color Sorting In PHP: Part 6

2nd January 2021 - 17 minutes read time

I finished my last post on sorting colors using PHP looking at incorporating all three dimensions into the display of the colors. This lead to some interesting displays of colors in discs using different shapes or lengths as an indication of the color in question. It was still lacking actually rendering out the third dimension in any meaingful way though.

As I am essentially looking at three dimensional data I thought about displaying the data as a cube in a 3D engine. I could then map the three dimensions of colors (red, green, blue) into the three dimensions of the 3D engine (x, y, z). This means creating a 3D scene with random color data and rendering that scene out. Funnily enough, there aren't a lot of people writing 3D rendering engines in PHP so I looked into writing a very basic version that would just show point data.

What I needed to do was render simple points (called vertices) in a 3D space and then use some form of projection system to display them in 2D, which I could then render out as an image. The first step in creating the 3D engine is creating the Vertex object that can store our data. All this needs is the parameters that will store the x, y, z coordinates of the vertex in 3D space.

The following Vertex class allows us to create a single vertex containing x, y, and z coordinate data.

  1. class Vertex {
  2. public $x;
  3. public $y;
  4. public $z;
  5.  
  6. public function __construct($x, $y, $z) {
  7. $this->x = $x;
  8. $this->y = $y;
  9. $this->z = $z;
  10. }
  11. }

Next we need to set up some sort of Scene object to manage the collection of vertices. The Scene object is an implementation of the Iterator interface from PHP and so needs to implement certain methods to be able to called in a for loop. All this class does is provide a nice mechanism to store the Vertex objects in it.

  1. <?php
  2.  
  3. class Scene implements \Iterator {
  4.  
  5. protected $scene = [];
  6.  
  7. private $position = 0;
  8.  
  9. public function add(Vertex $vertex) {
  10. $this->scene[] = $vertex;
  11. }
  12.  
  13. public function count() {
  14. return count($this->scene);
  15. }
  16.  
  17. /**
  18.   * {@inheritdoc}
  19.   */
  20. public function current() {
  21. return $this->scene[$this->position];
  22. }
  23.  
  24. /**
  25.   * {@inheritdoc}
  26.   */
  27. public function next() {
  28. ++$this->position;
  29. }
  30.  
  31. /**
  32.   * {@inheritdoc}
  33.   */
  34. public function key() {
  35. return $this->position;
  36. }
  37.  
  38. /**
  39.   * {@inheritdoc}
  40.   */
  41. public function valid() {
  42. return isset($this->scene[$this->position]);
  43. }
  44.  
  45. /**
  46.   * {@inheritdoc}
  47.   */
  48. public function rewind() {
  49. $this->position = 0;
  50. }
  51.  
  52. }

With this in place we can now add Vertex objects to the Scene and then loop through the scene as if it was a normal array. This is possible thanks to the use of the Iterator interface from PHP. The following code will create a Scene object, add a single Vertex object to it and then print out the coordinates of that Vertex.

  1. $scene = new Scene();
  2. $scene->add(new Vertex(0, 0, 0));
  3.  
  4. foreach ($scene as $key => $sceneVertex) {
  5. echo $sceneVertex->x . ':' . $sceneVertex->y . ':' . $sceneVertex->z . PHP_EOL;
  6. }

One thing I want to do is create the Vertex with the x, y, z coordinates of the color in question, but then rotate and move that color within the scene. As I don't want the actual color to change as the movements are applied I decided that extending the Vertex class into a new Color class would allow me to create a single object that would contain both parts and keep them separated from each other. Here is the Color class that extends the Vertex class (meaning we can still use the Scene class to manage the objects).

  1. class Color extends Vertex {
  2. public $red = 0;
  3. public $green = 0;
  4. public $blue = 0;
  5.  
  6. public function __construct($x, $y, $z)
  7. {
  8. $this->red = $x;
  9. $this->green = $y;
  10. $this->blue = $z;
  11. parent::__construct($x, $y, $z);
  12. }
  13. }

With that in place I can now create a Color object and move it around the 3D scene without the color changing.

As I am going to be rendering the scene around the origin (ie. 0, 0, 0) I will need to translate each vertex slightly that that the centre of the cube is also at the origin. As the three different color values range from 0 to 255 the cube generated will currently have one corner at the origin so this needs to be moved around to allow it to be in the centre of the scene.

Moving a vertex in 3D space is called translation, and is just a case of adding two vertices together. To add a vertex to another vertex we just add the x, y and z coordinates together. This has the effect of moving the object in 3D space.

The following translate() method is added to the Vertex class and translates the current vertex by a vertex passed to it as a parameter.

  1. public function translate(Vertex $vertex) {
  2. $this->x = $this->x + $vertex->x;
  3. $this->y = $this->y + $vertex->y;
  4. $this->z = $this->z + $vertex->z;
  5. }

To translate the cube of 0 to 255 into the origin we need to translate it by half of that value, in other words we need to move it by -128 in the x, y and z planes so that a value of 128 (ie half way between 0 and 255) lies at the origin. Using a translation vertex we can create the Color objects and use the translate() method to move the vertex to a new location. The following code will generate 1000 random colors and move them by -128 in the x, y, and z coordinates.  Each vertex is then added to the Scene object to generate the scene.

  1. $translationVertex = new Vertex(-128, -128, -128);
  2.  
  3. for ($i = 0; $i < 1000; $i++) {
  4. $red = ceil(mt_rand(0, 255));
  5. $green = ceil(mt_rand(0, 255));
  6. $blue = ceil(mt_rand(0, 255));
  7.  
  8. $colorVertex = new Color($red, $green, $blue);
  9. $colorVertex->translate($translationVertex);
  10. $scene->add($colorVertex);
  11. }

With 1000 vertices added to the scene we can look at how to render them. Rendering a 3D scene in 2D is perhaps the important part of this process and is called projection. What we are doing is taking the 3D information and projecting it onto a flat plane so that we can view it as a flat image. There are plenty of tutorials out there discussing the finer points of different projection methods, but for the purposes of this I will be using orthogonal projection (ie. flat) which is essentially taking the x and y coordinates of the vertex and projecting it onto a 2D grid, essentially negating the z coordinates.

Projection works using a matrix that you multiply by the vertex to get the new coordinates. Matrix multiplication forms a large part of 3D mathematics and is really important to understand. If you think of a vertex like a 1D matrix then you can multiply the vertex by the matrix to find the projection of that vertex. For orthogonal projection the projection matrix is as follows.

  1. $projectionMatrix = [
  2. [1, 0, 0,],
  3. [0, 1, 0,],
  4. [0, 0, 0,],
  5. ];

If we take a vertex with the coordinates of 100, 35, 77 then we can perform the following matrix multiplication calculation on the projection matrix to find our new x and y coordinates. 

  1. [1 0 0] [100]
  2. [0 1 0] x [35]
  3. [0 0 0] [77]
  4.  
  5. (1 x 100) + (0 x 35) + (0 x 77) [100]
  6. (0 x 100) + (1 x 35) + (0 x 77) = [35]
  7. (0 x 100) + (0 x 35) + (0 x 77) [0]

Take a look at matrixmultiplication.xyz for a some neat animations on how to do matrix multiplications using different dimensions of matrix.

You might have actually spotted that the matrix calculation we are performing here doesn't do anything to the x and y values. It's important to understand how a 3D scene is rendered and so the projection calculation is an important part of this. You can apply any sort of projection matrix you want using this same method to show different perspectives.

Putting this all together, let's render the colors into our 2D image. The only other bit of logic here is that the calculated x and y coordinates are then shifted to be in the middle of the image. This is needed with this simplistic form of perspective mapping. If I was using a different approach with a camera position being part of the calculation then this wouldn't be needed. Each point is given a +1 to make it slightly easier to see.

  1. // Set up projection matrix.
  2. $projectionMatrix = [
  3. [1, 0, 0,],
  4. [0, 1, 0,],
  5. [0, 0, 0,],
  6. ];
  7.  
  8. // Set up the image.
  9. $im = imagecreatetruecolor($imageX, $imageY);
  10. $backgroundColor = imagecolorallocate($im, 255, 255, 255);
  11.  
  12. // Loop through the scene.
  13. foreach ($scene as $key => $sceneVertex) {
  14. // Get the color.
  15. $filledColor = imagecolorallocate($im, $sceneVertex->red, $sceneVertex->green, $sceneVertex->blue);
  16.  
  17. // Project the vertex onto the plane.
  18. $x = ($sceneVertex->x * $projectionMatrix[0][0]) + ($sceneVertex->y * $projectionMatrix[1][0]) + ($sceneVertex->z * $projectionMatrix[2][0]);
  19. $y = ($sceneVertex->x * $projectionMatrix[0][1]) + ($sceneVertex->y * $projectionMatrix[1][1]) + ($sceneVertex->z * $projectionMatrix[2][1]);
  20. $z = ($sceneVertex->x * $projectionMatrix[0][2]) + ($sceneVertex->y * $projectionMatrix[1][2]) + ($sceneVertex->z * $projectionMatrix[2][2]);
  21.  
  22. // Move the origin to be in the middle of the frame.
  23. $x = $x + $imageX / 2;
  24. $y = $y + $imageY / 2;
  25.  
  26. // Draw the vertex on the image.
  27. imagefilledrectangle($im, $x, $y, $x+1, $y+1, $filledColor);
  28. }
  29.  
  30. // Render the image out.
  31. imagejpeg($im, 'scene-' . time() . '.png');

Running all this code produces the following image.

1000 colors rendered in a cube with no rotation.

This is looking at the cube face on. Although it looks like we are showing the correct colors it's not clear that this is a cube. So let's add some rotation so that we can view the cube in a perspective.

Rotation in a 3D plain requires some more matrix maths. Each plane of rotation (x, y, or z) requires a slightly different calculation that rotates the point around the origin. Note that rotating around a different point (not origin) requires slightly different maths here. All we need to do is plug the right values into the matrix and then multiply the vertex by that matrix. The result of the matrix multiplication is then stored in the vertex x, y and z coordinates.

Here is the code to rotate a vertex around the origin. The single parameter here is the angle in radians (ie, not degrees).

  1. public function rotateX($angle) {
  2. $matrix = [
  3. [1, 0, 0],
  4. [0, cos($angle), sin($angle)],
  5. [0, -sin($angle), cos($angle)],
  6. ];
  7.  
  8. $x = ($this->x * $matrix[0][0]) + ($this->y * $matrix[0][1]) + ($this->z * $matrix[0][2]);
  9. $y = ($this->x * $matrix[1][0]) + ($this->y * $matrix[1][1]) + ($this->z * $matrix[1][2]);
  10. $z = ($this->x * $matrix[2][0]) + ($this->y * $matrix[2][1]) + ($this->z * $matrix[2][2]);
  11.  
  12. $this->x = $x;
  13. $this->y = $y;
  14. $this->z = $z;
  15. }
  16.  
  17. public function rotateY($angle) {
  18. $matrix = [
  19. [cos($angle), 0, -sin($angle)],
  20. [0, 1, 0],
  21. [sin($angle), 0, cos($angle)],
  22. ];
  23.  
  24. $x = ($this->x * $matrix[0][0]) + ($this->y * $matrix[0][1]) + ($this->z * $matrix[0][2]);
  25. $y = ($this->x * $matrix[1][0]) + ($this->y * $matrix[1][1]) + ($this->z * $matrix[1][2]);
  26. $z = ($this->x * $matrix[2][0]) + ($this->y * $matrix[2][1]) + ($this->z * $matrix[2][2]);
  27.  
  28. $this->x = $x;
  29. $this->y = $y;
  30. $this->z = $z;
  31. }
  32.  
  33. public function rotateZ($angle) {
  34. $matrix = [
  35. [cos($angle), sin($angle), 0],
  36. [-sin($angle), cos($angle), 0],
  37. [0, 0, 1],
  38. ];
  39.  
  40. $x = ($this->x * $matrix[0][0]) + ($this->y * $matrix[0][1]) + ($this->z * $matrix[0][2]);
  41. $y = ($this->x * $matrix[1][0]) + ($this->y * $matrix[1][1]) + ($this->z * $matrix[1][2]);
  42. $z = ($this->x * $matrix[2][0]) + ($this->y * $matrix[2][1]) + ($this->z * $matrix[2][2]);
  43.  
  44. $this->x = $x;
  45. $this->y = $y;
  46. $this->z = $z;
  47. }

Using the above we can apply a x, y and z rotation to each vertex as it is rendered by 0.5 radians, which is about 28 degrees.

  1. $sceneVertex->rotateY(0.5);
  2. $sceneVertex->rotateX(0.5);
  3. $sceneVertex->rotateZ(0.5);

Adding the rotation produces the following image.

1000 colors rendered in a cube, rotated in x, y and z.

This is looking more like a cube now that we can see the other panes of the cube coming into the perspective of the render. I'll admit that it's a bit difficult to see properly, so I created a collage of images with the cube rotating and created a gif out of it. This might be slow to load onto the page, but it shows the rotation of colors around a central point really clearly.

Rotating color cube.

Out of interest I added about 2,500,000 different colors to the scene and rendered this into a colored cube. I reduced the size of each vertex here to 1 pixel and this produced the following image.

2.5 million colors in a cube.

This looks quite like the image of a color cube that can be found on wikipedia. Note that black (or 0, 0, 0 in the reg, green, blue color space) is in the top left and white (255, 255, 255) is in the bottom right. The cube looks a little bit messy because we are rendering every color without first ensuring that we aren't going to be rendering one color over the top of another. Ignoring the z index here has caused a little side effect of not rendering the colors behind first and then rendering the colors in front afterwards. This means some of the green color is bleeding through to the front where we should be seeing only red.

Technically, I haven't done any form of sorting here. I have just plotted the coordinates of colors in three dimensions and then rendered them onto a two dimensional image. Most of the work done here was in figuring out all of the maths involved to actually generate an image. So what if we did sort the colors? This would definitely help the rendering process and would mean that the cube generated wouldn't be as messy. Adding a sort function to the Scene class means that we can sort the scene by the z coordinates meaning that things in the background would be rendered first and then things in the foreground would be rendered second, essentially overlapping the items in the background.

The following sort function was added to the Scene class. This just runs a sort on the vertices in the Scene object using the usort() PHP function and compares the z axis of the vertex.

  1. public function sort() {
  2. usort($this->scene, function ($a, $b) {
  3. $aValue = $a->z;
  4. $bValue = $b->z;
  5. return $aValue <=> $bValue;
  6. });
  7.  
  8. return $this;
  9. }

Running a sort on the scene before rendering it out produces the following image.

2.5 million colors in a cube, sorted.

This looks really nice and clearly shows the relationship between color and the coordinates in a 3D space. I haven't changed the position or colors of the previous cube in any way, all I've done is made sure that items in the background aren't printed out on top of items in the foreground.

If you are interested in looking into a 3D engine in PHP a bit more then you might be interested to know that I have fleshed out the code a bit and created a PHP3D project on github. It's far from feature complete but has some starting points of matrix calculations and rotations. I probably won't put a lot more work into it as there isn't an awful lot of use for a 3D engine in PHP, but it was useful in getting this post written and for me to figure out some of the maths involved. Feel free to use it or extend it as you need.

Add new comment

The content of this field is kept private and will not be shown publicly.