PHP中的颜色排序:第6部分
我完成了最后一篇关于使用PHP对颜色进行排序的文章,着眼于将所有三个维度整合到颜色的显示中。这导致使用不同形状或长度作为指示颜色的光盘中一些有趣的颜色显示。实际上,它仍然缺乏以任何有意义的方式来渲染第三维。
当我实质上查看三维数据时,我想到了将数据显示为3D引擎中的多维数据集。然后,我可以将颜色的三个维度(红色,绿色,蓝色)映射到3D引擎的三个维度(x,y,z)中。这意味着使用随机的颜色数据创建3D场景并将其渲染出来。有趣的是,用PHP编写3D渲染引擎的人并不多,所以我考虑编写一个只显示点数据的非常基本的版本。
我需要做的是在3D空间中渲染简单点(称为顶点),然后使用某种形式的投影系统以2D形式显示它们,然后将其渲染为图像。创建3D引擎的第一步是创建可以存储数据的Vertex对象。所有这些需求就是将参数存储在3D空间中的顶点的x,y,z坐标的参数。
下面的Vertex类允许我们创建一个包含x,y和z坐标数据的顶点。
class Vertex { public $x; public $y; public $z; public function __construct($x, $y, $z) { $this->x = $x; $this->y = $y; $this->z = $z; } }
接下来,我们需要设置某种Scene对象来管理顶点的集合。Scene对象是PHP的Iterator接口的实现,因此需要实现某些方法才能在for循环中调用。此类提供的所有功能都提供了一种很好的机制来在其中存储Vertex对象。
scene[] = $vertex; } public function count() { return count($this->scene); } /** * {@inheritdoc} */ public function current() { return $this->scene[$this->position]; } /** * {@inheritdoc} */ public function next() { ++$this->position; } /** * {@inheritdoc} */ public function key() { return $this->position; } /** * {@inheritdoc} */ public function valid() { return isset($this->scene[$this->position]); } /** * {@inheritdoc} */ public function rewind() { $this->position = 0; } }
现在,我们可以将Vertex对象添加到场景中,然后像遍历普通数组一样遍历场景。这要归功于使用PHP中的Iterator接口。以下代码将创建一个Scene对象,向其中添加一个Vertex对象,然后打印出该Vertex的坐标。
$scene = new Scene(); $scene->add(new Vertex(0, 0, 0)); foreach ($scene as $key => $sceneVertex) { echo $sceneVertex->x . ':' . $sceneVertex->y . ':' . $sceneVertex->z . PHP_EOL; }
我想做的一件事是使用所讨论颜色的x,y,z坐标创建顶点,然后在场景中旋转并移动该颜色。由于我不希望实际颜色随着应用的移动而改变,因此我决定将Vertex类扩展为新的Color类将使我能够创建一个包含两个部分并使它们彼此分离的对象。这是Color类的扩展,它扩展了Vertex类(这意味着我们仍然可以使用Scene类来管理对象)。
class Color extends Vertex { public $red = 0; public $green = 0; public $blue = 0; public function __construct($x, $y, $z) { $this->red = $x; $this->green = $y; $this->blue = $z; parent::__construct($x, $y, $z); } }
现在,我可以创建一个Color对象,并在3D场景中移动它,而无需更改颜色。
当我要在原点(即0、0、0)周围渲染场景时,我需要稍微平移每个顶点,以使立方体的中心也在原点处。由于三个不同的颜色值的范围从0到255,当前生成的多维数据集将在原点处具有一个角,因此需要四处移动以使其位于场景的中心。
在3D空间中移动顶点称为平移,仅是将两个顶点加在一起的情况。要将一个顶点添加到另一个顶点,我们只需将x,y和z坐标相加即可。这具有在3D空间中移动对象的效果。
将以下translate()方法添加到Vertex类,并通过作为参数传递给它的顶点来转换当前顶点。
public function translate(Vertex $vertex) { $this->x = $this->x + $vertex->x; $this->y = $this->y + $vertex->y; $this->z = $this->z + $vertex->z; }
要将0到255的多维数据集转换为原点,我们需要将其转换为该值的一半,换句话说,我们需要在x,y和z平面中将其移动-128,以使值128(即一半介于0到255之间的距离)位于原点。使用平移顶点,我们可以创建Color对象,并使用该translate()方法将顶点移动到新位置。以下代码将生成1000种随机颜色,并将它们在x,y和z坐标中移动-128。然后将每个顶点添加到Scene对象以生成场景。
$translationVertex = new Vertex(-128, -128, -128); for ($i = 0; $i < 1000; $i++) { $red = ceil(mt_rand(0, 255)); $green = ceil(mt_rand(0, 255)); $blue = ceil(mt_rand(0, 255)); $colorVertex = new Color($red, $green, $blue); $colorVertex->translate($translationVertex); $scene->add($colorVertex); }
将1000个顶点添加到场景后,我们可以查看如何渲染它们。以2D渲染3D场景可能是此过程的重要部分,称为投影。我们正在做的是获取3D信息并将其投影到平面上,以便我们可以将其视为平面图像。那里有很多教程讨论了不同投影方法的精妙之处,但是出于这个目的,我将使用正交投影(即平面),其本质上是获取顶点的x和y坐标并将其投影到2D网格,实质上是z坐标的取反。
投影使用您乘以顶点以获取新坐标的矩阵进行。矩阵乘法是3D数学的重要组成部分,对理解它非常重要。如果您将某个顶点想成一维矩阵,则可以将该顶点乘以该矩阵以找到该顶点的投影。对于正交投影,投影矩阵如下。
$projectionMatrix = [ [1, 0, 0,], [0, 1, 0,], [0, 0, 0,], ];
如果采用坐标为100、35、77的顶点,则可以对投影矩阵执行以下矩阵乘法计算,以找到新的x和y坐标。
[1 0 0] [100] [0 1 0] x [35] [0 0 0] [77] (1 x 100) + (0 x 35) + (0 x 77) [100] (0 x 100) + (1 x 35) + (0 x 77) = [35] (0 x 100) + (0 x 35) + (0 x 77) [0]
看看matrixmultiplication.xyz上的一些精美动画,了解如何使用不同尺寸的矩阵进行矩阵乘法。
您可能实际上已经发现,我们在此处执行的矩阵计算对x和y值没有任何作用。了解3D场景的渲染方式非常重要,因此投影计算是其中的重要部分。您可以使用相同的方法来应用任何类型的投影矩阵,以显示不同的透视图。
将所有这些放在一起,让我们将颜色渲染到我们的2D图像中。这里唯一的逻辑位是计算出的x和y坐标然后被移动到图像的中间。这种简单的透视图映射形式就需要这样做。如果我使用其他方法,并且将摄像机位置作为计算的一部分,则将不需要此方法。每个点都有一个+1,以便于查看。
//设置投影矩阵。 $projectionMatrix = [ [1, 0, 0,], [0, 1, 0,], [0, 0, 0,], ]; //设置图像。 $im = imagecreatetruecolor($imageX, $imageY); $backgroundColor = imagecolorallocate($im, 255, 255, 255); //循环浏览场景。 foreach ($scene as $key => $sceneVertex) { //获得颜色。 $filledColor = imagecolorallocate($im, $sceneVertex->red, $sceneVertex->green, $sceneVertex->blue); //将顶点投影到平面上。 $x = ($sceneVertex->x * $projectionMatrix[0][0]) + ($sceneVertex->y * $projectionMatrix[1][0]) + ($sceneVertex->z * $projectionMatrix[2][0]); $y = ($sceneVertex->x * $projectionMatrix[0][1]) + ($sceneVertex->y * $projectionMatrix[1][1]) + ($sceneVertex->z * $projectionMatrix[2][1]); $z = ($sceneVertex->x * $projectionMatrix[0][2]) + ($sceneVertex->y * $projectionMatrix[1][2]) + ($sceneVertex->z * $projectionMatrix[2][2]); //将原点移动到框架的中间。 $x = $x + $imageX / 2; $y = $y + $imageY / 2; //在图像上绘制顶点。 imagefilledrectangle($im, $x, $y, $x+1, $y+1, $filledColor); } //渲染图像。 imagejpeg($im, 'scene-' . time() . '.png');
运行所有这些代码将产生以下图像。
这是看着立方体的脸。尽管看起来我们显示的是正确的颜色,但尚不清楚这是一个立方体。因此,让我们添加一些旋转,以便我们可以从透视图中查看多维数据集。
在3D平面中旋转需要更多的矩阵数学运算。每个旋转平面(x,y或z)都需要稍稍不同的计算,该计算绕着原点旋转该点。请注意,绕不同的点(不是原点)旋转此处需要的数学稍有不同。我们需要做的就是将正确的值插入矩阵,然后将顶点乘以该矩阵。然后将矩阵相乘的结果存储在顶点x,y和z坐标中。
这是围绕原点旋转顶点的代码。此处的单个参数是以弧度为单位的角度(即不是度)。
public function rotateX($angle) { $matrix = [ [1, 0, 0], [0, cos($angle), sin($angle)], [0, -sin($angle), cos($angle)], ]; $x = ($this->x * $matrix[0][0]) + ($this->y * $matrix[0][1]) + ($this->z * $matrix[0][2]); $y = ($this->x * $matrix[1][0]) + ($this->y * $matrix[1][1]) + ($this->z * $matrix[1][2]); $z = ($this->x * $matrix[2][0]) + ($this->y * $matrix[2][1]) + ($this->z * $matrix[2][2]); $this->x = $x; $this->y = $y; $this->z = $z; } public function rotateY($angle) { $matrix = [ [cos($angle), 0, -sin($angle)], [0, 1, 0], [sin($angle), 0, cos($angle)], ]; $x = ($this->x * $matrix[0][0]) + ($this->y * $matrix[0][1]) + ($this->z * $matrix[0][2]); $y = ($this->x * $matrix[1][0]) + ($this->y * $matrix[1][1]) + ($this->z * $matrix[1][2]); $z = ($this->x * $matrix[2][0]) + ($this->y * $matrix[2][1]) + ($this->z * $matrix[2][2]); $this->x = $x; $this->y = $y; $this->z = $z; } public function rotateZ($angle) { $matrix = [ [cos($angle), sin($angle), 0], [-sin($angle), cos($angle), 0], [0, 0, 1], ]; $x = ($this->x * $matrix[0][0]) + ($this->y * $matrix[0][1]) + ($this->z * $matrix[0][2]); $y = ($this->x * $matrix[1][0]) + ($this->y * $matrix[1][1]) + ($this->z * $matrix[1][2]); $z = ($this->x * $matrix[2][0]) + ($this->y * $matrix[2][1]) + ($this->z * $matrix[2][2]); $this->x = $x; $this->y = $y; $this->z = $z; }
使用上面的方法,我们可以对每个顶点应用ax,y和z旋转,因为每个顶点以0.5弧度(大约28度)渲染。
$sceneVertex->rotateY(0.5); $sceneVertex->rotateX(0.5); $sceneVertex->rotateZ(0.5);
添加旋转会产生以下图像。
现在,它看起来更像一个多维数据集,我们可以看到该多维数据集的其他窗格进入了渲染的透视图。我承认,很难正确看到它,所以我创建了一个拼贴图像,立方体旋转,并从中创建了一个gif。这可能会很慢地加载到页面上,但是它确实清楚地显示了围绕中心点的颜色旋转。
出于兴趣,我为场景添加了约2,500,000种不同的颜色,并将其渲染为彩色立方体。我在这里将每个顶点的大小减小到1像素,这产生了以下图像。
这看起来很像可以在Wikipedia上找到的彩色立方体的图像。请注意,黑色(或reg中的0、0、0,绿色,蓝色颜色空间)位于左上角,而白色(255、255、255)位于右下角。立方体看起来有些混乱,因为我们渲染每种颜色时都没有先确保不会在一种颜色之上渲染一种颜色。忽略z索引会产生一些副作用,即先不渲染后方的颜色,然后再渲染前方的颜色。这意味着一些绿色正在渗入到前面,我们应该只能看到红色。
从技术上讲,我在这里没有进行任何形式的排序。我刚刚绘制了三维的颜色坐标,然后将它们渲染到二维图像上。此处所做的大部分工作是弄清楚所有实际生成图像所需的数学。那么,如果我们对颜色进行排序怎么办?这无疑将有助于渲染过程,并且意味着生成的多维数据集不会那么混乱。在Scene类中添加排序功能意味着我们可以通过z坐标对场景进行排序,这意味着将首先渲染背景中的事物,然后再渲染前景中的事物,本质上是与背景中的项目重叠。
以下排序功能已添加到Scene类。这只是使用usort()PHP函数对Scene对象中的顶点进行排序,并比较顶点的z轴。
public function sort() { usort($this->scene, function ($a, $b) { $aValue = $a->z; $bValue = $b->z; return $aValue <=> $bValue; }); return $this; }
在渲染场景之前在场景上运行排序会生成以下图像。
这看起来非常好,并且清楚地显示了3D空间中颜色与坐标之间的关系。我没有以任何方式更改前一个多维数据集的位置或颜色,我所做的一切就是确保不将背景中的项目打印在前景中的项目之上。
如果您有兴趣进一步研究PHP中的3D引擎,那么您可能会想知道我已经充实了代码并在github上创建了一个PHP3D项目。它离功能的完善还很遥远,但是具有矩阵计算和旋转的一些起点。我可能不会做太多工作,因为PHP中的3D引擎没有太多使用,但是对于撰写这篇文章以及帮助我弄清所涉及的一些数学方法很有用。随意使用或根据需要扩展它。