Orillusion

    • 注册
    • 登录
    • 搜索
    • 版块
    • 最新
    • 标签

    WebGPU 中消失的 FBO 和 RBO

    中文社区
    fbo framebuffer renderbuffer rbo
    1
    1
    98
    正在加载更多帖子
    • 从旧到新
    • 从新到旧
    • 最多赞同
    回复
    • 在新帖中回复
    登录后回复
    此主题已被删除。只有拥有主题管理权限的用户可以查看。
    • 寻风觅迹
      寻风觅迹 最后由 shuangliu 编辑

      OpenGL 体系给图形开发留下了不少的技术积累,其中就有不少的“Buffer”,耳熟能详的就有顶点缓冲对象(VertexbufferObject,VBO),帧缓冲对象(FramebufferObject,FBO)等。

      切换到以三大现代图形开发技术体系为基础的 WebGPU 之后,这些经典的缓冲对象就在 API 中“消失了”。其实,它们的职能被更科学地分散到新的 API 去了。

      本篇讲一讲 FBO 与 RBO,这两个通常用于离屏渲染逻辑中,以及到了 WebGPU 后为什么没有这两个 API 了(用什么作为了替代)。

      1 WebGL 中的 FBO 与 RBO

      WebGL 其实更多的角色是一个绘图 API,所以在 gl.drawArrays 函数发出时,必须确定将数据资源画到哪里去。

      WebGL 允许 drawArrays 到两个地方中的任意一个:canvas 或 FramebufferObject. 很多资料都有介绍,canvas 有一个默认的帧缓冲,若不显式指定自己创建的帧缓冲对象(或者指定为 null)那就默认绘制到 canvas 的帧缓冲上。

      换句话说,只要使用 gl.bindFramebuffer() 函数指定一个自己创建的帧缓冲对象,那么就不会绘制到 canvas 上。

      本篇讨论的是 HTMLCanvasElement,不涉及 OffscreenCanvas

      1.1 帧缓冲对象(FramebufferObject)

      FBO 创建起来简单,它大多数时候就是一个负责点名的头儿,出汗水的都是小弟,也即它下辖的两类附件:

      • 颜色附件(在 WebGL1 中有 1 个,在 WebGL 2 可以有16个)
      • 深度模板附件(可以只用深度,也可以只用模板,也可以两个一起使用)

      关于 MRT 技术(MultiRenderTarget),也就是允许输出到多个颜色附件的技术,WebGL 1.0 使用 gl.getExtension('WEBGL_draw_buffers') 获取扩展来使用;而 WebGL 2.0 原生就支持,所以颜色附件的数量上有所区别。

      而这两大类附件则通过如下 API 进行设置:

      // 设置 texture 为 0 号颜色附件
      gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, color0Texture, 0)
      // 设置 rbo 为 0 号颜色附件
      gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, color0Rbo)
      
      // 设置 texture 为 仅深度附件
      gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0)
      // 设置 rbo 为 深度模板附件(需要 WebGL2 或 WEBGL_depth_texture)
      gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, depthStencilRbo)
      

      实际上,在需要进行 MRT 时,gl.COLOR_ATTACHMENT0、gl.COLOR_ATTACHMENT1 ... 这些属性只是一个数字,可以通过计算属性进行颜色附件的位置索引,也可以直接使用明确的数字代替:

      console.log(gl.COLOR_ATTACHMENT0) // 36064
      console.log(gl.COLOR_ATTACHMENT1) // 36065
      
      let i = 1
      console.log(gl[`COLOR_ATTACHMENT${i}`]) // 36065
      

      1.2 颜色附件与深度模板附件的真正载体

      颜色附件与深度模板附件是需要明确指定数据载体的。WebGL 若改将绘图结果绘制到非 canvas 的 FBO,那么就需要明确指定具体画在哪。

      如 1.1 小节的示例代码所示,每个附件都可以选择如下二者之一作为真正的数据载体容器:

      • 渲染缓冲对象(WebGLRenderbuffer)
      • 纹理对象(WebGLTexture)

      有前辈在博客中指出,渲染缓冲对象会比纹理对象稍好,但是要具体问题具体分析。

      实际上,在大多数现代 GPU 以及显卡驱动程序上,这些性能差异没那么重要。

      简单的说,如果离屏绘制的结果不需要再进行下一个绘制中作为纹理贴图使用,用 RBO 就可以,因为只有纹理对象能向着色器传递。

      关于 RBO 和纹理作为两类附件的区别的资料就没那么多了,而且这篇主要是比对 WebGL 和 WebGPU 二者的不同,就不再展开了。

      1.3 FBO/RBO/WebGLTexture 相关方法收集

      • gl.framebufferTexture2D(gl.FRAMEBUFFER, <attachment_type>, <texture_type>, <texture>, <mip_level>):将 WebGLTexture 关联到 FBO 的某个附件上
      • gl.framebufferRenderbuffer(gl.FRAMEBUFFER, <attachment_type>, gl.RENDERBUFFER, <rbo>):将 RBO 关联到 FBO 的某个附件上
      • gl.bindFramebuffer(gl.FRAMEBUFFER, <fbo | null>):设置帧缓冲对象为当前渲染目标
      • gl.bindRenderbuffer(gl.RENDERBUFFER, <rbo>):绑定 <rbo> 为当前的 RBO
      • gl.renderbufferStorage(gl.RENDERBUFFER, <rbo_format>, width, height):设置当前绑定的 RBO 的数据格式以及长宽

      下面是三个创建的方法:

      • gl.createFramebuffer()
      • gl.createRenderbuffer()
      • gl.createTexture()

      顺带回顾一下纹理的参数设置、纹理绑定与数据传递函数:

      • gl.texParameteri():设置当前绑定的纹理对象的参数
      • gl.bindTexture():绑定纹理对象为当前作用纹理
      • gl.texImage2D():向当前绑定的纹理对象传递数据,最后一个参数即数据

      2 WebGPU 中的对等概念

      WebGPU 已经没有 WebGLFramebuffer 和 WebGLRenderbuffer 这种类似的 API 了,也就是说,你找不到 WebGPUFramebuffer 和 WebGPURenderbuffer 这俩类。

      但是,gl.drawArray 的对等操作还是有的,那就是渲染通道编码器(令其为 renderPassEncoder)发出的 renderPassEncoder.draw 动作。

      2.1 渲染通道编码器(GPURenderPassEncoder)承担 FBO 的职能

      WebGPU 的绘制目标在哪呢?由于 WebGPU 与 canvas 元素不是强关联的,所以必须显式指定绘制到哪里去。

      通过学习可编程通道以及指令编码等概念,了解到 WebGPU 是通过一些指令缓冲来向 GPU 传递“我将要干啥”的信息的,而指令缓冲(Command Buffer)则由指令编码器(也即 GPUCommandEncoder)完成创建。指令缓冲由若干个 Pass(通道)构成,绘制相关的通道,叫做渲染通道。

      渲染通道则是由渲染通道编码器来设置的,一个渲染通道就设定了这个通道的绘制结果要置于何处(这个描述就类比了 WebGL 要绘制到哪儿)。具体到代码中,其实就是创建 renderPassEncoder 时,传递的 GPURenderPassDescriptor 参数对象里的 colorAttachments 属性:

      const renderPassEncoder = commandEncoder.beginRenderPass({
        // 是一个数组,可以设置多个颜色附件
        colorAttachments: [
          {
            view: textureView,
            loadValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
            storeOp: 'store',
          }
        ]
      })
      

      注意到,colorAttachments[0].view 是一个 textureView,也即 GPUTextureView,换言之,意味着这个渲染通道要绘制到某个纹理对象上。

      通常情况下,如果你不需要离屏绘制或者使用 msaa,那么应该是画到 canvas 上的,从 canvas 中获取其配置好的纹理对象如下操作:

      const context = canvas.getContext('webgpu')
      context.configure({
        gpuDevice,
        format: presentationFormat, // 此参数可以使用画布的客户端长宽 × 设备像素缩放比例得到,是一个两个元素的数组
        size: presentationSize, // 此参数可以调用 context.getPreferredFormat(gpuAdapter) 获取
      })
      
      const textureView = context.getCurrentTexture().createView()
      

      上述代码片段完成了渲染通道与屏幕 canvas 的关联,即把 canvas 视作一块 GPUTexture,使用其 GPUTextureView 与渲染通道的关联。

      其实,更严谨的说法是 渲染通道 承担了 FBO 的部分职能(因为渲染通道还有发出其它动作的职能,例如设置管线等),因为没有 GPURenderPass 这个 API,所以只能委屈 GPURenderPassEncoder 代替一下了。

      2.2 多目标渲染

      为了进行多目标渲染,也即片元着色器要输出多个结果的情况(代码中表现为返回一个结构体),就意味着要多个颜色附件来承载渲染的输出。

      此时,要配置渲染管线的片元着色阶段(fragment)的 targets 属性。

      相关的从创建纹理、创建管线、指令编码例子代码如下所示,用到两个纹理对象来充当颜色附件的容器:

      // 一、创建渲染目标纹理 1 和 2,以及其对应的纹理视图对象
      const renderTargetTexture1 = device.createTexture({
        size: [/* 略 */],
        usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
        format: 'rgba32float',
      })
      const renderTargetTexture2 = device.createTexture({
        size: [/* 略 */],
        usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
        format: 'bgra8unorm',
      })
      const renderTargetTextureView1 = renderTargetTexture1.createView()
      const renderTargetTextureView2 = renderTargetTexture2.createView()
      
      // 二,创建管线,配置片元着色阶段的多个对应目标的纹素输出格式
      const pipeline = device.createRenderPipeline({
        fragment: {
          targets: [
            {
              format: 'rgba32float'
            },
            {
              format: 'bgra8unorm'
            }
          ]
          // ... 其它属性省略
        },
        // ... 其它阶段省略
      })
      
      const renderPassEncoder = commandEncoder.beginRenderPass({
        colorAttachments: [
          {
            view: renderTargetTextureView1,
            // ... 其它参数
          },
          {
            view: renderTargetTextureView2,
            // ... 其它参数
          }
        ]
      })
      

      这样,两个颜色附件分别用上了两个纹理视图对象作为渲染目标,而且在管线对象的片元着色阶段也明确指定了两个 target 的格式。

      于是,你可以在片元着色器代码中指定输出结构:

      struct FragmentStageOutput {
        @location(0) something: vec4<f32>;
        @location(1) another: vec4<f32>;
      }
      
      @stage(fragment)
      fn main(/* 省略输入 */) -> FragmentStageOutput {
        var output: FragmentStageOutput;
        // 随便写俩数字,没什么意义
        output.something = vec4<f32>(0.156);
        output.another = vec4<f32>(0.67);
        
        return output;
      }
      

      这样,位于 location 0 的 something 这个 f32 型四维向量就写入了 renderTargetTexture1 的一个纹素,而位于 location 1 的 another 这个 f32 型四维向量则写入了 renderTargetTexture2 的一个纹素。

      尽管,在 pipeline 的片元阶段中 target 指定的 format 略有不一样,即 renderTargetTexture2 指定为 'bgra8unorm',而着色器代码中结构体的 1 号 location 数据类型是 vec4<f32>,WebGPU 会帮你把 f32 这个 [0.0f, 1.0f] 范围内的输出映射到 [0, 255] 这个 8bit 整数区间上的。

      事实上,如果没有多输出(也即多目标渲染),WebGPU 中大部分片元着色器的返回类型就是一个单一的 vec4<f32>,而最常见的 canvas 最佳纹理格式是 bgra8unorm,总归要发生 [0.0f, 1.0f] 通过放大 255 倍再取整到 [0, 255] 这个映射过程的。

      2.3 深度附件与模板附件

      GPURenderPassDescriptor 还支持传入 depthStencilAttachment,作为深度模板附件,代码举例如下:

      const renderPassDescriptor = {
        // 颜色附件设置略
        depthStencilAttachment: {
          view: depthTexture.createView(),
          depthLoadValue: 1.0,
          depthStoreOp: 'store',
          stencilLoadValue: 0,
          stencilStoreOp: 'store',
        }
      }
      

      与单个颜色附件类似,也需要一个纹理对象的视图对象为 view,需要特别注意的是,作为深度或模板附件,一定要设置与深度、模板有关的纹理格式。

      若对深度、模板的纹理格式在额外的设备功能(Device feature)中,在请求设备对象时一定要加上对应的 feature 来请求,例如有 "depth24unorm-stencil8" 这个功能才能用 "depth24unorm-stencil8" 这种纹理格式。

      深度模板的计算,还需要注意渲染管线中深度模板阶段参数对象的配置,例如:

      const renderPipeline = device.createRenderPipeline({
        // ...
        depthStencil: {
          depthWriteEnabled: true,
          depthCompare: 'less',
          format: 'depth24plus',
        }
      })
      

      2.4 非 canvas 的纹理对象作为两种附件的注意点

      除了深度模板附件里提及的纹理格式、请求设备的 feature 之外,还需要注意非 canvas 的纹理若作为某种附件,那它的 usage 一定包含 RENDER_ATTACHMENT 这一项。

      const depthTexture = device.createTexture({
        size: presentationSize,
        format: 'depth24plus',
        usage: GPUTextureUsage.RENDER_ATTACHMENT,
      })
      
      const renderColorTexture = device.createTexture({
        size: presentationSize,
        format: presentationFormat,
        usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
      })
      

      3 读取数据

      3.1 从 FBO 中读像素值

      从 FBO 读像素值,实际上就是读颜色附件的颜色数据到 TypedArray 中,想读取当前 fbo(或 canvas 的帧缓冲)的结果,只需调用 gl.readPixels 方法即可。

      //#region 创建 fbo 并将其设为渲染目标容器
      const fb = gl.createFramebuffer();
      gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
      //#endregion
      
      //#region 创建离屏绘制的容器:纹理对象,并绑定它成为当前要处理的纹理对象
      const texture = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, texture);
      
      // -- 若不需要作为纹理再次被着色器采样,其实这里可以用 RBO 代替
      //#endregion
      
      //#region 绑定纹理对象到 0 号颜色附件
      gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
      //#endregion
      
      // ... gl.drawArrays 进行渲染
      
      //#region 读取到 TypedArray
      const pixels = new Uint8Array(imageWidth * imageHeight * 4);
      gl.readPixels(0, 0, imageWiebdth, imageHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
      //#endregion
      

      gl.readPixels() 方法是把当前绑定的 FBO 及当前绑定的颜色附件的像素值读取到 TypedArray 中,无论载体是 WebGLRenderbuffer 还是 WebGLTexture.

      唯一需要注意的是,如果你在写引擎,那么读像素的操作得在绘制指令(一般指 gl.drawArrays 或 gl.drawElements)发出后的代码中编写,否则可能会读不到值。

      3.2 WebGPU 读 GPUTexture 中的数据

      在 WebGPU 中将渲染目标,也即纹理中访问像素是比较简单的,使用到指令编码器的 copyTextureToBuffer 方法,将纹理对象的数据读取到 GPUBuffer,然后通过解映射、读范围的方式获取 ArrayBuffer.

      //#region 创建颜色附件关联的纹理对象
      const colorAttachment0Texture = device.createTexture({ /* ... */ })
      //#endregion
      
      //#region 创建用于保存纹理数据的缓冲对象
      const readPixelsResultBuffer = device.createBuffer({
        usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
        size: 4 * textureWidth * textureHeight,
      })
      //#endregion
      
      //#region 图像拷贝操作,将 GPUTexture 拷贝到 GPUBuffer
      const encoder = device.createCommandEncoder()
      encoder.copyTextureToBuffer(
        { texture: colorAttachment0Texture },
        { buffer: readPixelsResultBuffer },
        [textureWidth, textureHeight],
      )
      device.queue.submit([encoder.finish()])
      //#endregion
      
      //#region 读像素
      await readPixelsResultBuffer.mapAsync()
      const pixels = new Uint8Array(readPixelsResultBuffer.getMappedRange())
      //#endregion
      

      要额外注意,如果要拷贝到 GPUBuffer 并且要交给 CPU 端(也就是 JavaScript)来读取,那这块 GPUBuffer 的 usage 一定要有 COPY_DST 和 MAP_READ 这两项;而且,这个纹理对象的 usage 也必须要有 COPY_SRC 这一项(作为颜色附件的关联纹理,它还得有 RENDER_ATTACHMENT 这一个 usage)。

      4 总结

      从 WebGL(也即 OpenGL ES 体系)到 WebGPU,离屏绘制技术、多目标渲染技术都有了接口和用法上的升级。

      首先是取消了 RBO 这个概念,一律使用 Texture 作为绘制目标。

      其次,更替了 FBO 的职权至 RenderPass,由 GPURenderPassEncoder 负责承载原来 FBO 的两类附件。

      因为取消了 RBO 概念,所以 RTT(RenderToTexture) 和 RTR(RenderToRenderbuffer) 就不再存在了,但是离屏绘制技术依旧是存在的,你在 WebGPU 中可以使用多个 RenderPass 完成多个绘制成果,Texture 作为绘制载体可以自由地经过资源绑定组穿梭在不同的 RenderPass 的某个 RenderPipeline 中。

      关于如何从 GPU 的纹理中读取像素(颜色值),第 3 节也有粗浅的讨论,这部分大多数用途是 GPU Picking;而关于 FBO 这个遗留概念,现在即 RenderPass 离屏渲染,最常见的还是做效果。

      1 条回复 最后回复 回复 引用 0
      • First post
        Last post

      Recent Post

      • 请问有没有本次测试所使用的threejs、babylon和orillusion的源码?

        • 阅读更多
      • B

        哪位大佬知道,纹理闪烁是怎么回事吗?.

        • 阅读更多
      • @StephenChips 同求

        • 阅读更多
      • A

        请问这个入门系列有 demo 代码没有

        • 阅读更多
      • O

        经过几次的学习已经能够构建出一个空间(场景),并在空间中创建物体(几何体),物体可以有不同的外观(材质),与现实的效果足够逼真(光照),终于把最重要的相关性最强的几部分3D功能用起来了。不过面对这块空间想做点什么,又感觉缺少了点什么,是的,只能观看不能操作,如果我要通过键盘、鼠标对场景进行实时的干预该如何做呢,经过了解输入系统可以满足我们的要求。

        输入系统

        输入系统是个比较杂乱的部分,不同平台都有对应的封装,我们可以回忆一下Win32编程将键盘和鼠标的输入集成到了事件系统,用户操作按键或操作鼠标会触发对应的消息码,指示消息,附带参数包含具体的按键信息或鼠标信息,按键信息一般包含按键码或鼠标键位。再回忆一下DOM的事件系统,使用addEventListener将click或mouse类的事件挂载,然后在回调函数中获得结果……
        回忆结束我们可以总结出来几个输入系统的特点:1、挂载感兴趣的事件;2、回调函数得到触发时处理业务逻辑。需要注意的是,键盘需要有按键表进行区分按键,对应的是鼠标需要区分不同按键,以及屏幕坐标,辅助键等一些附属信息。
        出于好奇orilluson的输入系统如何实现的,找来源码进行了一个大体的了解,可以看到输入系统的核心类是InputSystem,该类继承于CEventDispatcher类,CEventDispatcher类是可调度事件的所有类的基类,包含了事件的注册,注销,分发和清理等功能实现。内部保存了监听对象列表,当有消息需要处理时通过遍历监听器列表触发回调函数。InputSystem继承了CEventDispatcher类的事件处理能力外着重实现了键盘鼠标的事件处理。
        具体执行步骤如下:

        Engine3D.init:初始化引擎后,实例化了InputSystem类,并将canvas实例传入InputSystem类; InputSystem.initCanvas:InputSystem监听了画布的键盘与鼠标事件; addEventListener:引擎或对象通过addEventListener函数来挂载用户监听; dispatchEvent:当有挂载的监听事件响应时,回调函数会得执行。
        在输入系统的支持下,可以很轻松的使用键盘和鼠标与触控。
        输入系统的回调事件在类CEvent中,先熟悉一下这个类的常用定义: type:事件类型对应的一个字符串常量; param:注册事件时传递的参数,在注册事件时写入的参数在这里可以读出; ctrlKey:事件发生时 Ctrl 是否被按下,通过查询该键的值来判断Ctrl键的状态; altKey:事件发生时 Alt 是否被按下,通过查询该键的值来判断Alt键的状态; shiftKey:事件发生时 Shift 是否被按下,通过查询该键的值来判断Shift键的状态; 关于坐标

        一直以来的学习路径是以实用为主,但是现在必须要接触一点点不能称之为理论的理论了,那就是坐标系统。

        世界坐标

        首先要解决一个困惑的地方,过去在3D空间中的所有坐标都可以称为世界坐标,世界坐标是三维的,有三个维度(x,y,z),一般在引擎中创建可以由系统使用,开发用户程序需要遵守引擎对于世界的规划,相当于场景作为一个空间,世界坐标是对这个空间制定的规则。这里歪个楼,骇客帝国之所以叫矩阵,是不是因为在3D引擎中对空间世界的处理也是以矩阵为基础的。再拉回来,世界坐标一般以(0,0,0)为中心,我们创建的物体默认的位置也是在这里的,这里是世界的中心,一般分为右手或左手坐标系,好了关于世界坐标系这里已经够用了。

        屏幕坐标

        说回到屏幕坐标是我们过去所熟悉的,首先屏幕坐标是一个二维坐标,以像素为单位,屏幕的左下角为起点,向屏幕的左侧和上方依次是x和y坐标的正向。在网页开发中我们通过DOM事件系统获得的当前坐标一般都是指的屏幕坐标。在网页开发中并不是绝对的没有z轴,CSS中的z-index属性是否可以理解成一种z轴坐标呢。

        相互转换

        屏幕坐标是我们最终渲染到屏的最终展现形式,世界坐标是在三维空间内的标识,两者经常需要相互转换,例如今天需要讨论的输入系统的使用。假设在屏幕上点击了一个位置,需要转换到世界坐标,相似的在世界坐标内的位置或距离也需要转换为屏幕坐标。
        坐标转换有标准的算法,这里我们不必如此费力,完全可以借助引擎的工具,经过一翻查找,在相机组件的实现类Camera3D,有坐标转换的工具,可以一起熟悉一下

        object3DToScreenRay:世界坐标转换屏幕坐标; ScreenRayToObject3D:屏幕坐标转换为世界坐标; 键盘输入

        使用键盘输入,首先需要熟悉两种键盘事件:

        KEY_DOWN:键盘按下事件,使用输入系统挂载该事件,将会得到按下键盘事件通知; KEY_UP:键盘弹起事件,使用输入系统挂载该事件,将会得到弹起键盘事件通知;
        下面来一起梳理一下使用流程: 初始化:必要的引擎初始化; 输入挂载:使用键盘挂载系统指定事件和回调; 处理回调:在回调中获取参数。 基础示例

        这里写了一个最基本的示例,只将键盘的事件打印了出来。

        import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, ForwardRenderJob, DirectLight, KeyEvent } from "@orillusion/core"; export default class Keyboard { cameraObj: Object3D; camera: Camera3D; scene: Scene3D; boxObj: Object3D; async run() { await this.init(); await this.setup(); await this.start(); } /*** * 配置并初始化引擎 */ private async init() { // 初始化引擎 await Engine3D.init(); // 创建一个场景 this.scene = new Scene3D(); // 创建一个相机 this.cameraObj = new Object3D(); this.camera = this.cameraObj.addComponent(Camera3D); // 设置相机类型 this.camera.perspective(60, window.innerWidth / window.innerHeight, 1, 5000.0); // 设置相机控制器 let controller = this.cameraObj.addComponent(HoverCameraController); controller.setCamera(20, -20, 25); // 添加相机至场景 this.scene.addChild(this.cameraObj); } /** * 引擎功能代码 */ private async setup() { Engine3D.inputSystem.addEventListener(KeyEvent.KEY_UP, this.keyUp, this); Engine3D.inputSystem.addEventListener(KeyEvent.KEY_DOWN, this.keyDown, this); } /** * 启动渲染 */ private async start() { // 创建前向渲染 let renderJob: ForwardRenderJob = new ForwardRenderJob(this.scene); // 开始渲染 Engine3D.startRender(renderJob); } private keyDown(e: KeyEvent) { console.log('keyDown:', e.keyCode, e); } private keyUp(e: KeyEvent) { console.log('keyUp:', e.keyCode, e); } }

        运行这个示例后,在场景中按下或弹起键盘,在控制台能够看到输出。

        KeyEvent

        在回调函数中获得的参数类型是KeyEvent,KeyEvent是CEvent的子类,除了CEvent类的参数外,对于键盘事件的使用主要在于对该类型的解析,这里需要详细的了解事件的参数细节,常用到的需要进行一个了解:

        keyCode:按键code值,枚举类型可以参考官方文档的KeyCode定义。 鼠标与触控

        电脑端的鼠标操作与移动端的触控操作有许多共同的地方,在具体用法时如果能够合并为一,是可以节省一半的事件挂载操作的,不过需要留意触控与鼠标的事件对应关系。
        有了前面键盘操作的基础,鼠标与触控使用类型,我们先看支持的事件类型:

        POINTER_CLICK:触摸点击事件,对应鼠标的单击事件; POINTER_MOVE:触摸滑动事件,对应鼠标的移动事件 POINTER_DOWN:触摸开始事件, POINTER_UP:触摸结束事件 POINTER_OUT:触摸滑出事件
        既然已经合并了,后面鼠标与触控用触控来说明吧。 基础示例

        先实现一个最基础的触控功能,与键盘类似,先注册事件,然后响应事件。

        import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, ForwardRenderJob, PointerEvent3D } from "@orillusion/core"; export default class Mouse { cameraObj: Object3D; camera: Camera3D; scene: Scene3D; async run() { await this.init(); await this.setup(); await this.start(); } /*** * 配置并初始化引擎 */ private async init() { // 初始化引擎 await Engine3D.init(); // 创建一个场景 this.scene = new Scene3D(); // 创建一个相机 this.cameraObj = new Object3D(); this.camera = this.cameraObj.addComponent(Camera3D); // 设置相机类型 this.camera.perspective(60, window.innerWidth / window.innerHeight, 1, 5000.0); // 设置相机控制器 let controller = this.cameraObj.addComponent(HoverCameraController); controller.setCamera(20, -20, 25); // 添加相机至场景 this.scene.addChild(this.cameraObj); } /** * 引擎功能代码 */ private async setup() { Engine3D.inputSystem.addEventListener(PointerEvent3D.POINTER_UP, this.onUp, this); Engine3D.inputSystem.addEventListener(PointerEvent3D.POINTER_DOWN, this.onDown, this); Engine3D.inputSystem.addEventListener(PointerEvent3D.POINTER_CLICK, this.onPick, this); Engine3D.inputSystem.addEventListener(PointerEvent3D.POINTER_OVER, this.onOver, this); Engine3D.inputSystem.addEventListener(PointerEvent3D.POINTER_OUT, this.onOut, this); Engine3D.inputSystem.addEventListener(PointerEvent3D.POINTER_MOVE, this.onMove, this); } /** * 启动渲染 */ private async start() { // 创建前向渲染 let renderJob: ForwardRenderJob = new ForwardRenderJob(this.scene); // 开始渲染 Engine3D.startRender(renderJob); } private onUp(e: PointerEvent3D) { console.log('onUp:',e); } private onDown(e: PointerEvent3D) { console.log('onDown:',e); } private onPick(e: PointerEvent3D) { console.log('onPick:',e); } private onOver(e: PointerEvent3D) { console.log('onOver:',e); } private onOut(e: PointerEvent3D) { console.log('onOut:',e); } private onMove(e: PointerEvent3D) { console.log('onMove:',e); } } PointerEvent3D

        触控的参数是以PointerEvent3D类型作为回调函数的参数传递到应用,PointerEvent3D是CEvent的子类,除了CEvent类的参数外,需要熟悉一下这个类型的关键字段。

        mouseX:当前鼠标所在位置的X坐标; mouseY:当前鼠标所在位置的Y坐标; movementX:当前事件和上一个鼠标事件之间鼠标在水平方向上的移动值; movementY:当前事件和上一个鼠标事件之间鼠标在垂直方向上的移动值;
        坐标系列的数值请注意,可以使用前面相机组件提供的转换函数进行转换,不必自己写算法进行转换。 由对象挂载

        前面的挂载直接由引擎的输入系统挂载,这样在整个场景中都会响应,如果只需要在一个物体中响应鼠标的事件,我们可以将事件挂在物体上,为什么可以这么做呢,找出来代码可以看到,物体的容器是Object3D类,而Object3D类是Entiry的子类,Entity的父类是CEventDispatcher类,正是因为Object3D通过CEventDispatcher,继承了事件的能力。这一套继承加组件式的结构,实在是太好用了,有没有。
        这样就有了以下的代码:

        // 创建一个对象 this.boxObj = new Object3D(); this.boxObj.localPosition = new Vector3(0,0,0); // 创建渲染组件 let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer); // 设置形状 mr.geometry = new BoxGeometry(5, 5, 5); // 设置材质 mr.material = new HDRLitMaterial(); // 添加到场景 this.scene.addChild(this.boxObj); boxObj.addEventListener(PointerEvent3D.PICK_CLICK, this.onClick, this); onClick(e: PointerEvent3D) { console.log('onPick:',e); }

        运行后可以在控制台看到输出

        小结

        经过前面几次的学习,已经能够完事的构建出一个空间了,但是这块空间仍然缺乏灵动的能力,不能随时响应我们的操控,输入系统是一个随时干预系统的大杀器,可以让我们获得掌控感,是否控制欲获得了满足。
        今天只是一些最基础的用法,发挥想象力可以使这个空间好玩起来了。
        作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

        • 阅读更多

      Copyright © 2022 Orillusion | Contact US