当使用WebGPU或BabylonNative时,如何模拟越界视口

214 阅读5分钟

当使用WebGPU或BabylonNative时,如何模拟越界视口

大家好,我是梦兽。一个WEB全栈开发和Rust爱好者。如果你对Rust非常感兴趣,可以关注梦兽编程公众号获取群,进入和梦兽一起交流。

梦兽已经有差不多半年没有从事Web3D的开发了,梦兽也期待有新的计算平台兴起最好是与3D方面有关系的。

在我的过去我学习了很多有关3D图形的知识,今天要和大家讲讲的是视口这个概念。视口早在20世纪90年代就包含在第一个OpenGL1.0规范中!

视口到底是什么?

Babylon.js术语中,视口是屏幕上相机将绘制到的矩形区域。它具有值 x 、 y 、 width 和 height ,以及 x 和 y 和 height 标准化为屏幕宽度和高度。默认视口覆盖整个屏幕,因此它的 x 和 y 都设置为 0 ,并且它的 width 和 height 都设置为 1 。像这样 ...

默认视口以紫色表示,覆盖整个屏幕。

默认视口以紫色表示,覆盖整个屏幕。

大多数我们使用默认的viewport就足够了,你甚至不必要考虑它,因为它可以让你绘制到整个屏幕,如果你只想绘制到屏幕中的一个特定部位怎么办呢?这就是不起眼的viewport可以帮组你的地方。如果我们讲width和height都设置为0.5,我们就会得到。

如果你想让你的viewport从右下角移动到右上角,你可以将摄像机的viewport的x和y设置为0.5。

这一切看着都切到好处,如果我们的viewport定义的方式使其超出屏幕范围边缘怎么办呢?如果我们将视口的 x 和 y 设置为 0.5 但更改其 width 和 height 从 0.5 到 0.6 。

进一步发挥这种效果,我们可以把viewport的x和y值设置为负数,并把width和height设置大于屏幕,这样我们可以得到一种类似放大的效果。

请注意,此处只有相机的视口值发生变化,因此我们可以有效地放大场景,而无需在3D空间中移动相机或更改其视野,同时仍以屏幕的全像素分辨率渲染场景!

是不是感觉很简单?对吧?如果这么简单就不会让你刷到这篇文章了。

问题

某些图形设备不允许您将viewport的值设置为越界。这在 OpenGL 视口规范中没有明确定义,因此有些图形设备支持它,有些则不支持。对于那些不支持它的设备,视口的值被限制到屏幕尺寸,viewport值不能以使viewport离开屏幕的方式定义。由于规范中存在这种模糊性,由于规格中的这种不确定性,WebGPU和在BabylonNative中使用的bgfx图形库都会对视口值进行限制,以确保在所有可用硬件上保持一致性。这就意味着,我在前面展示的越界视口缩放效果,在针对WebGPU和/或BabylonNative时将无法工作。

我们在用BabylonNative替换应用程序中的旧3D引擎时就遇到了这个问题。改变应用程序的架构会很困难,所以决定尝试用另一种方法来解决它。我们使用了一个Babylon.js的材质插件。materialPlugins

通过这个API,我们可以修改场景中每个材质的顶点着色器,像视口那样移动和缩放几何体。(如果你对材质插件文档不熟悉,可以在这里查看它们。文档很好地解释了如何使用这些插件。)

这里有一个示例环境(playground),展示了当UI中的“Viewport material plugin”(视口材质插件)启用选项被勾选时,顶点着色器材质插件的实际运行情况。如果你滚动到示例代码的底部,你会看到这段着色器代码,它会被插入到场景中使用的每一个材质里...

完整代码地址 playground.babylonjs.com/#EXCRS4#8

gl_Position.x = gl_Position.x * viewport_w * viewport_h / viewport_w + (viewport_x + viewport_w - 1.0 + viewport_x) * gl_Position.w;
gl_Position.y = gl_Position.y * viewport_h + (viewport_y + viewport_h - 1.0 + viewport_y) * gl_Position.w;

这就是负责模拟视口的代码,它允许视口的值可以超出屏幕边界任意远。而且,因为它并没有使用标准的视口图形API,所以在WebGL、WebGPU和BabylonNative上都能以相同的方式工作!代码本身相当容易理解。它仅仅是按照你通常传递给视口API的数值来缩放和移动给定的gl_Position。唯一有点神秘的部分是在每行最后乘以gl_Position.w。这个w是什么?回答这个问题可能看起来很复杂,但实际上它所做的只是根据顶点在视锥体中的深度调整最终的x和y坐标。在正常的渲染管线中,GPU会通过执行gl_Position.xyz / gl_Position.w来进行透视除法。因为我们想要应用一个能够“存活”过这个除法的平移(因为这个平移必须在屏幕空间中发生),所以我们预先将平移量乘以gl_Position.w。

另一段值得注意的代码是使用剪裁矩形(scissor rectangle)来阻止任何绘制操作发生在定义的视区之外。这并不是我们迄今为止所见到的最明显的例子,但是如果你查看使用天空盒子的这个Playground示例,其中剪裁代码被注释掉,你可以看到背景被绘制到了定义的视区之外。当改变摄像机的视区值而不使用材质插件时,GPU会自动剪裁掉视区之外的内容,但是当我们启用了材质插件时,就需要手动进行这项操作。这就是为什么我们在每次渲染时都要在调用材质插件之前开启剪裁,并在之后关闭它。尝试在第113行取消注释剪裁代码,看看这对天空盒子产生了什么影响。

干杯! 谢谢你的阅读和继续关注更多的惊喜!

本文使用 markdown.com.cn 排版