平视显示器(head up display)简称HUD。游戏经常在三维场景上叠加文本或二维图形信息,如弹窗,血量条等,同时需要保证它们在屏幕上的位置和大小不变。
传统的H5游戏可以使用dom,或是在原本的webgl上面盖一个新的2D canvas(画布)做为HUD来实现,同时使用其接口就可以画出HUD所需要的内容。
但微信小游戏只支持一个画布,无法和传统H5游戏的绘制方式一样。因此,要在3D世界中实现HUD就必须在这个唯一的画布上实现。
我们在后台收到了许多反馈:如何用小游戏的框架来实现HUD的绘制。这一期的小故事,我们跟大家分享如何在微信3D小游戏中绘制HUD:
本文的内容包括:
1.微信小游戏只支持一个画布
2.如何使用三维平面模拟HUD?
3.相机变化导致HUD产生位移缩放
4.如何用图形渲染管线解决上述问题?
5.绘制场景时视点变化与投影阶段的问题
6.如何使用顶点着色器解决上述问题?
微信小游戏只支持一个画布
与浏览器不同,微信客户端只有一个画布,并且不能使用html。
普通H5游戏会使用html,或是创建一个新的2D canvas标签,定位在原本的webgl canvas上面,同时使用2D canvas的接口就可以画出HUD的内容。但微信小游戏不支持这样做。所以在三维世界中要实现HUD,需要在一个画布上实现。
所以在三维世界中要实现HUD,则必须在这个唯一的画布上实现。
如何使用三维平面模拟HUD?
对于图像,webgl可以通过纹理贴图来展示图像。开发者可将图片作为的纹理贴图,贴在一个三维矩形平面上,使平面一直正对相机,来模拟HUD。
对于文字,微信小游戏三维的canvas是使用webgl作为context的。但是webgl却无法像2D的context能直接画文字。开发者如果直接用webgl画出文字,需要导入文字模型的顶点数据,但由于文字比较复杂,顶点数量多,相当于渲染了一个复杂的3D物体,这种方式无论是从文件大小还是性能上,都会有损体验。
那么是否可以使用2D canvas 绘好文字,再作为纹理贴在三维平面上呢?
虽然微信小游戏只能渲染一个canvas,但是开发者可以创建多个的canvas实例。
Step1:开发者可创建一个离屏的2D canvas,再使用2D的接口绘制文字、图片等;
Step2:开发者可将这个离屏canvas传给webgl,当成一个texture,贴到一个三维的平面物体上,使其永远都在相机的正前方,通过这样模拟HUD 。
补充
webgl支持直接将canvas作为纹理;
void gl.texImage2D(target, level, internalformat, format, type, HTMLCanvasElement? pixels)。
相机变化导致HUD产生位移缩放
游戏场景中的相机是会改变的,比如说吃鸡游戏中的第一人称和第三人称视角转化。我们发现了一个问题:当相机的可视范围变化的时候,HUD就会发生形变。
那是因为视野看的越广,映射到屏幕上的时候,同一个物体就显得越小。
我们需要保证HUD在任何视角下位置大小都是正确无误的。那么如何才能做到呢?
要解决这个问题就需要明白计算机是如何把三维场景画到二维的屏幕上的。这个画的过程也就是计算机图形渲染管线帮我们完成的。
如何用图形渲染管线解决上述问题?
画一个三维物体到二维平面可以分为三个阶段:
●“准备数据” (应用程序阶段)
●“画点” (几何阶段)
●“画像素” (光栅化阶段)
一个HUD实际上是一个矩形的平面物体,通过矩形的4个顶点就可以描述出来一个平面的位置、大小。为了让平面的位置,大小看起来没问题,我们需要修改“画点”阶段的逻辑。这个阶段又可以进行如下的细分。
与摄影机相关的逻辑,是视点变换还有投影阶段。我们可以通过修改这两者的逻辑来达到我们的目的。
绘制场景时视点变化与投影阶段的问题
1.视点变化阶段的问题
我们需要绘制摄像机看到的世界,而摄像机可以处在任意位置观察这个世界。视点变化本质是就是根据摄像机看的方向来旋转物体,从而让三维空间的物体正确旋转到观察者看到的样子。原本是摆正放的物体,由于观察者的视角问题(歪着看),所以显示出来物体最终也是歪的。
通过在应用程序阶段定义相机的视点、观察目标点以及上方向等数据,我们可以得到一个叫做视图矩阵(View Matrix)的矩阵。把这个矩阵与物体的位置做矩阵乘法就可以得到物体变化后的新位置。
因为游戏世界中,摄像机的位置是不停变化的,而我们的物体却需要一直出现在摄像机正前方。所以游戏场景中的视觉矩阵(View Matrix)在每一帧的渲染中,可能都在变化。这里我们只要将HUD原本一直在变化的视觉矩阵(View Matrix)替换为我们需要的,并且保持不变就好了。
2.投影阶段的问题
投影其实是把透视摄像机原本的可视范围,压缩成一个单位立方体。
再通过屏幕映射,就会出现如下的效果出来。
这一个过程中,会通过摄像机定义的数据(比如长宽比,视场,近截面,远截面),来生成一个叫做投影矩阵(Projection Matrix)的矩阵。将这个矩阵与位置信息进行矩阵乘法,再进行一些归一化操作,就会得到单位立方体内的位置。
和视觉矩阵(View Matrix)一样,对于HUD的物体,我们也不能使用透视摄像机生成的矩阵,否则就会可能导致大小变化。我们替换成正视摄像机的矩阵。这样算出来的位置就是永远都是正常的,不需要担心游戏中更新了相机的数据。
如何使用顶点着色器解决上述问题?
现在我们要用顶点着色器来修改视点变换还有投影的逻辑。
顶点着色器与片元着色器都是 webgl 提供给我们用来操作渲染管线的能力。让我们可以使用glsl 这种编程语言来对 GPU 的能力进行编程。
顶点着色器运行在“画点”阶段(几何阶段),也就是对每个三维物体的顶点进行计算。片元着色器运行在“画像素”阶段(光栅化阶段),把顶点围起来的像素(其实是片元)画上颜色。
我们可以通过顶点着色器,修改视点变换与投影的逻辑,最后达到我们的效果。
总结
由于微信小游戏支持一个单独的画布,开发者想要在任何游戏场景下绘制正常的HUD,可以通过顶点着色器的能力,去修改视点变换与投影的所用到的矩阵,最终来解决这个问题。
微信小游戏还有很多与H5游戏、客户端游戏不一样的设计理念与特点,我们会在后续的文章里继续分享微信小游戏背后的小故事。
历史文章回顾 :小程序•小故事
如果大家有想了解的小程序相关能力的故事,欢迎在评论区留言,我们后续会考虑将这些能力背后的故事分期分享给大家。
先收藏,后放弃
做游戏啊,感觉就像在做数学题一样,我差点微积分都用了,最后因为微积分忘得差不多了,才没用。如果用了游戏引擎可能会好些吧。
我现在连2d都还没搞明白
三号质检员 张全蛋 是不是你,
你说的这个办法,是我熬了一个礼拜的夜班,经过无数次失败。最后咬牙跺脚采用的无赖之举。你所谓的解决办法我不能接受
这个解决方案太不优雅了,没想过要解决吗?
快带我开发小游戏啊
给个原生的源码看看吧,写的也太费劲了,不精通webgl和canva都没法用这个最主要的功能
直接使用webgl有点晦涩难懂, 使用threejs的解决方案, 看这里https://zhuanlan.zhihu.com/p/57831003
辛苦辛苦,原理算是搞清楚了....
但是有案例嘛.... Three.js
微信小游戏里面怎么写glsl的代码??
写得挺好,很有意思,但是篇幅过长,而且最终也没给出答案,还要下回分解?
老朽不才,初学js和webgl,听老朽来简单分析一下
顶点数据一般都要通过在GLSL进行矩阵变换,纯的(x,y,z)是无法显示成3d效果,在不考虑摄像机矩阵的情况下,通常如下
vec4 p1 = transform_m * position; // transform_m是自身的坐标变换矩阵
vec4 fp = perspective_m *p1; // prespective_m是3d透视矩阵
最后得到的fp就是在平目里显示的最终坐标位置。
如果把perspective_m矩阵换成一个2d的投影矩阵,即把webgl的默认坐标系改成我们常用的计算机二维坐标系(webgl默认是坐标系是-1到1,一般我们给的坐标数据都是基于像素的,且左上角是0,0,所以要通过一个矩阵将这些数据改成-1,1之间),那得到的顶点数据就是在一个平面上显示的。
通常perspective_m矩阵是一个uniform变量,即全局变量,一般不改,那也可以设置另外一组vertexAttrib4f数据(或者另外一个uniform变量),在显示3D情况下传入的是一个单位矩阵(1000 , 0100,0010,0001),一旦要绘制平面图形的时候就传入一个perspective的逆矩阵和要显示的2d转换矩阵的点乘结果,假设它叫new_m :
new_m = perspective_inverse_m*projection_m
perspective_inverse_m是透视矩阵的逆矩阵
projection_m是2d投影矩阵
在js代码里:
.....
let identityMatrix = ...... // 单位矩阵
let projectMatrix = ..... // 投影矩阵
let perspectiveInverseMatrix = ... // 透视矩阵的逆矩阵
let vertexData = figure.vertexData;
gl.bufferSubData(bufferPosition, offset,vertexData);
if(figure.is2d){
// 给出上述那个可以让perspective变成2d投影矩阵的那个矩阵:
let new_m = perspectiveInverseMatrix.multiply(projectMatrix);
gl.uniformMatrix4fv(new_m_position,new_m)
}else{
// 不是2d显示,给出一个单位矩阵
gl.uniformMatrix4fv(new_m_position,identityMatrix)
}
....
那在glsl里代码改成:
vec4 p1 = transform_m * position; // transform_m是自身的坐标变换矩阵
mat4 f_m = new_m* perspective_m; // 如果是3d,perspective透视矩阵是不会变的,如果是2d,得到的f_m是一个投影矩阵
vec4 fp = f_m *p1;
因为new_m = perspective_inverse_m*projection_m ;
f_m的最终结果其实就是projection_m (逆矩阵乘以矩阵结果是单位矩阵,矩阵和单位矩阵乘是不变的)
所以最后恕我直言,几个知识点就能说清的事,写了这么一大堆,有凑字数之嫌
webgl的坐标系和我们常用的不一样(如果你代码里图形坐标系就是在-1,1这样的形式,当我没说),webgl仅提供了一组光栅化api,三维效果都是要自己计算
2d显示需要一个投影矩阵。如何得到这个矩阵很简单,把x,y想办法变换到(-1,1)之间,然后把计算的步骤列出来,考虑如何通过一个矩阵就能得到该结果即可。
3d显示需要有一个透视矩阵,透视矩阵如何得到的可以研究一下什么是视锥,比起2d的投影算法要复杂很多很多很多
线性代数计算
什么是GLSL