Orillusion

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

    WebGL 与 WebGPU比对[4] - Uniform

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

      众所周知,在 GPU 跑可编程管线的时候,着色器是并行运行的,每个着色器入口函数都会在 GPU 中并行执行。每个着色器对一大片统一格式的数据进行冲锋,体现 GPU 多核心的优势,可以小核同时处理数据;不过,有的数据对每个着色器都是一样的,这种数据的类型是“uniform”,也叫做统一值。

      这篇文章罗列了原生 WebGL 1/2 中的 uniform 资料,以及 WebGPU 中的 uniform 资料,有一些例子供参考,以用来比对它们之间的差异。

      1. WebGL 1.0 Uniform

      1.1. 用 WebGLUniformLocation 寻址

      在 WebGL 1.0 中,通常是在 JavaScript 端保存 WebGLUniformLocation 以向着色器程序传递 uniform 值的。

      使用 gl.getUniformLocation() 方法获取这个 location,有如下几种方式

      • 全名:gl.getUniformLocation(program, 'u_someUniformVar')
      • 分量:通常是向量的一部分,譬如 gl.getUniformLocation(program, 'u_someVec3[0]') 是获取第 0 个元素(元素类型是 vec3)的 location
      • 结构体成员:gl.getUniformLocation(program, 'u_someStruct.someMember')

      上面三种情况与之对应的着色器代码:

      // 全名
      uniform float u_someUniformVar;
      
      // 分量
      uniform vec3 u_someVec3[3]; // 注意,这里是 3 个 vec3
      
      // 结构体成员
      struct SomeStruct {
        bool someMember;
      };
      uniform SomeStruct u_someStruct;  
      

      传值分三类,标量/向量、矩阵、采样纹理,见下文。

      1.2. 矩阵赋值用 uniformMatrix[234]fv

      对于矩阵,使用 gl.uniformMatrix[234]fv() 方法即可传递,其中,f 代表 float,v 代表 vector,即传入参数要是一个向量(即数组);

      以传递一个 4×4 的矩阵为例:

      // 获取 location(初始化时)
      const matrixLocation = gl.getUniformLocation(program, "u_matrix")
      
      // 创建或更新列主序变换矩阵(渲染时)
      const matrix = [/* ... */]
      
      // 传递值(渲染时)
      gl.uniformMatrix4fv(matrixLocation, false, matrix)
      

      1.3. 标量与向量用 uniform[1234][fi][v]

      对于普通标量和向量,使用 gl.uniform[1234][fi][v]() 方法即可传递,其中,1、2、3、4 代表标量或向量的维度(1就是标量啦),f/i 代表 float 或 int,v 代表 vector(即你传递的数据在着色器中将解析为向量数组)。

      举例:

      • 语句1,gl.uniform1fv(someFloatLocation, [4.5, 7.1])
      • 语句2,gl.uniform4i(someIVec4Location, 5, 2, 1, 3)
      • 语句3,gl.uniform4iv(someIVec4Location, [5, 2, 1, 3, 2, 12, 0, 6])
      • 语句4,gl.uniform3f (someVec3Location, 7.1, -0.8, 2.1)

      上述 4 个赋值语句对应的着色器中的代码为:

      // 语句 1 可以适配 1~N 个浮点数
      // 只传单元素数组时,可直接声明 uniform float u_someFloat;
      uniform float u_someFloat[2];
      
      // 语句 2 适配一个 ivec4
      uniform ivec4 u_someIVec4;
      
      // 语句 3 适配  1~N 个 ivec4
      // 只传单元素数组时,可直接声明 uniform float u_someIVec4;
      uniform ivec4 u_someIVec4[2];
      
      // 语句 4 适配一个 vec3
      uniform vec3 u_someVec3;
      

      到了 WebGL 2.0,在组分值类型会有一些扩充,请读者自行查阅相关文档。

      1.4. 传递纹理

      在顶点着色器阶段,可以使用顶点的纹理坐标对纹理进行采样:

      attribute vec3 a_pos;
      attribute vec2 a_uv;
      uniform sampler2D u_texture;
      varying vec4 v_color;
      
      void main() {
        v_color = texture2D(u_texture, a_uv);
        gl_Position = a_pos; // 假设顶点不需要变换
      }
      

      那么,在 JavaScript 端,可以使用 gl.uniform1i() 来告诉着色器我把纹理刚刚传递到哪个纹理坑位上了:

      const texture = gl.createTexture()
      const samplerLocation = gl.getUniformLocation(/* ... */)
      
      // ... 设置纹理数据 ...
      
      gl.activeTexture(gl[`TEXTURE${5}`]) // 告诉 WebGL 使用第 5 个坑上的纹理
      gl.bindTexture(gl.TEXTURE_2D, texture)
      
      gl.uniform1i(samplerLocation, 5) // 告诉着色器待会读纹理的时候去第 5 个坑位读
      

      2. WebGL 2.0 Uniform

      2.1. 标量/向量/矩阵传值方法扩充

      WebGL 2.0 的 Uniform 系统对非方阵类型的矩阵提供了支持,例如

      const mat2x3 = [
        1, 2, 3,
        4, 5, 6,
      ]
      gl.uniformMatrix2x3fv(loc, false, mat2x3)
      

      上述方法传递的是 4×3 的矩阵。

      而对于单值和向量,额外提供了无符号数值的方法,即由 uniform[1234][fi][v] 变成了 uniform[1234][f/ui][v],也就是下面 8 个新增方法:

      gl.uniform1ui(/* ... */) // 传递数据至 1 个 uint
      gl.uniform2ui(/* ... */) // 传递数据至 1 个 uvec2
      gl.uniform3ui(/* ... */) // 传递数据至 1 个 uvec3
      gl.uniform4ui(/* ... */) // 传递数据至 1 个 uvec4
      
      gl.uniform1uiv(/* ... */) // 传递数据至 uint 数组
      gl.uniform2uiv(/* ... */) // 传递数据至 uvec2 数组
      gl.uniform3uiv(/* ... */) // 传递数据至 uvec3 数组
      gl.uniform4uiv(/* ... */) // 传递数据至 uvec4 数组
      

      对应 GLSL300 中的 uniform 为:

      #version 300 es
      #define N ? // N 取决于你的需要,JavaScript 传递的数量也要匹配
        
      uniform uint u_someUint;
      uniform uvec2 u_someUVec2;
      uniform uvec3 u_someUVec3;
      uniform uvec4 u_someUVec4;
      
      uniform uint u_someUintArr[N];
      uniform uvec2 u_someUVec2Arr[N];
      uniform uvec3 u_someUVec3Arr[N];
      uniform uvec4 u_someUVec4Arr[N];
      

      需要额外注意的是,uint/uvec234 这些类型在高版本的 glsl 才能使用,也就是说不向下兼容 WebGL 1.0 及 GLSL100.

      然而,WebGL 2.0 带来的不单单只是这些小修小补,最重要的莫过于 UBO 了,马上开始。

      2.1. 什么是 UniformBlock 与 UniformBuffer 的创建

      在 WebGL 1.0 的时候,任意种类的统一值一次只能设定一个,如果一帧内 uniform 有较多更新,对于 WebGL 这个状态机来说不是什么好事,会带来额外的 CPU 至 GPU 端的传递开销。

      在 WebGL 2.0,允许一次发送一堆 uniform,这一堆 uniform 的聚合体,就叫做 UniformBuffer,具体到代码中:

      先是 GLSL 300

      uniform Light {
        highp vec3 lightWorldPos;
        mediump vec4 lightColor;
      };
      

      然后是 JavaScript

      const lightUniformBlockBuffer = gl.createBuffer()
      const lightUniformBlockData = new Float32Array([
        0, 10, 30, 0,    // vec3, 光源位置, 为了 8 Byte 对齐填充一个尾 0
        1, 1, 1, 1,     // vec4, 光的颜色
      ])
      gl.bindBuffer(gl.UNIFORM_BUFFER, lightUniformBlockBuffer);
      gl.bufferData(gl.UNIFORM_BUFFER, lightUniformBlockData, gl.STATIC_DRAW);
      
      gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, lightUniformBlockBuffer)
      

      先别急着问为什么,一步一步来。

      首先你看到了,在 GLSL300 中允许使用类似结构体一样的块状语法声明多个 Uniform 变量,这里用到了光源的坐标和光源的颜色,分别使用了不同的精度和数据类型(vec3、vec4)。

      随后,在 JavaScript 端,你看到了用新增的方法 gl.bindBufferBase() 来绑定一个 WebGLBuffer 到 0 号位置,这个 lightUniformBlockBuffer 其实就是集合了两个 Uniform 变量的 UniformBufferObject (UBO),在着色器中那块被命名为 Light 的花括号区域,则叫 UniformBlock.

      其实,创建一个 UBO 和创建普通的 VBO 是一样的,绑定、赋值操作也几乎一致(第一个参数有不同)。只不过 UBO 可能更需要考虑数值上的设计,例如 8 字节对齐等,通常会在设计着色器的时候把相同数据类型的 uniform 变量放在一起,达到内存使用上的最佳化。

      2.2. 状态绑定

      在 WebGL 2.0 中,JavaScript 端允许你把着色器程序中的 UniformBlock 位置绑定到某个变量中:

      const viewUniformBufferIndex = 0;
      const materialUniformBufferIndex = 1;
      const modelUniformBufferIndex = 2;
      const lightUniformBufferIndex = 3;
      gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'View'), viewUniformBufferIndex);
      gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'Model'), modelUniformBufferIndex);
      gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'Material'), materialUniformBufferIndex);
      gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'Light'), lightUniformBufferIndex);
      

      这里,使用的是 gl.getUniformBlockIndex() 获取 UniformBlock 在着色器程序中的位置,而把这个位置绑定到你喜欢的数字上的是 gl.uniformBlockBinding() 方法。

      这样做有个好处,你可以在你的程序里人为地规定各个 UniformBlock 的顺序,然后用这些 index 来更新不同的 UBO.

      // 使用不同的 UBO 更新 materialUniformBufferIndex (=1) 指向的 UniformBlock
      gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, redMaterialUBO)
      gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, greenMaterialUBO)
      gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, blueMaterialUBO)
      

      当然,WebGL 2.0 对 Uniform 还有别的扩充,此处不再列举。

      bindBufferBase 的作用类似于 enableVertexAttribArray,告诉 WebGL 我马上就要用哪个坑了。

      2.3. 着色器中的 Uniform

      着色器使用 GLSL300 语法才能使用 UniformBlock 和 新的数据类型,除此之外和 GLSL100 没啥区别。当然,GLSL300 有很多新语法,这里只捡一些关于 Uniform 的来写。

      关于 uint/uvec234 类型,在 2.1 节已经有例子了,这里不赘述。

      而关于 UniformBlock,还有一点需要补充的,那就是“命名”问题。

      UniformBlock 的语法如下:

      uniform <BlockType> {
        <BlockBody>
      } ?<blockName>;
      
      // 举例:具名定义
      uniform Model {
        mat4 world;
        mat4 worldInverseTranspose;
      } model;
      
      // 举例:不具名定义
      uniform Light {
        highp vec3 lightWorldPos;
        mediump vec4 lightColor;
      };
      

      如果使用具名定义,那么访问 Block 内的成员就需要使用它的 name 了,例如 model.world、model.worldInverseTranspose 等。

      举完整的例子如下:

      #version 300 es
      precision highp float;
      precision highp int;
      
      // uniform 块的布局控制
      layout(std140, column_major) uniform;
      
      // 声明 uniform 块:Transform,命名为 transform 供主程序使用
      // 也可以不命名,就直接用 mvpMatrix 即可
      uniform Transform
      {
        mat4 mvpMatrix;
      } transform;
      
      layout(location = 0) in vec2 pos;
      
      void main() {
        gl_Position = transform.mvpMatrix * vec4(pos, 0.0, 1.0);
      }
      

      注意,即使给 UniformBlock 命名为 transform,但是立面的 mvpMatrix 是不能与其它 Block 里面的成员共名的,transform 没有命名空间的作用。

      再看 JavaScript:

      //#region 获取着色器程序中的 uniform 位置并绑定
      const uniformTransformLocation = gl.getUniformBlockIndex(program, 'Transform')
      gl.uniformBlockBinding(program, uniformTransformLocation, 0)
      //endregion
      
      //#region 创建 ubo
      const uniformTransformBuffer = gl.createBuffer()
      //#endregion
      
      //#region 创建矩阵所需的 ArrayBufferView,列主序
      const transformsMatrix = new Float32Array([
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 1.0
      ])
      //#endregion
      
      //#region 传递数据给 WebGLBuffer
      gl.bindBuffer(gl.UNIFORM_BUFFER, uniformTransformBuffer)
      gl.bufferData(gl.UNIFORM_BUFFER, transformsMatrix, gl.DYNAMIC_DRAW);
      gl.bindBuffer(gl.UNIFORM_BUFFER, null)
      //#endregion
      
      // ---------- 在你需要绘制时 ----------
      //#region 绑定 ubo 到 0 号索引上的 uniformLocation 以供着色器使用
      gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uniformTransformBuffer)
      // ... 渲染
      // -------------
      

      2.4. 传递纹理

      纹理与 WebGL 1.0 一致,但是 GLSL300 的纹理函数有变,读者请自行查找资料比对。

      3. WebGPU Uniform

      WebGPU 有三个类型的 Uniform 资源:标量/向量/矩阵、纹理、采样器。

      各自有各自的容器,第一种统一使用 GPUBuffer,也就是所谓的 UBO;第二和第三种使用 GPUTexture 和 GPUSampler.

      3.1. 三类资源的创建与打组传递

      上述三类资源,把它们通过打成一组,也就是 GPUBindGroup,我叫它资源绑定组,进而传递给组织了着色器模块(GPUShaderModule)的各种管线(GPURenderPipeline、GPUComputePipeline)。

      统一起来好办事,这里为节约篇幅,数据传递就不再细说,着重看看它们的打组成绑定组的代码:

      const someUbo = device.createBuffer({ /* 注意 usage 要有 UNIFORM */ })
      const texture = device.createTexture({ /* 创建常规纹理 */ })
      const sampler = device.createSampler({ /* 创建常规采样器 */ })
      
      // 布局对象联系管线布局和绑定组本身
      const bindGroupLayout = device.createBindGroupLayout({
        entries: [
          {
            binding: 0, // <- 绑定在 0 号资源
            visibility: GPUShaderStage.FRAGMENT,
            sampler: {
              type: 'filtering'
            }
          },
          {
            binding: 1, // <- 绑定在 1 号资源
            visibility: GPUShaderStage.FRAGMENT,
            texture: {
              sampleType: 'float'
            }
          },
          {
            binding: 2,
            visibility: GPUShaderStage.FRAGMENT,
            buffer: {
              type: 'uniform'
            }
          }
        ]
      })
      const bindGroup = device.createBindGroup({
        layout: bindGroupLayout,
        entries: [
          {
            binding: 0,
            resource: sampler, // <- 传入采样器对象
          },
          {
            binding: 1,
            resource: texture.createView() // <- 传入纹理对象的视图
          },
          {
            binding: 2,
            resource: {
              buffer: someUbo // <- 传入 UBO
            }
          }
        ]
      })
      
      // 管线
      const pipelineLayout = device.createPipelineLayout({
        bindGroupLayouts: [bindGroupLayout]
      })
      const renderingPipeline = device.createRenderPipeline({
        layout: pipelineLayout
        // ... 其它配置
      })
      
      // ... renderPass 切换 pipeline 和 bindGroup 进行绘制 ...
      

      3.2. 更新 Uniform 与绑定组的意义

      更新 Uniform 资源其实很简单。

      如果是 UBO,一般会更新前端修改的灯光、材质、时间帧参数以及单帧变化的矩阵等,使用 device.queue.writeBuffer 即可:

      device.queue.writeBuffer(
        someUbo, // 传给谁
        0, 
        buffer, // 传递 ArrayBuffer,即当前帧中的新数据
        byteOffset, // 从哪里开始
        byteLength // 取多长
      )
      

      使用 writeBuffer 就可以保证用的还是原来创建那个 GPUBuffer,它与绑定组、管线的绑定关系还在;不用映射、解映射的方式传值是减少 CPU/GPU 双端通信成本

      如果是纹理,那就用 图像拷贝操作 中的几个方法进行纹理对象更新;

      一般不直接对采样器和纹理的更新,而是在编码器上切换不同的绑定组来切换管线所需的资源。尤其是纹理,若频繁更新数据,CPU/GPU 双端通信成本会增加的。

      延迟渲染、离屏绘制等需要更新颜色附件的,其实只需要创建新的 colorAttachments 对象即可实现“上一帧绘制的下一帧我能用”,不需要直接从 CPU 内存再刷入数据到 GPU 中。

      更新 Uniform 需要对每一帧几乎都要改的、几乎不变的资源进行合理分组,分到不同的绑定组中,这样就可以有针对性地更新,而无需把管线、绑定组重设一次,仅仅在通道编码器上进行切换即可。

      3.3. 着色器中的 Uniform

      此处不涉及太多 WGSL 语法。

      与 UniformBlock 类似,需要指定“一块东西”,WGSL 直接使用的结构体。

      首先,是 UBO:

      // -- 顶点着色器 --
      
      // 声明一个结构体类型
      struct Uniforms {
        modelViewProjectionMatrix: mat4x4<f32>;
      };
      
      // 声明指定其绑定ID是0,绑定组序号是0
      @binding(2)
      @group(0)
      var<uniform> myUniforms: Uniforms;
      
      // —— 然后这个 myUniforms 变量就可以在函数中调用了 ——
      

      然后是纹理和采样器:

      @group(0)
      @binding(1)
      var mySampler: sampler;
      
      @group(0)
      @binding(2)
      var myTexture: texture_2d<f32>;
      
      // ... 片元着色器主函数中进行纹理采样
      textureSample(myTexture, mySampler, fragUV);
      

      4. 对比总结

      WebGL 以 2 为比对基准,它与 WebGPU 相比,没有资源绑定组,没有采样器对象(采样参数通过另外的方法设置)。

      比起 WebGPU 的传 descriptor 式的写法,使用一条条方法切换 UniformBlock、纹理等资源可能会有所遗漏,这是全局状态写法的特点之一。当然,上层封装库会帮我们屏蔽这些问题的。

      与语法风格相比,其实 WebGPU 改进的更多的是这些 uniform 在每一帧更新时 CPU 到GPU 的负载问题,它是事先由编码器编码成指令缓冲最后一次性发送的,比起 WebGL 一条一条发送是更优的,在图形渲染、GPU运算这种地方,积少成多,性能就高了起来。

      关于 WebGL 2.0 的 Uniform 和 GLSL300 我学识不精,若有错误请指出。

      5. 参考资料

      • WebGL2Fundamentals - StateDiagram - UniformBuffers

      • Gist - A simple WebGL2 UniformBuffer Tutorial

      • CSDN - WebGL2 UniformBlock

      • Austin - WebGPUSamples

      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