好吧,我来这里是为了寻找答案,却没有找到简单明了的东西,所以我继续做愚蠢但有效(相对简单)的事情:蒙特卡洛优化。
简而言之,算法如下:随机扰动投影矩阵,直到将已知的3D坐标投影到已知的2D坐标为止。
这是Thomas坦克引擎的静态照片:
假设我们使用GIMP在地平面上找到我们认为是正方形的2D坐标(是否真的是正方形取决于您对深度的判断):
我2D图像中得到4分:(318, 247)
,(326, 312)
,(418, 241)
,和(452, 303)
。
按照惯例,我们说,这些点应该对应于三维点:(0, 0, 0)
,(0, 0, 1)
,(1, 0, 0)
,和(1, 0, 1)
。换句话说,y = 0平面中的单位平方。
通过将4D向量[x, y, z, 1]
与4x4投影矩阵相乘,然后将x和y分量除以z来实际获得透视校正,即可将这些3D坐标中的每一个投影到2D中。这差不多是gluProject()所做的,除了gluProject()
还考虑了当前视口并考虑了单独的modelview矩阵(我们可以假设modelview矩阵是恒等矩阵)。查看gluProject()
文档非常方便,因为我实际上想要一个适用于OpenGL的解决方案,但是请注意,文档缺少公式中的z除法。
请记住,该算法是从一些投影矩阵开始,并随机对其进行扰动,直到给出所需的投影为止。因此,我们要做的是投影四个3D点中的每一个,并查看我们离想要的2D点有多近。如果我们的随机扰动导致投影的2D点越来越接近我们在上面标记的点,那么我们保留该矩阵作为对我们最初(或先前)猜测的改进。
让我们定义我们的观点:
# Known 2D coordinates of our rectangle
i0 = Point2(318, 247)
i1 = Point2(326, 312)
i2 = Point2(418, 241)
i3 = Point2(452, 303)
# 3D coordinates corresponding to i0, i1, i2, i3
r0 = Point3(0, 0, 0)
r1 = Point3(0, 0, 1)
r2 = Point3(1, 0, 0)
r3 = Point3(1, 0, 1)
我们需要从一些矩阵开始,单位矩阵似乎是自然的选择:
mat = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
]
我们需要实际实现投影(基本上是矩阵乘法):
def project(p, mat):
x = mat[0][0] * p.x + mat[0][1] * p.y + mat[0][2] * p.z + mat[0][3] * 1
y = mat[1][0] * p.x + mat[1][1] * p.y + mat[1][2] * p.z + mat[1][3] * 1
w = mat[3][0] * p.x + mat[3][1] * p.y + mat[3][2] * p.z + mat[3][3] * 1
return Point(720 * (x / w + 1) / 2., 576 - 576 * (y / w + 1) / 2.)
基本上就是gluProject()
这样,720和576分别是图像的宽度和高度(即,视口),我们从576中减去以计算出我们从顶部算起y坐标的事实,而OpenGL通常从底部。您会注意到我们没有计算z,这是因为我们在这里确实不需要z(尽管确保它在OpenGL用于深度缓冲区的范围内可能很方便)。
现在我们需要一个函数来评估我们与正确解决方案的距离。此函数返回的值将用于检查一个矩阵是否优于另一个矩阵。我选择按平方距离的总和进行运算,即:
# The squared distance between two points a and b
def norm2(a, b):
dx = b.x - a.x
dy = b.y - a.y
return dx * dx + dy * dy
def evaluate(mat):
c0 = project(r0, mat)
c1 = project(r1, mat)
c2 = project(r2, mat)
c3 = project(r3, mat)
return norm2(i0, c0) + norm2(i1, c1) + norm2(i2, c2) + norm2(i3, c3)
要扰动矩阵,我们只需选择一个要在某个范围内随机变化的元素即可:
def perturb(amount):
from copy import deepcopy
from random import randrange, uniform
mat2 = deepcopy(mat)
mat2[randrange(4)][randrange(4)] += uniform(-amount, amount)
(值得注意的是,我们的project()
函数实际上根本没有使用mat[2]
,因为我们不计算z,并且由于我们所有的y坐标均为0,所以这些mat[*][1]
值也不相关。我们可以使用此事实,而从不尝试扰乱这些值,这会带来较小的加速,但这只是练习...)
为了方便起见,让我们添加一个函数,该函数perturb()
通过一次又一次地调用到目前为止我们发现的最佳矩阵是什么来进行大部分近似处理:
def approximate(mat, amount, n=100000):
est = evaluate(mat)
for i in xrange(n):
mat2 = perturb(mat, amount)
est2 = evaluate(mat2)
if est2 < est:
mat = mat2
est = est2
return mat, est
现在剩下要做的就是运行它...:
for i in xrange(100):
mat = approximate(mat, 1)
mat = approximate(mat, .1)
我发现这已经给出了非常准确的答案。运行一段时间后,我发现的矩阵是:
[
[1.0836000765696232, 0, 0.16272110011060575, -0.44811064935115597],
[0.09339193527789781, 1, -0.7990570384334473, 0.539087345090207 ],
[0, 0, 1, 0 ],
[0.06700844759602216, 0, -0.8333379578853196, 3.875290562060915 ],
]
误差约2.6e-5
。(请注意,我们所说的未在计算中使用的元素实际上并未从初始矩阵中更改;这是因为更改这些条目不会更改评估结果,因此更改永远不会进行。)
我们可以使用将该矩阵传递到OpenGL中glLoadMatrix()
(但请记住先对其进行转置,并记住使用恒等矩阵加载您的modelview矩阵):
def transpose(m):
return [
[m[0][0], m[1][0], m[2][0], m[3][0]],
[m[0][1], m[1][1], m[2][1], m[3][1]],
[m[0][2], m[1][2], m[2][2], m[3][2]],
[m[0][3], m[1][3], m[2][3], m[3][3]],
]
glLoadMatrixf(transpose(mat))
现在我们可以例如沿z轴平移以沿轨道获得不同的位置:
glTranslate(0, 0, frame)
frame = frame + 1
glBegin(GL_QUADS)
glVertex3f(0, 0, 0)
glVertex3f(0, 0, 1)
glVertex3f(1, 0, 1)
glVertex3f(1, 0, 0)
glEnd()
从数学的角度来看,这肯定不是很优雅。您不会得到一个封闭式方程,您只需将数字插入其中即可获得直接(准确)的答案。但是,它确实允许您添加其他约束,而不必担心使方程复杂化。例如,如果我们也想合并高度,则可以使用房屋的那一角,并说(在评估函数中)从地面到屋顶的距离应该是某某某点,然后再次运行该算法。是的,这是种蛮力,但是行之有效。