理解Oculus Rift畸变渲染
因为最近在搞畸变相关的东西,找了一些畸变的资料来研究,该文章英文地址,翻译中有一些个人添加的辅助信息,以括号标识,”注:”开头,以粗体表示,例如(注:以下为个人翻译,水平有限,欢迎指正).
与其他单一的技术相比,畸变渲染是Oculus Rift成为可能的最核心的技术.电脑游戏业和影视娱乐业推动了计算机渲染技术的飞速发展.在过去的几年里复杂的计算方法被广泛的使用到渲染技术当中,以便输出渲染后的帧图像,这也使像Rift这样的设备能够成为可能.
在Rift上开发一个新的应用需要详细的理解整个系统是如何运作的.对于大多数人来说,被告知镜头的畸变是被软件修复的就已经足够了,但是一个更加详细的理解往往是有用的,尤其是当你需要使渲染更加优秀,或则在畸变渲染和设置上有一些问题时.
目的
Rift上的镜头对视图应用了所谓的”枕型”畸变技术.这会使显示屏看起来拥有更大的视场角.在镜头的中心,是没有畸变的.但是当用户所看的与镜头中心的角度越来越大时,镜头上所看到的像角度小于实际的角度.如果你向左45度的方向看,镜头实际上会使传入的光产生畸变,你看到是从LCD屏上向左30度地方的光.这些值只是为了举例说明,每个镜头的畸变程度不同,值也不同.如果不进行校正就意味着越远离图像中心的地方,畸变就越大.
着色器的作用就是对图像施加”桶型” 畸变来修正之前的这种”枕型”畸变.
每一个垂直、水平的线,校准前都是向内弯曲的,我们需要按相同的弯曲度使他们向外弯曲(注:这样就修正了所有直线的弯曲).这样最理想的效果就是,我们保留了镜头增加视场角的好处,又没有畸变导致的问题.
Action
如何将”桶型”畸变应用到图片中呢?维基百科里的第二段描述了相应的计算规则.但是它用较多的数学的公式来表示这个规则,并且说明了比实际应用到着色器上更多的等式的条件.简单的说,包括如下:
1.找到畸变中心与渲染点之间的距离.我们称它为’r’,因为就像一个点落在圆的半径上一样.
2.给定一组系数,按照一定规则将这组系数与半径的乘积相加.这就得出一个我们称为distortionScale的值.
3.你正在渲染的点的最初的x,y值乘以distortionScale,得出新的x,y值就是屏幕上真正需要渲染的点的坐标.
知道这些就可以了.以下就是获取一个已知点到对应畸变中心的distortionScale.
uniform vec4 u_distortion;
float distortionScale(vec2 offset) {
// Note that this performs piecewise multiplication,
// NOT a dot or cross product
vec2 offsetSquared = offset * offset;
// Since the power to which we raise r when multiplying against each K is even
// there's no need to find r, as opposed to r^2
float radiusSquared = offsetSquared.x + offsetSquared.y;
float distortionScale = //
u_distortion[0] + //
u_distortion[1] * radiusSquared + //
u_distortion[2] * radiusSquared * radiusSquared + //
u_distortion[3] * radiusSquared * radiusSquared * radiusSquared;
return distortionScale;
}
这个函数的功能与SDK里的着色器上的HmdWarp函数类似,虽然它没有输入向量的转换,并且没有返回另一个向量,而是返回scale本身.
为什么人们对于着色器有一些问题?原因是与SDK版本相关的代码里的额外的转换相关.
Coordinate Systems
处理渲染不可避免的意味着处理坐标系统.几乎所有的系统的X轴方向是相同的,即从左到右,X轴的值不断增大.大多数数学系统中定义Y轴向上为Y轴的增量方向.
用于处理图像和渲染表面的大多数底层计算的API,将原点放在图像的左上角,当你向下移动的时候Y轴的值增加.幸运的是,通常我们不需要担心这个.OpenGL会处理我们写入到图形设的这些转换.目前来说我们只需要确保当我们加载这些到openGL纹理时我们的图片文件是垂直的就可以了.
但是,即使在OpenGL内部也有多个不同的坐标系,同时带来宽高比的问题.在你的电脑中看看Rift屏幕的实际像素排列如下:
考虑在我们畸变渲染之前会发生什么.场景将被渲染到一个屏外缓冲区.因为实际上我们做立体渲染的时候实际上是有2个屏外缓冲区,分别是Rift屏幕的左右两半.现在,让我们忽略立体渲染的现实情况,而只考虑左半边.
为了使用着色器,当着色器使能时我们用一个单一的纹理占满整个屏幕来渲染屏外缓冲区.做这些事情,当前的OpenGL实际上调用到大量的公式化的代码,这些代码涉及到顶点缓冲区和顶点属性指针.为了说明的目的,我们将使用旧的OpenGL 1.x的API调用来做这样的事情:
glBegin(GL_QUADS);
glTexCoord2f( 0, 0); glVertex2f(-1, -1);
glTexCoord2f( 1, 0); glVertex2f( 1, -1);
glTexCoord2f( 1, 1); glVertex2f( 1, 1);
glTexCoord2f( 0, 1); glVertex2f(-1, 1);
glEnd();
这些代码不包含纹理的绑定,着色器的安装,以及着色器的绑定.它仅仅列出了4个纹理坐标和4个顶点坐标,以及告诉OpenGL按照这个信息画一个矩形.
如下图说明了这些纹理坐标和顶点坐标的不同:
这些 OpenGL顶点坐标,默认范围是从-1到1.OpenGL纹理坐标范围从0到1,当你渲染一个纹理到整个屏幕是你会发现以上的不匹配.
那么我们为什么要关心所有的这些呢?好的,OpenGL片段着色器有非常有限的信息关于哪一部分需要渲染.当需要渲染一个单一像素到屏幕的四分之三的部分,相对于右边的四分之三,片段着色器将会被调用,并且被告知渲染纹理坐标为(0.75,0.25).从那里我们必须确定如何从纹理中提取的畸变坐标.跳过distortionScale()函数以上部分将不能正常工作.这些坐标并不代码镜头中心的偏移量,而是来自渲染表面的左下角.为了获得这个值,我们需要做一些另外的处理.
第一步是简单的.我们将纹理坐标翻倍,并且减去1.这样将我们移动到顶点坐标系当中.(注:看上面纹理坐标和顶点坐标的图,将纹理坐标翻倍,然后下移一个单位1,就跟顶点坐标一样了).
vec2 result = texCoord * 2.0 - 1.0;
纹理坐标向量的长度现在来看就是点到屏幕中心的距离.但是,这个偏移量并不是我们想要的.我们需要镜片中心在屏上的点的偏移量.
所以,我们将去镜片中心的偏移量:
result -= u_lensCenterOffset;
然而,这样仍然不足以传递给我们的distortionScale函数,因为实际运用中不能使用我们的偏移量来计算半径.这是因为我们的X轴和Y轴并不是相同的缩放比例.我们的X轴实际上是Y轴的0.8倍.(注:如上面图所示Rift单眼的分辨率是640*800,即X轴是Y轴的0.8倍),所以我们需要将Y轴偏移量除以0.8.
result.y /= u_aspect;
最后我们可以得出一个向量,用来作为distortionScale函数的参数传递进去.这个转换被封装为一个函数,如下:
vec2 textureCoordsToDistortionOffsetCoords(vec2 texCoord) {
vec2 result = texCoord // Convert the texture coordinates from "0 to 1" to "-1 to 1"
result *= 2.0; result -= 1.0;
// Convert from using the center of the screen as the origin to
// using the lens center as the origin
result -= u_lensCenterOffset;
// Correct for the aspect ratio
result.y /= u_aspect;
return result;
}
一旦我们生成了畸变偏移量,我们不能直接把它使用到纹理当中.我们不得不按我们获得他的方式的逆向过程来获取它(注:即从畸变偏移量逆向得出纹理坐标,下面这个函数与上面函数互逆):
vec2 distortionOffsetCoordsToTextureCoords(vec2 offset) {
vec2 result = offset;
// Correct for the aspect ratio
result.y *= u_aspect;
// Convert from using the lens center as the origin to
// using the screen center as the origin
result += u_lensCenterOffset;
// Convert the texture coordinates from "-1 to 1" to "0 to 1"
result += 1.0;
result /= 2.0;
return result;
}
这里有2点需要附加说明的.首先,这种方法缩小了渲染的数据,导致这些渲染数据不能充满全屏,或是接近屏幕的边缘.为了消除这样的情况,一个缩放因子被应用到最后的处理函数传递进来的偏移量上(注:缩放因子就是下面代码中的第一句的”u_fillScale”).这个缩放是通过找到你想要达到的屏幕上的点,找到它的偏移量,找到该偏移量的畸变值,然后找到该畸变的原始距离的比率.在实际情况中,偏移量以你想要渲染的X轴距离的最大值.所以这个偏移量是加上了镜头的偏移距离.在SDK的StereoConfig类中有一个GetDistortionScale()的函数,为你提供这个值,在片段着色器中方法如下所示:
vec2 distortionOffsetCoordsToTextureCoords(vec2 offset) {
vec2 result = offset / u_fillScale;
// Correct for the aspect ratio
result.y *= u_aspect;
// Convert from using the lens center as the origin to
// using the screen center as the origin
result += u_lensCenterOffset;
// Convert the texture coordinates from "-1 to 1" to "0 to 1"
result += 1.0;
result /= 2.0;
return result;
}
第二个需要附加说明的就是:多数渲染系统提出纹理的尺寸是2的幂次方.如果你创建的屏外缓冲区尺寸是640x800,那么很可能底层的纹理是1024x1024.这就意味着最终的缩放因子必须被应用到纹理坐标中,使点实际存在于纹理内存的子区域中.
片段着色器的最终版本在这里,它有一个对应的顶点着色器在这里,顶点着色器基本上就2行代码,并不有趣.它存在的原因是为了在不做任何转换的情况下提供纹理着色器.
注意所有的这些代码的目的是为了说明而提供的,并不是OpenGL的最佳实现方式.