(18)微信3D小游戏下HUD绘制的经验分享

平视显示器(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游戏、客户端游戏不一样的设计理念与特点,我们会在后续的文章里继续分享微信小游戏背后的小故事。





历史文章回顾 :小程序•小故事


如果大家有想了解的小程序相关能力的故事,欢迎在评论区留言,我们后续会考虑将这些能力背后的故事分期分享给大家。


最后一次编辑于  08-17  (未经腾讯允许,不得转载)
收藏 0评论 16
  • 念

    先收藏,后放弃

    赞同 31没有帮助
    0评论
    复制
    08-07
  • (●ω● )(●ω● )

    做游戏啊,感觉就像在做数学题一样,我差点微积分都用了,最后因为微积分忘得差不多了,才没用。如果用了游戏引擎可能会好些吧。

    赞同 6没有帮助
    0评论
    复制
    08-07
  • 刘荣康刘荣康

    我现在连2d都还没搞明白

    赞同 6没有帮助
    1评论
    复制
    08-07
    评论
  • 煮饭烫伤手煮饭烫伤手

    你说的这个办法,是我熬了一个礼拜的夜班,经过无数次失败。最后咬牙跺脚采用的无赖之举。你所谓的解决办法我不能接受

    赞同 3没有帮助
    0评论
    复制
    08-26
  • 子龙子龙

    这个解决方案太不优雅了,没想过要解决吗?

    赞同 3没有帮助
    0评论
    复制
    08-13
  • 木木景彡木木景彡

    给个原生的源码看看吧,写的也太费劲了,不精通webgl和canva都没法用这个最主要的功能

    赞同 1没有帮助
    0评论
    复制
    09-11
  • 清峰清峰

    其实在3D场景中显示平面图形很简单,只要在顶点着色器上不使用任何变换矩阵,直接指定顶点的xy坐标(-1,+1),同时禁用深度缓存就行了。

    赞同 1没有帮助
    5评论
    复制
    08-17
    • 张晨  🔆张晨 🔆

      HUD之间也会有层级关系,就像html的zIndex一样

      赞同 1没有帮助回复复制08-19
    • 清峰清峰

      HUD之间的层级关系应该在2D绘制部分完成,然后将所有的最终结果一次性绘制到3D空间,只要保证绘制的物体在裁剪空间范围内,就可以忽略所有的变换过程,直接渲染。

      赞同 0没有帮助回复复制08-20
    • 清峰清峰

          //创建2DCanvas和2D上下文

          //创建2DCanvas和2D上下文

          var canvas_2d = wx.createCanvas('canvas_2d');

          var ctx = canvas_2d.getContext('2d');

          //加载巴比伦3D引擎(使用默认的Canvas)

          var engine = self.engine = new BABYLON.Engine(canvas, true);


      ...

          //一个特殊的顶点着色,没有任务矩阵变换

          BABYLON.Effect.ShadersStore["hudVertexShader"] = `

          #ifdef GL_ES

          precision highp float;

          #endif

          // Attributes

          attribute vec3 position;

          attribute vec2 uv;

          // Normal

          varying vec2 vUV;

          void main(void) {

          gl_Position = vec4(position, 1.0);

          vUV = uv;

          }        

          `;

          //最普通片元着色器,只有一个2D采样

          BABYLON.Effect.ShadersStore["hudFragmentShader"] = `

          #ifdef GL_ES

          precision highp float;

          #endif

          varying vec2 vUV;

          // Refs

          uniform sampler2D textureSampler;

          void main(void) {

          gl_FragColor = texture2D(textureSampler, vUV);

          }   

          `;

          var hudMaterial = new BABYLON.ShaderMaterial("hud", scene, {

            vertexElement: "hud",

            fragmentElement: "hud"

          },

            {

              needAlphaBlending: true,

              attributes: ["position", "uv"],

              samplers: ["textureSampler"]

            });

          var planeVertexData = BABYLON.VertexData.CreatePlane({ size: 2 });//从-1,-1 到 1,1的简单平面覆盖整个3D空间

          delete planeVertexData.normals;

          var hud = new BABYLON.Mesh("Hud", scene);//HUD网格对象

          planeVertexData.applyToMesh(hud);


          //贴图大小和2D屏幕大小一致

          var hudTexture = new BABYLON.DynamicTexture("dynamic texture", { width: canvas_2d.width, height: canvas_2d.height }, scene, true);

          var textureContext = hudTexture.getContext();//贴图的绘制上下文

          hud.material = hudMaterial;

          hudMaterial.setTexture("textureSampler", hudTexture);

          hudMaterial.backFaceCulling = false; //关闭背面裁剪

          hudMaterial.disableDepthWrite = true;//关闭写深度缓存

          hudTexture.update();//贴图更新



          // 实现游戏帧循环

          this.loop = () => {

            //在2DCanvas上绘制两个矩形

            ctx.clearRect(0, 0, canvas_2d.width, canvas_2d.height);

            ctx.fillStyle = 'blue';

            ctx.fillRect(0, 0, 100, 100);

            ctx.fillStyle = 'red';

            ctx.fillRect(canvas_2d.width - 100, canvas_2d.height - 100, 100, 100);

            //将2DCanvas绘制贴图的上下文

            textureContext.drawImage(canvas_2d, 0, 0);

            //贴图更新

            hudTexture.update();

            //保证每一帧HUD都在

            hud.position = new BABYLON.Vector3(camera._currentTarget.x, camera._currentTarget.y, camera._currentTarget.z);

            scene.render();

          ...

          }



      赞同 1没有帮助回复复制08-20
    • 展示更多
    评论
  • flyfly

    请问有源程序吗,,模板啥的

    赞同 0没有帮助
    0评论
    复制
    昨天 21:05
  • zanxaszanxas

    卧槽,微信还能开发3d游戏?还写shader?

    赞同 0没有帮助
    0评论
    复制
    09-14
  • 平凡之路平凡之路

    菜鸟路过

    赞同 0没有帮助
    0评论
    复制
    09-11