实施严格的三点视角

2021-06-06 06:50:42

这可能似乎是一个奇怪的文章:互联网上的每个教程都教导你三点透视只是“普通3D”的艺术术语,你设置了相机,调整距离,fov和缩放,你已经完成了。使用笔和纸时使用的消失点对应于x,y和z轴与剪切平面相交的位置,这就是她写的一切......除了这不是“真实”的三点视角。这是三点视角的易于电脑图形版本:严格的版本有点棘手。

使它令人棘手的事情是,在严格实施三点的角度来看,您的消失点必须真实地消失点:它们不代表轴向无限远的轴和距离相对于距离的剪裁平面的交叉点。您的相机,消失点是所有平行线到Infinity收敛的精确点。这是计算机图形的问题,因为这意味着我们不处理线性空间,这意味着我们不能使用线性代数来计算使用矩阵操作来计算好的“3D世界坐标到2D屏幕坐标”。这是一个轻微的问题,因为这是允许高效3D计算机图形上的基本方法,这些方法几乎是任何现代硬件。

所以让我们看看这么疯狂的是什么,以及我们如何实现它。

在我们继续之前,我想简单地说明你几乎永远不需要严格的三点视角:这是“有用”,因为这是一个相当奇怪的3D审美。但它是一个编程挑战,只有在互联网上没有人在互联网上找到了这篇文章,所以这肯定足够挑战,以解决这个疯狂的投影并解释我们需要实现的代码它。

让我们来看看两点观点,了解我们处理的内容:

我们有两个消失点,标记为z和x,以及我们简单地说“这是(0,0,0)”的一些任意的“零”点,因为现在有一个隐含的y坐标,所以我们'LL很快就会明确)。我们还需要一些点作为我们的“零”:当你绘制一张纸时,你可以决定在哪里,但是当使用计算机时,我们需要明确地了解固定点。这实际上使“两点透视”是计算机的三点透视,因为你需要指定三个点,但这既不是这里也不在那里。我们还需要说出高度从我们的零到地平线,因为(再次)你画画你可以只是翅膀它,但是计算机需要知道该值是什么,使任意坐标可以映射到屏幕正确。制作“两点视角”实际上“三点和一个数字”的观点。假设我们的角度来看,以地平线的高度只是一个,那么这将告诉我们以下观点:

与我们有一切我们需要用两点透视构建漂亮的图形。除了有一件事,你几乎肯定已经注意到了,但可能没有考虑:上面的“图表”不是正常的图表。我们看到网格变得更好,更漂亮,我们越靠近x,z以及地平线。而不是标准电网,更精确地知道笛卡尔,具体地是欧几里德,坐标系:

也就是说,而不是无限的网格,我们不能适合任何类型的有限尺寸的纸张,我们有一个有界网格,其中世界坐标与无穷大之间的距离是屏幕坐标的固定距离。只要我们了解世界和屏幕协调之间的区别:屏幕坐标就像相机一样,它是我们“看到”给出了我们告诉相机使用的投影,而世界坐标是“实际的事情” 。即使我们的投影可以将无限远变为一个定点,就像世界坐标系所关注的那样,仍然没有办法才能实际上是无限的,更不用说超越无限,即使我们的投影让我们放弃了鼠标光标完全在,甚至超过,其中Infinity将投影到屏幕上。

这是笛卡尔,但非欧几里德,坐标系的示例。也就是说:常规网格与此之间有一对一的映射,但我们不能使用线性代数来表达该映射。

然而,通过查看将无限范围映射到固定尺寸的图示,上述图形仅存在让我们对映射无穷大的想法感到舒服。我们以两点透视处理的实际投影比上面所示的更复杂:

我们的网格不仅是线性的,它甚至不是笛卡尔:虽然它看起来像这样的三角形与上述矩形网格相同,但是右上角向内移动,使其与其他两点形成对角线,即实际上不是这种情况。如果您查看左上角和右下角,您会看到我们的网格线都收敛。使用标签z和x为两点,您可以看到所有值x = ...,z =∞位于同一个点,z,如果我们所做的一切都在移动的情况下,这不是这种情况。如果就是如此,所有这些值都会在z和对角线的中点之间的某个位置(具有x的类似情况),我们的网格线不会收敛到单个点。相反,整个对角线表示“点”(∞,∞)。即使在现实中没有任何东西可以到达那里,一旦点在X,Z或两个尺寸中到达无穷大,它将变得在整个对角线上展开,而线(n,∞)和(∞,n)变得点z和x。

作为一个非笛卡尔坐标系,两点透视是一种预测的一个例子,我们将需要评估旧的方式:通过使用转换矩阵将世界坐标转换为屏幕坐标来数学上无法实现。所有现代3D图形都是基于的,从软件到GPU上物理芯片执行的数学一切都从软件到达的数学。

所以,这就是我们正在使用的,我们最好地弄清楚我们需要写作的代码。

所以,让我们实施两点透视,鉴于我们的四个点z,x和我们的零点,我们将致电C.

首先,让我们定义我们的转换以将线性值变为指数比。我们想要一些东西,给定的x = 1的值,产生值“0.5”表示可以在X轴中间找到x = 1,x = 2产生“0.75”(即三个季度距离(0沿X轴),0,0),等等。

您可能已经发现了模式,在那里我们只是通过将我们的整个步骤S的剩余时间间隔减半,所以这只是指数衰变:

当s = 0时,这给出了我们f(s)= 1,如果s =∞(当然是不可能忽略这几乎不可能),我们得到f(s)= 0。这有点与我们实际想要的相反,即有f(0)是0,f(∞)是1,所以我们可以强迫:

现在S = 0给出了我们f(s)= 0,并且s =∞给我们f(s)= 1。完美的。当然,实现这一目标是微不足道的:

我们现在可以定义一个函数,该函数将3D世界坐标(现在有Y = 0)进入2D屏幕坐标,通过做您在纸上进行的操作:找到坐标沿X轴的距离,请执行相同的距离对于Z轴,然后我们的坐标可以被绘制为从z到X轴上的点的线路,并且从x到z轴上的点。例如,绘制x = 0.5,z = 1给出:

vec2获得(双x,double z){if(x == 0&& z == 0)返回c; vec2 px = lerp(c,x,distancetoratio(x)); vec2 pz = lerp(c,z,distancetoratio(z));返回lli(x,pz,z,px); }

在此代码中,LERP是线性插值函数(对于在这种情况下为矢量而不是标量),LLI计算两行的交点:

VEC2 LLI(VEC2 L1P1,VEC2 L1P2,VEC2 L2P1,VEC2 L12){返回LLI(L1P1。X,L1P1。Y,L1P2。x,L1P2。Y,L2P1。x,l2p1。y,l2p2. x,l2p2。y ); vec2 lli(双x1,双y1,双x2,双y2,双x3,双y3,双x4,双y4){双d =(x1-x2)*(y3 - y4) - (y1 - y2)* (x3 - x4); if(d == 0)返回null;双F12 =(X1 * Y2 - Y1 * X2);双F34 =(X3 * Y4 - Y3 * X4);双NX = F12 *(x3 - x4) - f34 *(x1 - x2);双NY = F12 *(Y3 - Y4) - F34 *(Y1 - Y2);返回新的vec2(nx / d,ny / d); }

通过上面的代码到位,我们现在可以在y = 0平面上绘制东西:

void drawsomegeometry(){beginshape();顶点(0,0);顶点(3,0);顶点(3,1);顶点(1,1);顶点(1,3);顶点(o,3); endshape(关闭); beginshape();顶点(2,2);顶点(2,3);顶点(3,3);顶点(3,2); endshape(关闭); void顶点(双x,double z){addshapefertex(get(x,z)); }

当然,两点和三点的角度来看,就是绘制透视图而不是平面投影,所以让我们延长我们的get()函数,以便考虑到高度。这需要少量指定/计算的值,作为指定我们的消失点的一部分,因此我们可以在我们的高度更新的Get()函数中使用它们:

vec2 hc = lli(c,c。加(0,10),z,x); // c投射到地平线z - x双dyc = c。 y - hc。 y; //在C之间的屏幕像素中的Y距离及其投影双yscale = 5.0; //这决定了绘制的高度和#34;级别"观看者双母= DYC / YSCale; //我们需要缩放World-y以获得屏幕 - y

我们可以同时设置所有这些,我们设置x,z和c屏幕坐标,这意味着我们将在我们开始绘制事物的时间内拥有所有这些。

vec2获得(双x,双y,double z){if(x == 0& y == 0&& z == 0)返回c; //我们开始与之前相同,涵盖y == 0 vec2 px = lerp(c,x,steptodistanceratio(x)); vec2 pz = lerp(c,z,steptodistanceratio(z)); vec2 ground = lli(x,pz,z,px);如果(y == 0)返回地面; //如果它没有,我们的高度是从地面平面的垂直偏移,//基于我们的地面平面点的接近//到地平线(X-Z)的接近程度缩放了高度缩放作为我们的消失点是多么近的。 //我们到左边,或我们的中心线的右边?布尔inz =(地面。x< c。x); //基于What //关闭我们是Z,OR X的第一个高度缩放因子,具体取决于//中心线的哪一侧我们的接地平面坐标位于所在的//中心线。 Double Rx = inz? (地面。x - z. x)/(c。x - z。x):( x。x-ground。x)/(x。x - c。x); //然后,基于//确定电平面坐标对地平线接近的第二高度缩放系数。 vec2 onaxis = lli(inz?z:x,c,地面,地面。加(0,10));双RY =(接地。Y - HC。Y)/(onaxis。Y - HC。y); //我们的最终屏幕高度是世界高度,时间// Yeactor(如果X / Z坐标//为零),这是两个缩放因子的倍数。返回地面。减去(0,Rx * Ry * Y * Yfactor); }

void drawsomegeometry(){vec2 [] p = {get(0,0,0),get(1,0,2),get(0,0,2),get(0 ,7,2),GET(1,7,2),GET(1,7,0),GET(0,7,0),} // ......然后在这些角点之间的某些行... }

看起来相当不错!但这只是两点的角度。三点透视通过也通过使升高成为“固定距离到无限”轴的非欧几里德的透视。如果你这样做了这一点:事情即将真正奇怪!

我们现在有三个消失的点,可以达到三个消失点(除了像素舍入之外),并且在两点透视中,至少我们的垂直线有平行线,所以已经消失了:所有垂直现在都在y孵化。所以,让我们写一个get3( )使用三点透视计算屏幕坐标的函数。

首先,让我们勾勒出如何在这种空间中获得我们的3D点:

我们基本上正在做同样的事情我们为两点的角度来看,两次:我们从其x和y轴坐标构造点xy,然后我们为点yz做同样的事情,然后我们找到了线之间的交叉点XY-Z和线X-YZ。就是这样,我们已经完成了。在代码:

vec2 get3(双x,双y,double z){if(x == 0& y == 0&& z == 0)返回c; vec2 px = lerp(c,x,steptodistanceratio(x)); vec2 pz = lerp(c,z,steptodistanceratio(z)); if(y == 0)返回lli(x,pz,z,px); vec2 py = lerp(c,y,steptodistanceratio(y)); vec2 yz = lli(y,pz,z,py); vec2 xy = lli(y,px,x,py);返回lli(xy,z,x,yz); }

那么,我们在之前浏览了1x7x2波束的这看起来像是什么样的,使用两点透视图?

那不是很好......这是正确的,但它也很可怕......这是一个原因是,通过使用基座2的指数衰减我们真正快速地接近y。但是,我们可以通过更改我们用作指数基础的值来控制此操作。

双YBase = 1.25;双台阶optodistanceratio(双步){返回steptodistanceratio(2.0,步骤);双台阶台原序(双基础,双步){返回1.0 - 1.0 / POW(基础,步骤); vec2 get3(双x,double y,double z){if(x == 0& y == 0&& z == 0)返回c; vec2 px = lerp(c,x,steptodistanceratio(x)); vec2 pz = lerp(c,z,steptodistanceratio(z)); if(y == 0)返回lli(x,pz,z,px); vec2 py = lerp(c,y,steptodistanceratio(ybase,y)); vec2 yz = lli(y,pz,z,py); vec2 xy = lli(y,px,x,py);返回lli(xy,z,x,yz); }

哈哈哈,不,我们没有,我们实际上一直在看一个令人难以置信的幸运的边缘案例,在那里我们有效地幸运地了解我们选择的坐标,让我们漂亮,干净,直线合作。这实际上并不是指数空间在积分之间绝大多数线路如何工作,因此让我们发现大部分时间实际上看起来像什么样的形状。

到目前为止,我们一直在看绘制直,轴对齐的线条,这可能会欺骗你认为指数空间与欧几里德空间相当相似,这将是一个巨大的错误。实际上只有两种线条在严格的两个或三点视角下直接看:轴对齐线,垂直于轴。事实上,其他一切都是一种曲线,我们可以看出我们是否试图连接一些“应该”在直线上躺在一条直线上,如果这是欧几里德空间。例如,让我们在XY平面上绘制y = x / 2,看看会发生什么:

我们希望这可以是一条直线,可能指向意想不到的方向,但......

......实际上,我们大部分时间都没有得到线条。相反,在世界坐标方面躺在直线上的所有这些点都最终在指数空间中的曲线上升。这意味着当我们使用严格的两三点角度时,我们无法使用标准线原语绘制点之间的边缘。我们需要一个曲线原语。

void曲线(Vec3 p1,vec3 p2){双步= max(8.0,p2。减去(p1)。mag()); // i." 8个或更多步骤" beginshape(); for(Double i = 0; i< = 1; i + = 1.0 /步骤){顶点(get(lerp(lerp(p1,p2,i))); } endshape(); }

void drawcurveillustration(){曲线(新vec3(0,0,0),新vec3(20,5,0)); // y = 0.25x曲线(新vec3(0,0,0),新vec3(20,10,0)); // y = 0.5x曲线(新vec3(0,0,0),新vec3(20,15,0)); // y = 0.75x曲线(新vec3(0,0,0),新vec3(20,20,0)); // y = x曲线(新vec3(0,0,0),新vec3(15,20,0)); // y = 1.33x曲线(新vec3(0,0,0),新vec3(10,20,0)); // y = 2x曲线(新vec3(0,0,0),新vec3(5,20,0)); // y = 4x}

我们现在可以看到非欧几里德的这种指数空间是:欧几里德空间中发散直线的功能成为收敛曲线(在我们的消失点处会聚),除了轴对齐和垂直于轴线,保留其直线行为。

这意味着大多数时候即使是一个简单的立方体也会看起来“没有像多维数据集。当然,如果我们使用轴对齐的坐标设置具有边缘长度2的简单多维数据集,它将看起来与标准3D投影没有什么不同:

但是,如果我们旋转同样的立方体,那么三个轴就没有......好...

void draw(){vec3 center =新vec3(1,1,1,1); TawrotatedCube(2,Center,0.15); void drawrotatedcube(双边缘,vec3中心,双角度){vec3 [] pts = getCubepoints(边缘); for(vec3 p:pts){p。 Rotatex(中心,角); p。旋转(中心,角); p。 Rotatez(中心,角);绘制猜测(PTS); }

并记住:在世界坐标中,这些边缘都没有实际弯曲,它们都是直线,它们之间的完全直角。指数空间完全忽略了这一点。

现在应该非常明显,严格的三点透视是令人难以置信的利基。您将来要在未来任何一点的任何时候将用于任何东西的可能性基本上为零。但它确实教我们一些有趣的东西,也许这些对未来项目的良好灵感来说,你不知道你将在工作!