Multi-View与Multi-Viewport
随着VR的流行,内置地支持multi-view和multi-viewport已经成为GPU的一种需要。当前GPU主要通过支持已有的扩展来内置实现multi-view和multi-viewport,本篇对常见的几个扩展做下总结。
1. Stereo rendering与Multi-view
Stereo Rendering(立体渲染)是一种让人眼能感受到立体效果的渲染方式,为给予人眼立体效果,需要2个Camera对同一个场景进行成像,即设置不同的View矩阵渲染两张图像,这两张略有差异的图片能使人眼感觉出深度。在普通的pipeline下,立体渲染的一种方案是在Vertex Shader中实现按不同View渲染两次[1]:
Set MVP_L for left camera
Rendering Geometry
Set MVP_R for right camera
Rendering Geometry
可以看到,同样的Geometry被重复渲染了两遍,CPU-GPU的带宽加倍,而这两份Geometry大部分Shading计算(如Model Transform,Attribute计算等)是相同的,仅仅只有View矩阵存在差异。这些数据或计算上的冗余显然可以在GPU内部优化,以提高Performance。
另一种方案是起Geometry Shader,在GS里边给每个View送出各自的position或attribute:
for(int view_id =0; view_id < 2; view_id++)
{
MVP = MVP_Matrix[view_id];
gl_Position = MVP * in_position;
}
这样能避免CPU-GPU带宽问题,同时可以避免相同属性的重复运算,但GS的performance本身并不理想。在GS单个Shader中loop过多的顶点属性,本身会使用较多资源,进而限制Shading的并行度。
OVR_multiview扩展旨在让GPU支持更高效的Multiview rendering(最常见是2个View),启用该扩展时,GPU将在Vertex Shader(VS)中loop,使用ViewID得到per-view的属性,如position和依赖于view的normal(OVR_multiview只允许position依赖于view_ID,不过新的扩展OVR_multiview2放宽了限制,允许其他属性根据view_ID取值)。
新的函数FramebufferTextureMultiviewOVR与FramebufferTextureMultiview类似,但它会使支持该扩展的GPULoop多次。
FramebufferTextureMultiviewOVR( enum target, enum attachment, uint texture, int level, int baseViewIndex, sizei numViews );
一个简单的例子[2],在VS中,我们用modelViewProjection[gl_ViewID_OVR]获取MVP矩阵,而同样的几何体会被渲染两遍,第一次gl_ViewID_OVR=0,第二次gl_ViewID_OVR=1。
#version 300 es
#extension GL_OVR_multiview : enable
layout(num_views = 2) in;
in vec3 vertexPosition;
in vec3 vertexNormal;
uniform mat4 modelViewProjection[2];
uniform mat4 model;
out vec3 v_normal;
void main()
{
gl_Position = modelViewProjection[gl_ViewID_OVR] * vec4(vertexPosition, 1.0);
v_normal = (model * vec4(vertexNormal, 0.0f)).xyz;
}
#version 300 es
precision mediump float;
in vec3 v_normal;
out vec4 f_color;
vec3 light(vec3 n, vec3 l, vec3 c)
{
float ndotl = max(dot(n, l), 0.0);
return ndotl * c;
}
void main()
{
vec3 albedo = vec3(0.95, 0.84, 0.62);
vec3 n = normalize(v_normal);
f_color.rgb = vec3(0.0);
f_color.rgb += light(n, normalize(vec3(1.0)), vec3(1.0));
f_color.rgb += light(n, normalize(vec3(-1.0, -1.0, 0.0)), vec3(0.2, 0.23, 0.35));
f_color.a = 1.0;
}
Shader里输出各自数据给每个View,使用ViewID索引出相应的数据,渲染结果被写到2D array texture的两个slice中,在VR设备中会显示到两个屏幕上。
OVR_multiview除了用来给2只眼睛发送各自的view外,另一个可能的用途是给每只眼睛送出2笔不同分辨率的View,人眼成像通常是中心清晰而周围模糊些。因此,对于中心出给高分辨率,四周给低分辨率,这样可减少像素的渲染。
不过,OVR_multiview还是有不少的限制:不支持transform feedback(DX的Stream output)、tessellation control or evaluation shaders以及geometry shader
2. Multi Viewport
通过viewport_relative声明,NV_viewport_array2还支持把vertex emit到不同的layer上
layout (viewport_relative) out highp int gl_Layer;
因此,GPU内部的逻辑过程大概是这样子的:
//In a vertex shader or evaluation shader
for(int i=0; i<Viewport_Num; i++
{
if((viewport_mask>>i)&1)
{
gl_layer+=(viewport_relative) ? i : 0;
}
Emit vertex;
}
}
这个扩展被Nvidia Maxwell用于cubemap rendering的加速。
2.2 NV_viewport_swizzle
在一些multi viewport的应用中(比如通过单个pass把primitive渲染到cube的6个面上),有时顶点position可能需要简单的修改,比如通过翻转,XY坐标的互换来简单调整每个面上物体的朝向。NV_viewport_swizzle支持per-viewport swizzle,应用可以通过函数设置XYZW的swizzle方式
void ViewportSwizzleNV(uint index,
enum swizzlex, enum swizzley,
enum swizzlez, enum swizzlew)
其中swizzlex,swizzley,swizzlez,swizzlew的取值可以是
VIEWPORT_SWIZZLE_POSITIVE_X_NV
VIEWPORT_SWIZZLE_NEGATIVE_X_NV
VIEWPORT_SWIZZLE_POSITIVE_Y_NV
VIEWPORT_SWIZZLE_NEGATIVE_Y_NV
VIEWPORT_SWIZZLE_POSITIVE_Z_NV
VIEWPORT_SWIZZLE_NEGATIVE_Z_NV
VIEWPORT_SWIZZLE_POSITIVE_W_NV
VIEWPORT_SWIZZLE_NEGATIVE_W_NV
以swizzlex为例,对于这8中swizzle方式,GPU将对position坐标做不同的调整:
if (swizzlex == VIEWPORT_SWIZZLE_POSITIVE_X_NV) x' = x;
if (swizzlex == VIEWPORT_SWIZZLE_NEGATIVE_X_NV) x' = -x;
if (swizzlex == VIEWPORT_SWIZZLE_POSITIVE_Y_NV) x' = y;
if (swizzlex == VIEWPORT_SWIZZLE_NEGATIVE_Y_NV) x' = -y;
if (swizzlex == VIEWPORT_SWIZZLE_POSITIVE_Z_NV) x' = z;
if (swizzlex == VIEWPORT_SWIZZLE_NEGATIVE_Z_NV) x' = -z;
if (swizzlex == VIEWPORT_SWIZZLE_POSITIVE_W_NV) x' = w;
if (swizzlex == VIEWPORT_SWIZZLE_NEGATIVE_W_NV) x' = -w;
swizzle针对的坐标是clipping space坐标,即在转化成NDC坐标之前执行swizzle。设置设置适当的swizzle,可以借助GPU直接渲染到cube map的6个面上。
一个例子:
layout(triangles) in;
layout(passthrough) in Inputs {
vec2 texcoord;
vec3 normal;
vec4 baseColor;
}
layout(passthrough) in gl_PerVertex {
vec4 gl_Position;
} gl_in[];
layout(viewport_relative) out int gl_Layer;
void main()
{
// Figure out which faces the primitive projects onto and
// generate a corresponding viewport mask.
uint mask = 0;
for (int i = 0; i < 6; i++) {
if (!shouldCull(face)){//If primitive don't project onto this face
mask |= 1U << i;
}
}
gl_ViewportMask = mask;
gl_Layer = 0;
}
3. w scaling与分辨率
NV_clip_space_w_scaling
通常渲染结束后,整张图像还需要做一次后处理——Barrel distortion(桶形失真),以抵消VR设备固有的pincushion distortion(枕形失真)。而经过Barrel distortion的图像的特点边沿的分辨率较低,这意味着原始图像靠近边沿的许多像素对最终的校正图像并无贡献[3]。
NV_clip_space_w_scaling为VR应用提供w-scaling的功能,通过AP为viewport指定两个参数xcoeff和ycoeff:
void ViewportPositionWScaleNV(uint index, float xcoeff, float ycoeff);
GPU负责在Perspective division和Viewport Transform之前对w做以下scaling:
w' = xcoeff * x + ycoeff * y + w;
对于4个象限设置不同符号的xcoeff/ycoeff(需要把所有Geometry重复画到4个同样的viewport上,但每次只scissor出一个象限),使得距离中心越远则w越大,分辨率越低。
GPU只负责做w-scaling使得分辨率呈线性变化,至于un-scaling和后处理(barrel distortion)还是需要AP完成。下面的fragment shader可以包含到后处理的Shader中,负责做un-scale,然后让后处理继续做barrel distortion。
// Vertex Shader
// Draw a triangle that covers the whole screen
const vec4 positions[3] = vec4[3](vec4(-1, -1, 0, 1),
vec4( 3, -1, 0, 1),
vec4(-1, 3, 0, 1));
out vec2 uv;
void main()
{
vec4 pos = positions[ gl_VertexID ];
gl_Position = pos;
uv = pos.xy;
}
// Fragment Shader
uniform sampler2D tex;
uniform float xcoeff;
uniform float ycoeff;
out vec4 Color;
in vec2 uv;
void main()
{
// Handle uv as if upper right quadrant
vec2 uvabs = abs(uv);
// unscale: transform w-scaled image into an unscaled image
// scale: transform unscaled image int a w-scaled image
float unscale = 1.0 / (1 + xcoeff * uvabs.x + xcoeff * uvabs.y);
//float scale = 1.0 / (1 - xcoeff * uvabs.x - xcoeff * uvabs.y);
vec2 P = vec2(unscale * uvabs.x, unscale * uvabs.y);
// Go back to the right quadrant
P *= sign(uv);
Color = texture(tex, P * 0.5 + 0.5);
}
Reference
[1] Optimizing Virtual Reality: Understanding Multiview
[2] Using multiview rendering - Mali Developer Center
[3] Optimizing VR renderers with OVR_multiview - Imagination Technologies
文章被以下专栏收录
推荐阅读
前端如何适配手机屏幕之viewport
响应式网页开发基础:DPR 与 viewport
响应式设计与开发中,有两个重要的概念,一直会贯穿整个流程,但是又有很多人搞不清楚。它们就是 DPR 与 viewport,下面让我们看看这两个概念。什么是 DPR?我们知道在 Chrome 浏览器控制台…
响应式网页开发基础:DPR 与 viewport
响应式设计与开发中,有两个重要的概念,一直会贯穿整个流程,但又有很多人搞不清楚。它们就是 DPR 与 viewport,下面让我们看看这两个概念。什么是 DPR?我们知道在 Chrome 浏览器控制台 c…
前端每周清单第 31 期: iOS 11 Viewport 解析,Preact PWA 性能优化案例,JS 内存泄露分析
Multi-View与Multi-Viewport
随着VR的流行,内置地支持multi-view和multi-viewport已经成为GPU的一种需要。当前GPU主要通过支持已有的扩展来内置实现multi-view和multi-viewport,本篇对常见的几个扩展做下总结。
1. Stereo rendering与Multi-view
Stereo Rendering(立体渲染)是一种让人眼能感受到立体效果的渲染方式,为给予人眼立体效果,需要2个Camera对同一个场景进行成像,即设置不同的View矩阵渲染两张图像,这两张略有差异的图片能使人眼感觉出深度。在普通的pipeline下,立体渲染的一种方案是在Vertex Shader中实现按不同View渲染两次[1]:
Set MVP_L for left camera
Rendering Geometry
Set MVP_R for right camera
Rendering Geometry
可以看到,同样的Geometry被重复渲染了两遍,CPU-GPU的带宽加倍,而这两份Geometry大部分Shading计算(如Model Transform,Attribute计算等)是相同的,仅仅只有View矩阵存在差异。这些数据或计算上的冗余显然可以在GPU内部优化,以提高Performance。
另一种方案是起Geometry Shader,在GS里边给每个View送出各自的position或attribute:
for(int view_id =0; view_id < 2; view_id++)
{
MVP = MVP_Matrix[view_id];
gl_Position = MVP * in_position;
}
这样能避免CPU-GPU带宽问题,同时可以避免相同属性的重复运算,但GS的performance本身并不理想。在GS单个Shader中loop过多的顶点属性,本身会使用较多资源,进而限制Shading的并行度。
OVR_multiview扩展旨在让GPU支持更高效的Multiview rendering(最常见是2个View),启用该扩展时,GPU将在Vertex Shader(VS)中loop,使用ViewID得到per-view的属性,如position和依赖于view的normal(OVR_multiview只允许position依赖于view_ID,不过新的扩展OVR_multiview2放宽了限制,允许其他属性根据view_ID取值)。
新的函数FramebufferTextureMultiviewOVR与FramebufferTextureMultiview类似,但它会使支持该扩展的GPULoop多次。
FramebufferTextureMultiviewOVR( enum target, enum attachment, uint texture, int level, int baseViewIndex, sizei numViews );
一个简单的例子[2],在VS中,我们用modelViewProjection[gl_ViewID_OVR]获取MVP矩阵,而同样的几何体会被渲染两遍,第一次gl_ViewID_OVR=0,第二次gl_ViewID_OVR=1。
#version 300 es
#extension GL_OVR_multiview : enable
layout(num_views = 2) in;
in vec3 vertexPosition;
in vec3 vertexNormal;
uniform mat4 modelViewProjection[2];
uniform mat4 model;
out vec3 v_normal;
void main()
{
gl_Position = modelViewProjection[gl_ViewID_OVR] * vec4(vertexPosition, 1.0);
v_normal = (model * vec4(vertexNormal, 0.0f)).xyz;
}
#version 300 es
precision mediump float;
in vec3 v_normal;
out vec4 f_color;
vec3 light(vec3 n, vec3 l, vec3 c)
{
float ndotl = max(dot(n, l), 0.0);
return ndotl * c;
}
void main()
{
vec3 albedo = vec3(0.95, 0.84, 0.62);
vec3 n = normalize(v_normal);
f_color.rgb = vec3(0.0);
f_color.rgb += light(n, normalize(vec3(1.0)), vec3(1.0));
f_color.rgb += light(n, normalize(vec3(-1.0, -1.0, 0.0)), vec3(0.2, 0.23, 0.35));
f_color.a = 1.0;
}
Shader里输出各自数据给每个View,使用ViewID索引出相应的数据,渲染结果被写到2D array texture的两个slice中,在VR设备中会显示到两个屏幕上。
OVR_multiview除了用来给2只眼睛发送各自的view外,另一个可能的用途是给每只眼睛送出2笔不同分辨率的View,人眼成像通常是中心清晰而周围模糊些。因此,对于中心出给高分辨率,四周给低分辨率,这样可减少像素的渲染。
不过,OVR_multiview还是有不少的限制:不支持transform feedback(DX的Stream output)、tessellation control or evaluation shaders以及geometry shader
2. Multi Viewport
Multi-viewport,即在一个Shader中把primitive同时渲染到多个Viewport上去时。Geometry Shader是最常规的选择,因为只有GS才能看到整个per-primitive的build-in属性gl_ViewportIndex。 NV_viewport_array2扩展便是为了支持比GS Loop更高效的Multi-viewport, 应用可在Vertex Shader/Control Shader/Evaluatoin Shader/Geometry Shader设置build-in属性gl_ViewportMask指定需要发送的viewport,而GPU原则上可用可编程或Fixed Function的方式,在Raster之前Loop每个有效的viewport,并把Raster结果送出。通过viewport_relative声明,NV_viewport_array2还支持把vertex emit到不同的layer上
layout (viewport_relative) out highp int gl_Layer;
因此,GPU内部的逻辑过程大概是这样子的:
//In a vertex shader or evaluation shader
for(int i=0; i<Viewport_Num; i++
{
if((viewport_mask>>i)&1)
{
gl_layer+=(viewport_relative) ? i : 0;
}
Emit vertex;
}
}
这个扩展被Nvidia Maxwell用于cubemap rendering的加速。
2.2 NV_viewport_swizzle
在一些multi viewport的应用中(比如通过单个pass把primitive渲染到cube的6个面上),有时顶点position可能需要简单的修改,比如通过翻转,XY坐标的互换来简单调整每个面上物体的朝向。NV_viewport_swizzle支持per-viewport swizzle,应用可以通过函数设置XYZW的swizzle方式
void ViewportSwizzleNV(uint index,
enum swizzlex, enum swizzley,
enum swizzlez, enum swizzlew)
其中swizzlex,swizzley,swizzlez,swizzlew的取值可以是
VIEWPORT_SWIZZLE_POSITIVE_X_NV
VIEWPORT_SWIZZLE_NEGATIVE_X_NV
VIEWPORT_SWIZZLE_POSITIVE_Y_NV
VIEWPORT_SWIZZLE_NEGATIVE_Y_NV
VIEWPORT_SWIZZLE_POSITIVE_Z_NV
VIEWPORT_SWIZZLE_NEGATIVE_Z_NV
VIEWPORT_SWIZZLE_POSITIVE_W_NV
VIEWPORT_SWIZZLE_NEGATIVE_W_NV
以swizzlex为例,对于这8中swizzle方式,GPU将对position坐标做不同的调整:
if (swizzlex == VIEWPORT_SWIZZLE_POSITIVE_X_NV) x' = x;
if (swizzlex == VIEWPORT_SWIZZLE_NEGATIVE_X_NV) x' = -x;
if (swizzlex == VIEWPORT_SWIZZLE_POSITIVE_Y_NV) x' = y;
if (swizzlex == VIEWPORT_SWIZZLE_NEGATIVE_Y_NV) x' = -y;
if (swizzlex == VIEWPORT_SWIZZLE_POSITIVE_Z_NV) x' = z;
if (swizzlex == VIEWPORT_SWIZZLE_NEGATIVE_Z_NV) x' = -z;
if (swizzlex == VIEWPORT_SWIZZLE_POSITIVE_W_NV) x' = w;
if (swizzlex == VIEWPORT_SWIZZLE_NEGATIVE_W_NV) x' = -w;
swizzle针对的坐标是clipping space坐标,即在转化成NDC坐标之前执行swizzle。设置设置适当的swizzle,可以借助GPU直接渲染到cube map的6个面上。
一个例子:
layout(triangles) in;
layout(passthrough) in Inputs {
vec2 texcoord;
vec3 normal;
vec4 baseColor;
}
layout(passthrough) in gl_PerVertex {
vec4 gl_Position;
} gl_in[];
layout(viewport_relative) out int gl_Layer;
void main()
{
// Figure out which faces the primitive projects onto and
// generate a corresponding viewport mask.
uint mask = 0;
for (int i = 0; i < 6; i++) {
if (!shouldCull(face)){//If primitive don't project onto this face
mask |= 1U << i;
}
}
gl_ViewportMask = mask;
gl_Layer = 0;
}
3. w scaling与分辨率
NV_clip_space_w_scaling
通常渲染结束后,整张图像还需要做一次后处理——Barrel distortion(桶形失真),以抵消VR设备固有的pincushion distortion(枕形失真)。而经过Barrel distortion的图像的特点边沿的分辨率较低,这意味着原始图像靠近边沿的许多像素对最终的校正图像并无贡献[3]。
NV_clip_space_w_scaling为VR应用提供w-scaling的功能,通过AP为viewport指定两个参数xcoeff和ycoeff:
void ViewportPositionWScaleNV(uint index, float xcoeff, float ycoeff);
GPU负责在Perspective division和Viewport Transform之前对w做以下scaling:
w' = xcoeff * x + ycoeff * y + w;
对于4个象限设置不同符号的xcoeff/ycoeff(需要把所有Geometry重复画到4个同样的viewport上,但每次只scissor出一个象限),使得距离中心越远则w越大,分辨率越低。
GPU只负责做w-scaling使得分辨率呈线性变化,至于un-scaling和后处理(barrel distortion)还是需要AP完成。下面的fragment shader可以包含到后处理的Shader中,负责做un-scale,然后让后处理继续做barrel distortion。
// Vertex Shader
// Draw a triangle that covers the whole screen
const vec4 positions[3] = vec4[3](vec4(-1, -1, 0, 1),
vec4( 3, -1, 0, 1),
vec4(-1, 3, 0, 1));
out vec2 uv;
void main()
{
vec4 pos = positions[ gl_VertexID ];
gl_Position = pos;
uv = pos.xy;
}
// Fragment Shader
uniform sampler2D tex;
uniform float xcoeff;
uniform float ycoeff;
out vec4 Color;
in vec2 uv;
void main()
{
// Handle uv as if upper right quadrant
vec2 uvabs = abs(uv);
// unscale: transform w-scaled image into an unscaled image
// scale: transform unscaled image int a w-scaled image
float unscale = 1.0 / (1 + xcoeff * uvabs.x + xcoeff * uvabs.y);
//float scale = 1.0 / (1 - xcoeff * uvabs.x - xcoeff * uvabs.y);
vec2 P = vec2(unscale * uvabs.x, unscale * uvabs.y);
// Go back to the right quadrant
P *= sign(uv);
Color = texture(tex, P * 0.5 + 0.5);
}
Reference
[1] Optimizing Virtual Reality: Understanding Multiview
[2] Using multiview rendering - Mali Developer Center
[3] Optimizing VR renderers with OVR_multiview - Imagination Technologies
文章被以下专栏收录
推荐阅读
前端如何适配手机屏幕之viewport
响应式网页开发基础:DPR 与 viewport
响应式设计与开发中,有两个重要的概念,一直会贯穿整个流程,但是又有很多人搞不清楚。它们就是 DPR 与 viewport,下面让我们看看这两个概念。什么是 DPR?我们知道在 Chrome 浏览器控制台…
响应式网页开发基础:DPR 与 viewport
响应式设计与开发中,有两个重要的概念,一直会贯穿整个流程,但又有很多人搞不清楚。它们就是 DPR 与 viewport,下面让我们看看这两个概念。什么是 DPR?我们知道在 Chrome 浏览器控制台 c…
前端每周清单第 31 期: iOS 11 Viewport 解析,Preact PWA 性能优化案例,JS 内存泄露分析
不过目前除了fun house似乎还没有什么实际应用...
多重同步投影的应用很宽泛,最直接的便是能解决三联屏这样的环绕透视的扭曲,其实哪怕16:9单屏都存在边缘扭曲的现象,不过这个技术的实现依赖于游戏开发者的附加,除了ansel以外其他诸多新技术都不是驱动层可调用的.
1 条评论
不过目前除了fun house似乎还没有什么实际应用...
多重同步投影的应用很宽泛,最直接的便是能解决三联屏这样的环绕透视的扭曲,其实哪怕16:9单屏都存在边缘扭曲的现象,不过这个技术的实现依赖于游戏开发者的附加,除了ansel以外其他诸多新技术都不是驱动层可调用的.