Orillusion

    • 注册
    • 登录
    • 搜索
    • 版块
    • 最新
    • 标签
    1. 主页
    2. oldguy
    O
    • 资料
    • 关注 0
    • 粉丝 1
    • 主题 7
    • 帖子 7
    • 最佳 4
    • 有争议的 0
    • 群组 0

    oldguy

    @oldguy

    8
    声望
    12
    资料浏览
    7
    帖子
    1
    粉丝
    0
    关注
    注册时间 最后登录

    oldguy 取消关注 关注

    oldguy 发布的最佳帖子

    • orillusion入门系列一 | 第一印象

      做了很多年的应用开发想要在技术上有些突破,是时候学点真正的技术了,我选择了3D技术,因为接触3D技术之前我似乎是一只二维生物,只能在一个平面上思考问题,但是3D技术难度不小,所以必须找个足够高的肩膀来踩,那就要借助引擎的力量了。经过一翻搜索,orillusion 着实让我感动,满足了我所有的要求,而且还是国产的。

      为什么选择 orillusion

      没有专业的对比,仅凭我个人的好恶盲目来选择,简单从这两个方面来考虑

      Native or Web?

      如果您是专业人士建议详细对比两种类型的发展历史和原理再做出判断,对于我一个3D新手来说,有一个简单粗暴的判断思路,那就是在应用系统上,CS架构仍在某些特定领域仍然存在,但是能够迁移到BS的,几乎都在大规模的迁移到了BS架构上,所以我挺Web渲染架构。

      足够新

      新人就要有新人的觉悟,老牌的各路引擎已经有无数高手浸淫其中多年了,冒然投入进去会被沉重的历史信息淹没,作技术还是要有点野心的新手终有一天会成老鸟,所以要选择最新推出的面向最新技术架构的引擎,那么有多新呢,标准还没正式应用的够新吧,那就是以面向WebGPU为第一图形标准实现功能,至于WebGPU是什么,我的理解是更新、更快、更强,至于具体如何做到,相信以后我们会知道的。

      准备工作

      唯一必须的的准备工作是做好心理建设,不要想一口吃成个胖子,不要过早深究原理,我们的目标是借助引擎的能力来实现3D效果,除此以外还有两件小事需要留意。

      开发环境

      请先安装nodejs,并配置好环境变量,同时假设vue3已经可以正常使用。这里我使用vite作为项目创建工具,使用vscode做编辑器,没有其它的前置条件,轻装上阵。如果这一步有什么疑问,请自行查找资料,相信聪明的你不会被难住。

      运行环境

      由于WebGPU标准并未正式推出,需要使用开发浏览器,这里推荐 chrome canary ,下载后在在canary中运行效果,由于canary仍然在变动中,需要在地址栏中输入 chrome://flags/#enable-unsafe-webgpu,然后开启 Unsafe WebGPU 选项。

      c7ae164b-15fd-46fe-b723-41491446e63c-image.png
      如果无法开启或者没有效果,需要搜索最新的开启方法,相信不久的将来在正式版就可以附带WebGPU支持了,不需要这么繁琐了。
      同时为了做两手准备,也可以考虑用 Edge Canary 来应个急,下载后正常安装,启动后在地址栏中输入edge://flags/#enable-unsafe-webgpu,开启WebGPU支持。

      创建项目

      这里我们先创建一个普通的vue3项目,再将引擎引入项目,操练起来吧,很快就能看到我们在做什么了。

      1. 创建根目录
        a. 选择一个文件夹,创建orillusion目录
        b. 进入orillusion,并在该目录下打开命令提示行
      2. 创建项目
        a. 执行命令 npm create vite@latest
        b. Project name 下输入 hello3d 作为项目名并回车
        c. 使用键盘方向键选择 vue 作为框架,并回车
        d. 仍然使用键盘方向键选择 vue-ts 作为项目类型,并回车,已经创建了 hello3d 项目目录结构
        e. 输入 cd hello3d 进入项目目录
        f. 输入 npm install 安装依赖包,至此,基本的vue3项目已经创建完成
      3. 安装 orillusion
        a. 输入 npm install @orillusion/core --save 安装 orillusion 开发包(如果安装失败,请注意分辨网络原因或安装权限)
        b. 输入 npm run dev ,可以看到在5173端口启动了服务
        c. 打开 canary 浏览器,输入完整地址可以看到显示了vue3的欢迎页,至此项目已经安装完成,后面进行开发。

      编写代码

      1. 在 vscode 中打开 hello3d 目录
      2. 在 src 目录下新建目录 demo
      3. 在 demo 目录下创建文件 hello.ts
      import { Engine3D, Scene3D, Object3D, Camera3D, ForwardRenderJob, HDRLitMaterial, BoxGeometry, MeshRenderer, DirectLight, HoverCameraController, Color, Vector3 } from "@orillusion/core";
      
      export default class Hello {
          async run() {
              console.log('hello 3d');
              // 初始化引擎
              await Engine3D.init();
              // 创建一个场景
              let scene3D: Scene3D = new Scene3D();
              // 创建一个相机
              let cameraObj: Object3D = new Object3D();
              let camera = cameraObj.addComponent(Camera3D);
              // 设置相机类型
              camera.perspective(60, window.innerWidth / window.innerHeight, 1, 5000.0);
              // 设置相机控制器
              let controller = cameraObj.addComponent(HoverCameraController);
              controller.setCamera(20, -20, 25);
              // 添加相机至场景
              scene3D.addChild(cameraObj);
              // 创建一个对象
              const obj: Object3D = new Object3D();
              // 创建渲染组件
              let mr: MeshRenderer = obj.addComponent(MeshRenderer);
              // 设置形状
              mr.geometry = new BoxGeometry(5, 5, 5);
              // 设置材质
              mr.material = new HDRLitMaterial();
              // 添加到场景
              scene3D.addChild(obj);
              // 创建前向渲染
              let renderJob: ForwardRenderJob = new ForwardRenderJob(scene3D);
              // 开始渲染
              Engine3D.startRender(renderJob);
          }
      }
      

      完成项目

      可以看到,创建完一个项目后,只添加了hello.ts文件和修改了App.vue。这样一个真正的3D项目开发完成了,再在浏览器里面看一下运行效果(不要忘记在canary)。

      215b515d-ee86-4b19-847c-f968b6fab428-image.png

      你得到了一个可以用鼠标操作的立方体,按住左键拖拽可以任意旋转,滑动滚动可以调整距离,按住右键拖拽可以快速调整立方体位置,可以多操作一下熟悉一下这种最常见的操作方式。
      回忆我刚运行这一步时还是有点激动的,终于迈进了3D技术的大门,尽管是借用了引擎的助力,不过我们程序员就要擅长找到适合自己的工具。

      代码解析

      作为3D小白,我的目标是始于orillusion但不会止于某一引擎,通过充分熟悉一个引擎的用法来积累3D知识。可以看到每一个类都对应着3D世界的基础概念,在这里做一个简单的对应,顺便看看一个3D世界由哪些部分组成的,当然这里只有一个基础印象即可,不必深究。

      • 立方体:我们看到的立方体,是一个基础几何体,由类BoxGeometry来创建,实例化这个类在构造函数中指定长 宽 高,引擎就会为我们绘制出一个立方体,就像我们看到的这样。
      • 相机:相机是一个比较抽象但是在3D中无处不在,可以理解成我们的眼睛,我们是通过相机来观察3D内景物的,比如前面的立方体要在相机的可视范围内我们才能看到,在这里用组件Camera3D来定义。
      • 场景:是一个容器,前面的立方体、相机等等还有其它的对象、组件都是要添加到场景内才能被引擎组织和使用的,我们创建一个3D程序必须有一个场景,我们可以通过Scene3D类创建场景

      这里只介绍了三个最基础的组成部分,几何体、相机、场景,这三个部分在每一个3D程序中都是最基础的不可少的,以后我们会逐渐深入了解他们,也将会慢慢知道更多的工具。

      小结

      这篇文章是个开篇,快速上手了一个入门级别的3D示例,主要的作用并不是学习3D的基本技能,可以说更重要的是消除疑惑,最简单的上手3D项目。不得不说orillusion没有让我失望,以我目前这么简单的诉求也不会让我失望吧。

      作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

      发布在 中文社区 orillusion引擎 engine
      O
      oldguy
    • orillusion入门系列三 | 几何体

      前两次的学习总结中,成功运行了一个最基础的功能,对引擎的的整体代码进行了归纳,根据我的习惯提供了一个并不严谨的开发步骤,并且介绍了两个工具。主要解决了认识引擎以及如何开始使用引擎。今天我准备学习使用引擎开发真正的功能了,在这里继续使用大白话(水平有限只能如此)介绍学习过程。
      专业的3D系统开发需要符合软件工程实践,只以像我这样的小白视角以入门为目的提供一个思路,我们前面了解到首先要通过场景开辟一个空间,加上相机开启上帝视角,那么之后呢?我认为是“造物”,一个3D空间无论复杂还是简单,都可以看成是一个虚拟世界,是对真实世界的理解的反映,在真实世界中我们能看到的物体是由点、线、面组成的,在3D场景中最基础的是由点组成的,3D中的点由三个坐标(x、y、z)分量构成的,GPU在处理时一般是按照三角型处理的,这些点我们一般称为顶点。在3D中的一座山、一条路、一个篮球,都是由若干的顶点构成的。但是只有顶点是不够的,每个物体还会有不同的外观,我们一般用材质来表示,材质对物体的外观进行装饰成不同的样式。所以顶点+材质就组成了一个物体,这个物体我们一般称为网格(mesh),今天我们先忽略材质,专注于造物。

      几何体

      面向3D的世界我还是学前班阶段,所以我们从最基础的几何体开始。orillusion目前提供了四种几何体可以直接使用,并不算多,实践中肯定会需要更多的类型吧,期待后续会增加,这里先熟悉这四种,对于了解几何体是足够的了。

      长方体

      长方体是我的最爱,我们在第一天就熟悉了如何使用长方体,一个长方体由宽度、高度、深度三个分量组成,这里我们复习一下如何使用

      // 创建一个容器对象
      this.boxObj = new Object3D();
      // 创建渲染组件
      let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
      // 创建一个长方体
      mr.geometry = new BoxGeometry(5, 5, 5);
      // 设置材质
      mr.material = new HDRLitMaterial();
      // 添加到场景
      this.scene.addChild(this.boxObj);
      

      99816cf9-e104-441d-aaba-f88ab72545b9-image.png
      这里需要注意,几何体的属性在创建时通过构造函数参数指定,不支持动态的修改,动态改变形状建议使用Object3D对象的缩放属性。

      球体

      球体也是我们非常熟悉的一种几何体,创建球体必须要指定半径,以及水平和垂直分段数。

      // 创建一个对象
      this.boxObj = new Object3D();
      // 创建渲染组件
      let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
      // 设置球体的实例
      mr.geometry = new SphereGeometry(3, 100, 100);
      // 设置材质
      mr.material = new HDRLitMaterial();
      // 添加到场景
      this.scene.addChild(this.boxObj);
      

      运行效果如下
      a3cd9adc-1d7f-4789-9e22-1081a398f3f0-image.png

      平面

      创建一个平台我们至少需要指定长和宽两个属性

      // 创建一个对象
      this.boxObj = new Object3D();
      // 创建渲染组件
      let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
      // 设置形状
      mr.geometry = new PlaneGeometry(10, 10);
      // 设置材质
      mr.material = new HDRLitMaterial();
      // 添加到场景
      this.scene.addChild(this.boxObj);
      

      运行效果如下
      512a8267-23ee-4f83-b539-0e4c71d95128-image.png

      圆柱体

      我们这里创建一个半径为5,高为10的圆柱体

      // 创建一个对象
      this.boxObj = new Object3D();
      // 创建渲染组件
      let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
      // 设置形状
      mr.geometry = new CylinderGeometry(5, 5, 10);
      // 设置材质
      mr.material = new HDRLitMaterial();
      // 添加到场景
      this.scene.addChild(this.boxObj);
      

      运行效果如下
      99c32a9a-92a1-4819-84a9-a8e30506b1b0-image.png

      Object3D 对象

      我们不是第一次接触Object3D对象了,这里我们通过实例用一下这个组件,我们以长方体为例,长方体是通过组件的方式添加到一个Object3D容器中的,所以通过Object3D可以操作立方体的属性。

      位置

      分别可以直接读写对象的x、y、z坐标。

      this.boxObj.x = v;
      this.boxObj.y = v;
      this.boxObj.z = v;
      

      也可以通过 Vector3 类型一次性设置三个分量,这里不作演示了

      旋转

      分别可以直接读写对象x、y、z三个方向的旋转角度。

      this.boxObj.rotationX = v;
      this.boxObj.rotationy = v;
      this.boxObj.rotationz = v;
      

      也可以通过 Vector3 类型一次性设置三个分量,这里不作演示了

      缩放

      分别可以直接读写对象x、y、z三个方向的缩放。

      this.boxObj.scaleX = v;
      this.boxObj.scaleY = v;
      this.boxObj.scaleZ = v;
      

      也可以通过 Vector3 类型一次性设置三个分量,这里不作演示了

      动态演示或隐藏

      设置对象的 visible 属性可以对对象演示或隐藏

      this.boxObj.visible = v;
      

      使用脚本

      前面创建的物体看起来很死板,用鼠标操作一下才会动,如何让这个自己有一定的行为呢。那要请出组件了。组件一般用来封装可复用的功能,添加至不同的Object3D中,引擎内部会自动调用该组件的功能作用在这个容器对象中。
      这里我们看一个最基本的组件脚本的模板:

      class Script extends ComponentBase {
        // 覆写 初始化
        public init(){
          // 该函数在组件被创建时调用,可以用来初始化内部的变量
          // 注意,此时组件被挂载到 Object3D 上,所以无法访问 this.object3D
        }
        // 覆写 渲染开始
        public start(){
          // 该函数在组件开始渲染前被调用,
          // 一颗访问 this.object3D, 通常用来获取节点的属性或其他组件
        }
        // 覆写 update
        public update() {
          // 每帧渲染循环调用,通常定义节点的循环逻辑
          // 例如,每一帧更新物体旋转角度
          this.object3D.rotationY += 1;
        }
      }
      

      以上模板的注释说明了用法,这里我准备做一个自动旋转的功能,只要实现 update 函数就可以做到。

      update(): void {
        // 旋转
        this.transform.rotationX = Math.sin(Time.time * 0.001) * 100.0;
      }
      

      之后将该组件添加至boxObj中,看来需要熟识上面的脚本模板,这几个生命周期函数非常重要,在后面定制功能时必不可少。而且除了用户代码,引擎内部也定义了一些组件,后面我们要不断去了解。

      完成代码

      这里以立方体为例,将代码罗列一下。

      import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, MeshRenderer, BoxGeometry, HDRLitMaterial, ForwardRenderJob, Stats } from "@orillusion/core";
      import * as dat from 'dat.gui';
      import { RotationScript } from "./rotationScript";
      
      export default class Geometry {
      
          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();
          }
      
          /**
           * 引擎功能代码
           */
          private async setup() {
              // 创建一个场景
              this.scene = new Scene3D();
      
              // 添加性能监控面板
              this.scene.addComponent(Stats);
              // 创建一个相机
              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);
      
              this.createBox();
      
              this.addGUI();
      
          }
      
          /**
           * 启动渲染
           */
          private async start() {
              // 创建前向渲染
              let renderJob: ForwardRenderJob = new ForwardRenderJob(this.scene);
              // 开始渲染
              Engine3D.startRender(renderJob);
          }
      
          /**
           * 创建立方体
           */
          private async createBox() {
              // 创建一个对象
              this.boxObj = new Object3D();
              // 挂载脚本
              this.boxObj.addComponent(RotationScript);
              // 创建渲染组件
              let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
              // 设置形状
              mr.geometry = new BoxGeometry(5, 5, 5);
              // 设置材质
              mr.material = new HDRLitMaterial();
              // 添加到场景
              this.scene.addChild(this.boxObj);
      
          }
      
          private async addGUI(){
              // 创建 dat 实例
              const gui = new dat.GUI();
              // 创建保存属性值对象
              const geometryInfo = {
                  enable:true,
                  visible:true,
                  rotationX:0,
                  rotationY:0,
                  rotationZ:0,
                  scaleX:0,
                  scaleY:0,
                  scaleZ:9,
                  x: 0,
                  y: 0,
                  z: 0,
              };
      
              // 创建一个x坐标事件监听,当修改x值时,直接修改立方体坐标。
              gui.add(geometryInfo, "enable").onChange((v) => {
                  console.log('enable:', v);
                  this.boxObj.enable = v;
              });
              gui.add(geometryInfo, "visible").onChange((v) => {
                  console.log('visible:', v);
                  this.boxObj.visible = v;
              });
              gui.add(geometryInfo, "x", -100, 100).step(0.1).onChange((v) => {
                  console.log('x:', v);
                  this.boxObj.x = v;
              });
              gui.add(geometryInfo, "y", -100, 100).step(0.1).onChange((v) => {
                  console.log('y:', v);
                  this.boxObj.y = v;
              });
              gui.add(geometryInfo, "z", -100, 100).step(0.1).onChange((v) => {
                  console.log('z:', v);
                  this.boxObj.z = v;
              });
              gui.add(geometryInfo, "rotationX", -100, 100).step(0.1).onChange((v) => {
                  console.log('rotationX:', v);
                  this.boxObj.rotationX = v;
              });
              gui.add(geometryInfo, "rotationY", -100, 100).step(0.1).onChange((v) => {
                  console.log('rotationY:', v);
                  this.boxObj.rotationY = v;
              });
              gui.add(geometryInfo, "rotationZ", -100, 100).step(0.1).onChange((v) => {
                  console.log('rotationZ:', v);
                  this.boxObj.rotationZ = v;
              });
      
              gui.add(geometryInfo, "scaleX", 0, 10).step(0.1).onChange((v) => {
                  console.log('scaleX:', v);
                  this.boxObj.scaleX = v;
              });
              gui.add(geometryInfo, "scaleY", 0, 10).step(0.1).onChange((v) => {
                  console.log('scaleY:', v);
                  this.boxObj.scaleY = v;
              });
              gui.add(geometryInfo, "scaleZ", 0, 10).step(0.1).onChange((v) => {
                  console.log('scaleZ:', v);
                  this.boxObj.scaleZ = v;
              });
          }
      
      }
      
      

      脚本完整代码

      import { ComponentBase, Time } from "@orillusion/core";
      
      export class RotationScript extends ComponentBase {
      
          update(): void {
            // 旋转
            this.transform.rotationX = Math.sin(Time.time * 0.001) * 100.0;
          }
        }
        
      

      668c248c-7592-4935-8ed4-76b70e37e3b9-image.png

      因为加上了脚本,运行时能够看到这个方立体在自动旋转。右侧的操作面板做对应的操作也可以看到变化。

      小结

      今天学习了最基本的功能,没有按照通常的3D学习顺序,这里只按照最朴素(土)的习惯性思维来介绍。现在我们学会了造物,后续要继续学习如何装扮这些物体。
      看到了社区的反馈,有大佬对使用vue框架持提出质疑,瑟瑟发抖中,后面略去界面库部分。
      作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

      发布在 中文社区 orillusion引擎 engine
      O
      oldguy
    • orillusion入门系列二 | 快速入门

      前文我们自己动手使用引擎体验到了3D的效果,今天我们尝试梳理一下使用引擎做3D开发从何处入手,快速的从整体上了解如何使用 orillusion 开发3D项目。

      引擎的挑战

      3D是一项面向视觉的技术,为了获得更逼真、更炫酷、更流畅的体验,需要极强的理论根基和工程实践经验。建议先粗略的看看引擎所要面对的一些实际问题,聊一下对引擎浮浅的认识,以便我们能更好的理解如何使用引擎。

      • 从贵族到平民 数学是构建3D世界的支柱,3D效果的发展离不开对数学和算法的深刻理解和应用,比如为了体现光线的真实感,为了实现服装飘逸的感觉,都有许多个数学公式作为底层支撑。另一方面需要充分了解硬件的特性,这都是非常耗费时间的工作,引擎使得没有这方面基础的普通开发者集中时间在实现需求上。
      • 从贪婪到克制 无论硬件是否会继续以遵循摩尔定律的规律提升,引擎要以足够贪婪的程度榨取硬件的性能,使硬件上的投资获得回报,同时要克制自身对资源的占用,即要又要的典范。
      • 从混乱到秩序 引擎所面向的世界是多方面的,不同的硬件之间如何协调,多变的网络环境,多种资源处理,稳定灵活高效的任务调度等等,在引擎的帮助下有条不紊的组织在一起。
      • 始终面带微笑 引擎要发挥出足够的作用,必须要让用户足够容易上手,提供给用户要有足够友好的api,还有完备的工具链和文档,所以引擎要提供尽可能的友好使用体验。

      使用引擎

      因为以上罗列的一些原因,引擎需要需要做许多取舍,在使用引擎的时候也有一些固定的要求。大体上我们分为搭建环境、引擎配置、引擎初始化、开启渲染任务这几个步骤。

      搭建环境

      Orillusion 支持多种方式安装,具体可以参考官方文档,上一篇文章使用了 vue&vite 系列的开发环境,这里假设您已经进行了安装。

      引擎配置

      引擎为了满足多样的需求和不同的环境,提供了许多配置选项,一般代码的最初位置进行引擎配置,后续的初始化和运行需要依赖配置项。这里只列出两条配置示例,更多的配置项可以参考官方文档。

      // 最大实体数量
      Engine3D.engineSetting.memorySetting.doMatrixMaxCount = 100000;
      // 最大灯光数量
      Engine3D.engineSetting.lightSetting.maxLight = 1024;
      

      初始化

      引擎进行了配置之后,需要进行初始化,在初始化这一步可以根据需求申请必要的内存。这里我们可以配置 canvas变量,也可以指定渲染前回调、每一帧回调以前每一帧渲染后的回调。

      // 先配置,后初始化
      await Engine3D.init({
          canvasConfig:{
              // 配置 cavans
              canvas: document.getElementById("webGpuCanvas"),
              alpha: false,
              zIndex: 1
          },
          beforeRender: ()=>{
              // 每一帧渲染前回调
          },
          renderLoop: ()=>{
              // 每一帧回调
          },
          lateRender: ()=>{
              // 每一帧渲染后回调
          }
      });
      

      我们目前没有特别的功能需求,一般用最简化的初始化代码。

      await Engine3D.init();
      

      功能代码

      编写一个相对简单的3D程序,我们可以简单的将过程理解为几个步骤:

      1. 开辟一个3D空间:创建一个场景,相当于一个3D空间,后续的资源可以添加到这个场景中。
      // 创建一个场景
      let scene3D: Scene3D = new Scene3D();
      
      1. 设置一个观察点:这个观察点我们一般用相机组件来体现,所有看到的场景内的信息都是通过相机组件的角度实现的,相机支持一系列的配置参数和方法,后续我们会有专门的章节来学习,同时该相机也要添加至场景中。
      // 创建一个实体对象,用来管理相机组件
      let cameraObj: Object3D = new Object3D();
      // 向 相机 对象内添加相机组件 Camera3D 是我们的相机组件
      let camera = cameraObj.addComponent(Camera3D);
      
      // 相机的配置略
      ...
      
      // 添加相机至场景
      scene3D.addChild(cameraObj);
      
      1. 添加物体:这里我们为了演示,添加了一个立方体,同样需要添加至场景中。
      // 创建一个对象
      const obj = new Object3D();
      // 创建渲染组件
      let mr = obj.addComponent(MeshRenderer);
      // 设置形状
      mr.geometry = new BoxGeometry(5, 5, 5);
      // 设置材质
      mr.material = new HDRLitMaterial();
      // 添加到场景
      scene3D.addChild(obj);
      

      开始渲染任务

      所谓渲染形象来说是,将场景内的景物,通过相机作为观察点,生成二维图像,显示在演示设备上的过程。我们这里一般显示设备是前端的canvas,所以渲染可以理解为从场景到canvas的过程,为了保持流畅,需要高速的刷动作。
      渲染方式有多种,最常见的是前向渲染,其它模式我们遇到再聊。

      // 创建一个前向渲染器
      let renderJob = new ForwardRenderJob(this.scene);
      // 后处理
      // ...
      // 开始渲染任务
      Engine3D.startRender(renderJob);
      

      后处理

      从代码上看,后处理是要放到开始渲染任务之前,但是从逻辑上这里放到最后一步,因为后处理是在生成图片后,界面显示前所做的一次纯粹的图片处理。
      后处理需要在开始渲染任务前添加在渲染器中。

      renderJob.addPost(new SSRPost());//屏幕空间反射
      renderJob.addPost(new SSAOPost());//屏幕空间SSAO
      renderJob.addPost(new GlobalFog());//屏幕空间雾化
      renderJob.addPost(new HDRBloomPost());//HDR Bloom 泛光
      renderJob.addPost(new FXAAPost());//屏幕抗锯齿
      

      后处理是个相对独立又非常有趣的功能,后续我们会专门深入了解。

      调测工具

      为了更好的观察程序的运行状态和动态的操控功能,了解两款相对应的工具很有必要。

      性能监控(Stats)

      Orillusion 官方提供的一个性能监控工具组件,可以显示引擎的运行状态,我们可能直接添加到场景中即可,默认会显示在运行界面的左上角,显示位置可以修改,这里我们使用默认设置就可以了。

      import {Stats} from '@orillusion/core'
      scene.addComponent(Stats);
      

      界面操控(dat.gui)

      一段程序在完成时,逻辑已经通过代码固定下来了,若需动态的改变程序逻辑,脱离不开设计好的输入控制和代码本身的逻辑。但是出于调试或测试的原因,需要动态的修改某个api或变量如何做呢?我们假设只通过界面来控制逻辑,orillusion 官方提供了 GUIHelp 工具类来实现这个功能,不过我们当前推荐使用 dat.gui 来做界面操控工具。

      简介

      dat.gui 是由google工程师出品的很轻量级界面操控工具,使用 javascript 实现,可以方便的集成至基于web的系统内,实现灵活的界面操控,特别适合作为3D程序观察调试程序的工具。
      dat.gui的优点显而易见:

      • 使用简单 dat.gui可以自动的操作dom节点,同步至界面显示效果,不需要我们手动繁琐的通过js来操作dom结构。
      • 轻量级 dat.gui代码量不大,不会对系统的加载造成太大的负担。
      • 功能强大 dat.gui提供了数字、逻辑、颜色、字符串、函数等几种数据类型,不同的数据类型有与之相适配的界面元素,为修改代码的变量和逻辑提供了很大的便利。

      用法

      详细的dat.gui建议查看官方或其它资料,我们这里只是简单的介绍需要用到的部分,后续随着操控部分的增加也会不断的更新,现在我们仅需要熟悉事件监听即可。
      目前支持两种事件监听类型:

      事件 触发
      onChange 值被修改时触发
      onFinishChange 失去焦点时触发

      具体代码在后续代码中一共演示。

      编写代码

      1. 添加@别名 以便代码中可以使用 @ 指代源码路径
        a. 安装node类型 npm i @types/node
        b. 修改 vite.config.ts 文件,新加 @ 别名配置
      export default defineConfig({
        resolve: {
          alias: {
            '@': join(__dirname, "src"),
          }
        },
        plugins: [vue()]
      })
      

      c. 修改 tsconfig.ts 文件,配置TypeScript类型提示,增加 baseUrl和paths配置项

      "baseUrl": ".",
      "paths": {
        "@/*": ["src/*"]
      }
      
      1. 重构目录结构
        a. 修改 demo 目录为 orilluson
        b. 在 src目录创建 views目录
        c. 在views 目录创建 Index.vue单文件组件,编辑代码如下
      <template>
      
      </template>
      
      <script lang="ts" setup>
        import { onMounted } from 'vue';
        import Hello from '@/orillusion/hello';
        onMounted(()=>{
          new Hello().run();
        })
      </script>
      
      <style scoped>
      </style>
      

      d. 重构项目根目录的App.vue文件,编辑代码如下

      <template>
        <Index></Index>
      </template>
      
      <script lang="ts" setup>
      import Index from "@/views/Index.vue";
      </script>
      
      <style scoped>
      
      </style>
      
      1. 安装 dat.gui: npm install --save dat.gui
      2. 完善 hello.ts ,主要从以下几个方面来进行
        a. 重构 Hello 类,将 函数内的临时变量声明成类成员变量
        b. 根据我们前面的引擎步骤,增加处理函数
        c. 增加 dat.gui 操作立方体的控制项
      3. 完成 hello.ts 代码
      import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, MeshRenderer, BoxGeometry, HDRLitMaterial, ForwardRenderJob, Stats } from "@orillusion/core";
      import * as dat from 'dat.gui';
      
      export default class Hello {
          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();
          }
      
          /**
           * 引擎功能代码
           */
          private async setup() {
              // 创建一个场景
              this.scene = new Scene3D();
      
              // 添加性能监控面板
              this.scene.addComponent(Stats);
              // 创建一个相机
              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);
      
              this.createBox();
              this.addGUI();
      
          }
      
          /**
           * 启动渲染
           */
          private async start() {
              // 创建前向渲染
              let renderJob: ForwardRenderJob = new ForwardRenderJob(this.scene);
              // 开始渲染
              Engine3D.startRender(renderJob);
          }
      
          /**
           * 创建立方体
           */
          private async createBox() {
              // 创建一个对象
              this.boxObj = new Object3D();
              // 创建渲染组件
              let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
              // 设置形状
              mr.geometry = new BoxGeometry(5, 5, 5);
              // 设置材质
              mr.material = new HDRLitMaterial();
              // 添加到场景
              this.scene.addChild(this.boxObj);
          }
      
          private async addGUI(){
              // 创建 dat 实例
              const gui = new dat.GUI();
              // 创建保存属性值对象
              const geometryInfo = {
                  x: 0,
                  y: 0,
                  z: 0
              };
      
              // 创建一个x坐标事件监听,当修改x值时,直接修改立方体坐标。
              gui.add(this.boxObj, "x", -100, 100).step(0.1).onChange((v) => {
                  console.log('x:', v);
                  this.boxObj.x = v;
              });
              gui.add(geometryInfo, "y", -100, 100).step(0.1).onChange((v) => {
                  console.log('y:', v);
                  this.boxObj.y = v;
              });
              gui.add(geometryInfo, "z", -100, 100).step(0.1).onChange((v) => {
                  console.log('z:', v);
                  this.boxObj.z = v;
              });
          }
      
      }
      

      完成项目

      今天的代码我们主要演示了两款测试工具,在左上角是性能监控面板,在右上角是操控面板,用鼠标滑动滚动条或者直接输入数值,可以分别理性立方体的x、y、z的值,物体对应的会发生移动。
      8414362f-b932-4fa1-97d3-fd5cff4010b2-image.png

      代码解析

      今天的代码在第一篇的基础上有所增加,但是以下三个类型是第二次出现了,我们不去深究更详细的原理,需要对这三个常用的对象类型有所了解。

      • Object3D 是引擎内置的实体对象,通常被当做基本的组件容器,可以通过不同组件组合来实现不同的类型功能。我的理解是,Object3D内置一些基础功能对象,还可以是功能组件的容器,比如我们这里创建一个用于管理立方体的容器boxObj。
      • MeshRenderer 是一个用于渲染网格的组件,这个组件非常重要,包含几何属性和材质属性,每个需要渲染的物体都需要有几何属性和材质属性。
      • HDRLitMaterial PBR 材质,基于物理渲染,旨在模拟现实世界光照效果,以后我们会专门来学习材质的部分,这里只要知道这是一种材质就可以。
        以上三个类型都非常重要,首先我们要创建一个Object3D作为容器,然后在这个容器中创建MeshRenderer作为渲染组件,渲染组件中需要实例化几何体和材质。就这样由容器与功能组件相互配合完成功能,当然都需要按照一定的层级结构添加至场景中。

      小结

      今天根据我的理解总结了一个引擎所必须面对的几个难点,可以更好的理解引擎的设计,同时拆解了 orilluson 代码的几个步骤,以便能够快速的上手3D代码开发。进一步的介绍了两个非常常用的调测工具,为以后自由的调用功能提供了工具。之后继续完善项目,运行项目演示效果。最后我们熟悉了最常见的三个组件。
      作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

      发布在 中文社区 orillusion引擎 engine
      O
      oldguy
    • orillusion入门系列五 | 光照01

      回忆一下过去的学习过程,场景开辟了一处空间,可以作为容器,几何体使空间中有了物体,物体具有一些典型的形状,材质使物体得到了美化和更有真实感,今天按照惯例应该到光照了。没有光我们看不到任何物体,渲染的工作主要在对于光的处理上。
      光是由光源发出的,光在传播的过程中会经过物体的反射、折射、漫反射等多种变化,而反射回去的光又会反复的物体间反射,反射的过程中还会产生阴影,所以这是一个非常复杂的过程。我们从典型的光源组件开始,由浅入深的使用 orillusion 引擎尝试下对光的操作。

      光源组件

      观察我们的生活环境我们被各种光包围,orillusion封装了常见的三种光源组件,使用时创建 Object3D类型的对象,然后将光源组件添加至刚创建的对象中,就可以实现典型的光源效果。
      光源组件的用法可以参照以下示例:
      // 创建对象,对象是组件的容器

      let obj= new Object3D();
      // 将对象添加至场景
      scene.addChild(obj);
      // 添加一个光源组件至对象中,我们这里以平行光为例
      let light= obj.addComponent(DirectLight);
      

      平行光

      光顾名思义平行光是一种光线的角度相同,不同位置的光照强度相机的光,自然界中最常见到的平行光源是太阳。在场景中为了影响整体的光影效果,一般引擎会提供平行光功能。
      平行光一般具有以下的参数:

      • lightColor(光照颜色): 每个平行光都必须有一定颜色,这里如果不指定会又白色作为默认颜色,需要注意的是目标物体所呈现出来的颜色还和物体本身的材质有关。
      • intensity(光照强度):用来调节光线的强弱,毫无疑问光线强度越大光线越充足,越弱则越暗,如果完全没有光,场景内将陷入黑暗,通常除了光源还会有环境光,所以默认情况下不会是漆黑一片。
      • castShadow(开启阴影):只有开启阴影的光才会产生阴影,同时接收阴影的物体本身也要打开对应的参数才能看到阴影效果。
      • 方向:这不是平行光本身的参数了,这里必须要强调一点,平行光是有方向的,平行光的方向是由组件所对应的对象调节的,对象本身的旋转属性(rotationX、rotationY、rotationZ),用来调节对象所包含的光照组件方向,下面点光源和聚光灯类似。
        这里罗列一下关键的代码:
      // 创建一个容器对象
      let lightObj = new Object3D();
      //类中声明一个光源对象,这里初始化组件实例
      this.directLight = lightObj.addComponent(DirectLight);
      // 为了观察效果,打开了阴影
      this.directLight.castShadow = true;
      // 将对象添加到场景中
      this.scene.addChild(lightObj);
      
      // 定义了调整的参数结构
      const lightInfo = {
          intensity:1.0,
          rotationX:0,
          rotationY:0,
          rotationZ:0,
          lightColor:'#fff',
      }
      // 这里使用data.gui来实现界面效果
      let lightFolder = gui.addFolder("Light");
      // 强度直接修改
      lightFolder.add(lightInfo, "intensity", 0, 10).step(0.1).onChange((v) => {
          console.log('intensity:', v);
          this.directLight.intensity = v;
      });
      lightFolder.add(lightInfo, "rotationX", -360, 360).onChange((v) => {
          console.log('rotationX:', v);
          this.directLight.transform.rotationX = v;
      });
      lightFolder.add(lightInfo, "rotationY", -360, 360).onChange((v) => {
          console.log('rotationY:', v);
          this.directLight.transform.rotationY = v;
      });
      lightFolder.add(lightInfo, "rotationZ", -360, 360).onChange((v) => {
          console.log('rotationZ:', v);
          this.directLight.transform.rotationZ = v;
      });
      lightFolder.addColor(lightInfo, "lightColor").onChange((v) => {
          console.log('lightColor:', v);
          let color: Color = new Color();
          color.setHex(v);
          this.directLight.lightColor = color;
      });
      

      修改完后可以在 canary 中直接运行,查看效果如下:
      2338c3cc-7ee6-46c2-b46b-3e2c27321021-image.png
      这个示例里面除了我们重点关注的平行光以外,还有阴影,反射等等,下面会一一介绍。使用思路仍然是通过熟悉 orilluson 的api,调用不同的接口与参数即可。
      这里可以看到,没有做位置的变换操作,在当前的版本调整位置是没有效果的,调整强度参数能够看到光线的强度随着数值而动态变化。

      点光源

      有了前面最好理解的平行光作为铺垫我们已经熟悉了光源对象的使用,点光源就是另种特性的光源,类似于萤火虫、没有灯罩的白炽灯这样的发光体,本身是一个点,向四面八方发射光线。
      点光源有更多的属性,除了平行光的属性外,罗列一下点光源所特有的属性:

      • range(距离):从光源到光线衰减到0的最远距离,该值越大能够发出光线受到影响的范围越大。
      • radius(半径):光照的最亮那部分的半径。
      • at(衰减系数):光照的效果根据系数有所衰减。
      • 位置:点光源由于受距离的影响,不同距离显示的效果不同,所以位置起着重要的作用。与平行光的旋转类似,点光源的位移是由对应的对象所控制。
        点光源的调用关键代码如下:
      // 创建一个光源对象
      let lightObj = new Object3D();
      
      // 由于点光源的位置影响光照效果,所以附加一个球形物体用来标记位置 
      let mr = lightObj.addComponent(MeshRenderer);
      mr.geometry = new SphereGeometry(0.5, 10, 10);;
      mr.material = new HDRLitMaterial();
      
      //创建点光源组件
      this.lightLight = lightObj.addComponent(PointLight);
      // 打开阴影效果
      this.lightLight.castShadow = true;
      // 光源对象添加到场景
      this.scene.addChild(lightObj);
      // 设置光源的位置 
      lightObj.y = 10;
      
      // 点光源可以使用的参数
      const lightInfo = {
          intensity:1.0,
          range:0,
          radius:0,
          at:0,
          x:0,
          y:0,
          z:0,
          rotationX:0,
          rotationY:0,
          rotationZ:0,
          lightColor:'#fff',
      }
      
      // 对各参数进行设置
      let lightFolder = gui.addFolder("Light");
      lightFolder.add(lightInfo, "intensity", 0, 10).step(0.1).onChange((v) => {
          console.log('intensity:', v);
          this.lightLight.intensity = v;
      });
      lightFolder.add(lightInfo, "range", 0, 10).step(0.1).onChange((v) => {
          console.log('range:', v);
          this.lightLight.range = v;
      });
      lightFolder.add(lightInfo, "radius", 0, 10).step(0.1).onChange((v) => {
          console.log('radius:', v);
          this.lightLight.radius = v;
      });
      lightFolder.add(lightInfo, "at", 0, 10).step(0.1).onChange((v) => {
          console.log('at:', v);
          this.lightLight.at = v;
      });
      lightFolder.add(lightInfo, "x", -10, 10).onChange((v) => {
          console.log('x:', v);
          this.lightLight.transform.x = v;
      });
      lightFolder.add(lightInfo, "y", -10, 10).onChange((v) => {
          console.log('y:', v);
          this.lightLight.transform.y = v;
      });
      lightFolder.add(lightInfo, "z", -10, 10).onChange((v) => {
          console.log('z:', v);
          this.lightLight.transform.z = v;
      });
      lightFolder.add(lightInfo, "rotationX", -360, 360).onChange((v) => {
          console.log('rotationX:', v);
          this.lightLight.transform.rotationX = v;
      });
      lightFolder.add(lightInfo, "rotationY", -360, 360).onChange((v) => {
          console.log('rotationY:', v);
          this.lightLight.transform.rotationY = v;
      });
      lightFolder.add(lightInfo, "rotationZ", -360, 360).onChange((v) => {
          console.log('rotationZ:', v);
          this.lightLight.transform.rotationZ = v;
      });
      lightFolder.addColor(lightInfo, "lightColor").onChange((v) => {
          console.log('lightColor:', v);
          let color: Color = new Color();
          color.setHex(v);
          this.lightLight.lightColor = color;
      });
      

      以上代码运行的效果如下:
      71ddfcd4-9203-4e21-bfed-a8fe640de169-image.png
      可以看到点光源是一个球形的光照效果,一般用来模拟点状的光源,计算量大于平行光源。因为点光源向各个方向的光线强弱相同,所以调整方向不会有效果,但是位置的作用非常明显。
      建议实现这个代码,实际进行操作可以加强印象和理解。

      聚光灯

      聚光灯与点光源非常类似,与点光源相比聚光灯只显示一个椎体范围内的效果,通常用来模拟手动筒,探照灯等这些发光体。
      聚光灯所支持的参数除点光源以外还有以下几个:

      • innerAngle(内切角):光锥内切角,聚光在小于这个角度的范围内有光线
      • outerAngle(外切角):光锥外切角,光线会在内切角到外切角的范围内逐步衰减到0
      • 位置与方向:聚光灯光源受位置和方向双重影响,所以可以通过对应的对象调整位置与方向。
        关键调用示例代码如下:
      // 创建光照,应该已经很熟了吧,不做过多解释
      let sp = new SphereGeometry(0.5, 10, 10);
      
      let lightObj = new Object3D();
      
      let mr = lightObj.addComponent(MeshRenderer);
      mr.geometry = sp;
      mr.material = new HDRLitMaterial();
      
      this.spotLight = lightObj.addComponent(SpotLight);
      this.spotLight.castShadow = true;
      this.scene.addChild(lightObj);
      lightObj.y = 10;
      
      // 增加界面操作
       const lightInfo = {
          intensity:1.0,
          range:0,
          radius:0,
          innerAngle:0,
          outerAngle:0,
          at:0,
          x:0,
          y:0,
          z:0,
          rotationX:0,
          rotationY:0,
          rotationZ:0,
          lightColor:'#fff',
      }
      
      let lightFolder = gui.addFolder("Light");
      lightFolder.add(lightInfo, "intensity", 0, 10).step(0.1).onChange((v) => {
          console.log('intensity:', v);
          this.spotLight.intensity = v;
      });
      lightFolder.add(lightInfo, "range", 0, 100).step(0.1).onChange((v) => {
          console.log('range:', v);
          this.spotLight.range = v;
      });
      lightFolder.add(lightInfo, "radius", 0, 10).step(0.1).onChange((v) => {
          console.log('radius:', v);
          this.spotLight.radius = v;
      });
      lightFolder.add(lightInfo, "at", 0, 10).step(0.1).onChange((v) => {
          console.log('at:', v);
          this.spotLight.at = v;
      });
      lightFolder.add(lightInfo, "innerAngle", 0, 100).step(0.1).onChange((v) => {
          console.log('innerAngle:', v);
          this.spotLight.innerAngle = v;
      });
      lightFolder.add(lightInfo, "outerAngle", 0, 100).step(0.1).onChange((v) => {
          console.log('outerAngle:', v);
          this.spotLight.outerAngle = v;
      });
      lightFolder.add(lightInfo, "x", -10, 10).onChange((v) => {
          console.log('x:', v);
          this.spotLight.transform.x = v;
      });
      lightFolder.add(lightInfo, "y", -10, 10).onChange((v) => {
          console.log('y:', v);
          this.spotLight.transform.y = v;
      });
      lightFolder.add(lightInfo, "z", -10, 10).onChange((v) => {
          console.log('z:', v);
          this.spotLight.transform.z = v;
      });
      lightFolder.add(lightInfo, "rotationX", -360, 360).onChange((v) => {
          console.log('rotationX:', v);
          this.spotLight.transform.rotationX = v;
      });
      lightFolder.add(lightInfo, "rotationY", -360, 360).onChange((v) => {
          console.log('rotationY:', v);
          this.spotLight.transform.rotationY = v;
      });
      lightFolder.add(lightInfo, "rotationZ", -360, 360).onChange((v) => {
          console.log('rotationZ:', v);
          this.spotLight.transform.rotationZ = v;
      });
      lightFolder.addColor(lightInfo, "lightColor").onChange((v) => {
          console.log('lightColor:', v);
          let color: Color = new Color();
          color.setHex(v);
          this.spotLight.lightColor = color;
      });
      

      运行界面效果如下:
      1850f7b0-3fe6-4e40-8e1f-a6ac07dca197-image.png

      可以将聚光灯当成一个手电筒玩耍,多操作界面操作,会很快熟悉这种光源的。

      其它

      orilluson对功能模块进行了重新划分,Stats模块需要单独安装,执行命令 npm install @orillusion/stats --save进行安装,引入代码修改为import { Stats } from "@orillusion/stats";其它不变。

      小结

      这是第一次接触光照的特性,所以从最简单易于理解的三种光源来着手,orilluson提供的这三类光源非常方便,为我接触3D的光照提供了一个很好的切入点。后面要继续熟悉环境光,全局光照,以前阴影等等。
      作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

      发布在 中文社区 orillusion引擎 engine
      O
      oldguy

    oldguy 发布的最新帖子

    • orillusion入门系列六 | 输入系统01

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

      输入系统

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

      1. Engine3D.init:初始化引擎后,实例化了InputSystem类,并将canvas实例传入InputSystem类;
      2. InputSystem.initCanvas:InputSystem监听了画布的键盘与鼠标事件;
      3. addEventListener:引擎或对象通过addEventListener函数来挂载用户监听;
      4. 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:键盘弹起事件,使用输入系统挂载该事件,将会得到弹起键盘事件通知;
        下面来一起梳理一下使用流程:
      1. 初始化:必要的引擎初始化;
      2. 输入挂载:使用键盘挂载系统指定事件和回调;
      3. 处理回调:在回调中获取参数。

      基础示例

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

      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新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

      发布在 中文社区 orillusion引擎 engine
      O
      oldguy
    • orillusion入门系列五 | 光照02

      上一次了解到了光照的基本三类光源,这三类光源似乎在现实中不存在的,只是一种理想抽象,今天以光源为基础学习更多的光照功能,体验一下orilluson其它的光照功能。
      现实生活中我们被各种光包围,在光源发出光之后,环境会吸收光,反射光,我们可以称之为环境光。今天我们将这些实际中会遇到的光照效果一一验证,通过orilluson可以入门3D的光照吗,带着疑问来动手作吧。

      太阳

      场景作为一个空间的容器内置了天空的概念,天空中的太阳是直接光的典型元素,通常说的平行光在现实中很好的参照光源就是太阳。这里我们忽略场景的其它部分,就场景内置的太阳是环境光重要的一部分,现在来熟悉一下太阳的用法,这里要打起精神,竟然可以定制太阳。以我简陋的专业知识来推测,太阳似乎只是一个发光体加一个平行光源来实现的,既然引擎有这个便利的工具要实操起来。
      我们先看一下太阳支持的主要参数:

      • displaySun:这是一个开关,是否显示太阳,经过实际测试后要特别注意,关闭后太阳的光照效果仍然存在,只是太阳本身的物体会隐藏。
      • sunX:太阳本地的X坐标,调整这个参数太阳本体会移动,光照的方向和效果也会变化,与真实的太阳效果类似,配合Y坐标可以模拟太阳在天空移动的效果,所谓日升日落,斗转星移不在话下。
      • sunY:与sunX类似,是太阳的Y坐标。
      • sunRadius:设置太阳的半径。
      • sunRadiance:曝光强度,默认是10,取值范围一般0至100,数值越大整个场景的光越强烈,数值越小整个场景越暗,直到为0时只有太阳有光亮,整个场景已经没有曝光度。
      • sunBrightness:太阳本体的曝光强度,调整这个参数可以看到本体的亮度出现变化。

      用法

      AtmosphericScatteringSky 类是大气散射纹理,太阳的设置在这个类中,包括前面介绍的太阳参数。因为太阳是天空盒的属性,而天空盒一般加载在场景中,因此需要从场景开始。
      这里只记录最核心的代码:

      // 创建一个场景对象
      let scene3D:Scene3D = new Scene3D();
      // envMap 是场景的环境贴图变量,这里将环境转换为大气散射类型
      let sky = (scene.envMap as AtmosphericScatteringSky);
      // 以下参数可以参照前面的参数介绍
      sky.sunX = 0.55;
      sky.sunY = 0.8;
      sky.eyePos = 3607;
      sky.sunRadius = 500;
      sky.sunRadiance = 0.8;
      sky.sunBrightness = 1;
      // ...
      

      以上代码可以将一个场景的太阳进行配置,之后添加物体就会有阳光的可适配效果了。

      GUIHelp

      GUIHelp是orilluson提供的一个界面调试工具,类似dat.gui的功能效果,所不同的是内部组件可供调试的组件已经封装在了各个组件中,所以如果要快速的借用引擎封装的调试功能推荐用这款工具。GUIHelp内部也是用dat.gui实现的,所以界面风格是一样的。GUIHelp的用法只需要引入对应的资源,调用 GUIHelp.init()初始化之后,就可以在需要的组件中开启和关闭调试了,具体哪些组件可以开启需要我们不断的去发现。
      现在以GUIHelp工具开启天空盒太阳的设置效果,这样可以节省我们许多自定义的工作。

      // 初始化调试工具
      GUIHelp.init()
      // 创建场景
      let scene3D:Scene3D = new Scene3D();
      // ... 配置天空盒
      ...
      // 开启天空盒调试工具
      scene3D.debugAtomSky();
      

      以上代码只是关键的部分,领会思路就可以。
      以下是运行效果:
      cd84d3c2-f2f4-4f6c-8591-9d6433d06109-image.png

      其它

      场景内还有参数可以配置环境光效果,例如scene.exposure设置曝光度,可以通过这个数值来动态的设置全场景的曝光强度。

      全局光照

      当学习到全局光照时我的心情即兴奋又悲伤,兴奋是因为场景内总是给人塑料感,缺乏真实感,全局光照效果让我感觉到真实而震撼,悲伤是这里面的理论太复杂,行业发展很快,不过好在这里不深入理论算法,只是借用引擎来体验效果,是不是显得容易多了。
      所谓全局光照是与局部光照相对应的,通常认为全局光照是局部光照和间接光照的叠加。前面所介绍的由光直接照射效果称为局部光照,则物体反射而来的光为间接光照。
      我们按照引擎全局光照的使用顺序展开

      配置项

      全局光照的配置项路径在 Engine3D.engineSetting.globalIlluminationSetting 路径,支持的主要配置需要了解

      • enable:全局光照的开关,默认是关闭的,如果需要使用全局光照,需要在初始化引擎是将该配置项设置为true。
      • debug:调试全局光照组件开关,打开这个配置项在操作调试面板可以看到全局光照的参数,可以动态的调整,以前会显示探针的操作选项。
      • offsetX:探针组的注册点在x轴的偏移量,关于探针后面会介绍。
      • offsetY:探针组的注册点在y轴的偏移量。
      • offsetZ:探针组的注册点在z轴的偏移量。
      • probeXCount:探针在x轴的数量。
      • probeYCount:探针在y轴的数量。
      • probeZCount:探针在z轴的数量。
      • indirectIntensity:间接光的光照强度,前面介绍了全局光照同时处理间接光照和反射,这个参数是对间接光照的强度设置。
      • bounceIntensity:反射光的光照强度,是全局光照很重要的一个配置。
        更多的参数可以参考官方文档

      全局光照组件

      全局光照功能引擎封装在了一个组件中GlobalIlluminationComponent,前面配置了全局光照相应的参数后,需要创建一个任意的Object3D容器对象,并将全局光照组件添加到对象中。此时再开启渲染时可以看到全局光照效果,但是全局光照比较耗费计算机性能,建议初始效果不要调的太明显。

      示例代码

      这里记录一下最基本的代码,避免完整的代码占用篇幅。

      // 设置全局光照的配置
      Engine3D.engineSetting.globalIlluminationSetting.enable = false;
      Engine3D.engineSetting.globalIlluminationSetting.debug = true;
      Engine3D.engineSetting.globalIlluminationSetting.probeYCount = 6;
      Engine3D.engineSetting.globalIlluminationSetting.probeXCount = 6;
      Engine3D.engineSetting.globalIlluminationSetting.probeZCount = 6;
      Engine3D.engineSetting.globalIlluminationSetting.probeSpace = 36;
      Engine3D.engineSetting.globalIlluminationSetting.offsetX = -4;
      Engine3D.engineSetting.globalIlluminationSetting.offsetY = 29;
      Engine3D.engineSetting.globalIlluminationSetting.offsetZ = 7;
      Engine3D.engineSetting.globalIlluminationSetting.indirectIntensity = 2;
      Engine3D.engineSetting.globalIlluminationSetting.probeSize = 32;
      Engine3D.engineSetting.globalIlluminationSetting.octRTSideSize = 32;
      Engine3D.engineSetting.globalIlluminationSetting.autoRenderProbe = true;
      
      // 需要打开阴影,并实时计算才能观察到效果
      Engine3D.engineSetting.shadowSetting.shadowBound = 2;
      Engine3D.engineSetting.shadowSetting.shadowBias = -0.0008;
      Engine3D.engineSetting.shadowSetting.debug = false;
      Engine3D.engineSetting.shadowSetting.autoUpdate = true;
      Engine3D.engineSetting.shadowSetting.updateFrameRate = 1;
      
      // 其它初始化代码,例如相机,场景等
      ...
      
      //  添加全局光照组件
      let giObj= new Object3D();
      giObj.addComponent(GlobalIlluminationComponent);
      scene.addChild(giObj);
      
      // 添加两个物体,具体代码省略,前面的相关代码太多了
      ...
      

      运行后效果如下:
      c85dedf1-9a1c-4690-89a7-fdb3e99b9db8-image.png

      这里我添加了一个立方体和一个球体,在中间白色的区域是开启了全局光照的范围,能够看到更有了真实感,调整右侧面板可以观察所有的参数效果。

      阴影

      有光自然有阴影,阴影需要配置的方面比较少,前面已经用过许多次这里专门记录一下使用思路。

      阴影的配置

      在尝试使用光照阴影相关的功能时,遇到过几个问题是因为对阴影的配置不了解,这里把遇到问题的配置项进行一个记录。
      阴影的配置路径在Engine3D.engineSetting.shadowSetting下。

      • debug:调试开关,打开这个开关可以在控制面板操作参数值,仍然是依赖引擎的GUIHelp工具实现。
      • autoUpdate:阴影是否自动更新,这个配置项非常重要,在试用光照阴影效果时尝试过移动物体但阴影不动,打开这个开会会自动更新阴影。

      产生阴影

      目前只有平行光源会产生阴影,平行光组件需要将castShadow打开,在物体材质上调整光照的角度会产生阴影的效果。

      对比

      普通阴影和全局光照的阴影效果有很大的差别,这里将两个效果做个对比,上面是普通阴影,下面是开启了全局光照的效果。
      e4f4202a-7d8b-48e7-a750-6fbe969b23ec-image.png
      1599e838-db62-481e-a318-3920c2999e0d-image.png
      同样的材质和光照强度以及场景环境,下面加了全局光照有了更厚重逼真的感觉,这差距好比女人之间八卦产生的友谊和男人之间战斗建立的友谊一样大。

      小结

      光照是非常核心又非常高级的部分,在3D中渲染效果的好坏以及性如何和对光的处理有直接的关系,从上一次的三种光源可以使场景内有光,根据不同的用途可不同的光源,到今天学习的环境光,使场景内有了一轮太阳,尽管这轮太阳是天空盒材质的,不过不影响我们把它当成一个光源,到最后的全局光照使的场景内的环境更真实逼真,全局光照还有许多特性和新技术,需要不断的去学习接受挑战。
      作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

      发布在 中文社区 orillusion引擎 engine
      O
      oldguy
    • orillusion入门系列五 | 光照01

      回忆一下过去的学习过程,场景开辟了一处空间,可以作为容器,几何体使空间中有了物体,物体具有一些典型的形状,材质使物体得到了美化和更有真实感,今天按照惯例应该到光照了。没有光我们看不到任何物体,渲染的工作主要在对于光的处理上。
      光是由光源发出的,光在传播的过程中会经过物体的反射、折射、漫反射等多种变化,而反射回去的光又会反复的物体间反射,反射的过程中还会产生阴影,所以这是一个非常复杂的过程。我们从典型的光源组件开始,由浅入深的使用 orillusion 引擎尝试下对光的操作。

      光源组件

      观察我们的生活环境我们被各种光包围,orillusion封装了常见的三种光源组件,使用时创建 Object3D类型的对象,然后将光源组件添加至刚创建的对象中,就可以实现典型的光源效果。
      光源组件的用法可以参照以下示例:
      // 创建对象,对象是组件的容器

      let obj= new Object3D();
      // 将对象添加至场景
      scene.addChild(obj);
      // 添加一个光源组件至对象中,我们这里以平行光为例
      let light= obj.addComponent(DirectLight);
      

      平行光

      光顾名思义平行光是一种光线的角度相同,不同位置的光照强度相机的光,自然界中最常见到的平行光源是太阳。在场景中为了影响整体的光影效果,一般引擎会提供平行光功能。
      平行光一般具有以下的参数:

      • lightColor(光照颜色): 每个平行光都必须有一定颜色,这里如果不指定会又白色作为默认颜色,需要注意的是目标物体所呈现出来的颜色还和物体本身的材质有关。
      • intensity(光照强度):用来调节光线的强弱,毫无疑问光线强度越大光线越充足,越弱则越暗,如果完全没有光,场景内将陷入黑暗,通常除了光源还会有环境光,所以默认情况下不会是漆黑一片。
      • castShadow(开启阴影):只有开启阴影的光才会产生阴影,同时接收阴影的物体本身也要打开对应的参数才能看到阴影效果。
      • 方向:这不是平行光本身的参数了,这里必须要强调一点,平行光是有方向的,平行光的方向是由组件所对应的对象调节的,对象本身的旋转属性(rotationX、rotationY、rotationZ),用来调节对象所包含的光照组件方向,下面点光源和聚光灯类似。
        这里罗列一下关键的代码:
      // 创建一个容器对象
      let lightObj = new Object3D();
      //类中声明一个光源对象,这里初始化组件实例
      this.directLight = lightObj.addComponent(DirectLight);
      // 为了观察效果,打开了阴影
      this.directLight.castShadow = true;
      // 将对象添加到场景中
      this.scene.addChild(lightObj);
      
      // 定义了调整的参数结构
      const lightInfo = {
          intensity:1.0,
          rotationX:0,
          rotationY:0,
          rotationZ:0,
          lightColor:'#fff',
      }
      // 这里使用data.gui来实现界面效果
      let lightFolder = gui.addFolder("Light");
      // 强度直接修改
      lightFolder.add(lightInfo, "intensity", 0, 10).step(0.1).onChange((v) => {
          console.log('intensity:', v);
          this.directLight.intensity = v;
      });
      lightFolder.add(lightInfo, "rotationX", -360, 360).onChange((v) => {
          console.log('rotationX:', v);
          this.directLight.transform.rotationX = v;
      });
      lightFolder.add(lightInfo, "rotationY", -360, 360).onChange((v) => {
          console.log('rotationY:', v);
          this.directLight.transform.rotationY = v;
      });
      lightFolder.add(lightInfo, "rotationZ", -360, 360).onChange((v) => {
          console.log('rotationZ:', v);
          this.directLight.transform.rotationZ = v;
      });
      lightFolder.addColor(lightInfo, "lightColor").onChange((v) => {
          console.log('lightColor:', v);
          let color: Color = new Color();
          color.setHex(v);
          this.directLight.lightColor = color;
      });
      

      修改完后可以在 canary 中直接运行,查看效果如下:
      2338c3cc-7ee6-46c2-b46b-3e2c27321021-image.png
      这个示例里面除了我们重点关注的平行光以外,还有阴影,反射等等,下面会一一介绍。使用思路仍然是通过熟悉 orilluson 的api,调用不同的接口与参数即可。
      这里可以看到,没有做位置的变换操作,在当前的版本调整位置是没有效果的,调整强度参数能够看到光线的强度随着数值而动态变化。

      点光源

      有了前面最好理解的平行光作为铺垫我们已经熟悉了光源对象的使用,点光源就是另种特性的光源,类似于萤火虫、没有灯罩的白炽灯这样的发光体,本身是一个点,向四面八方发射光线。
      点光源有更多的属性,除了平行光的属性外,罗列一下点光源所特有的属性:

      • range(距离):从光源到光线衰减到0的最远距离,该值越大能够发出光线受到影响的范围越大。
      • radius(半径):光照的最亮那部分的半径。
      • at(衰减系数):光照的效果根据系数有所衰减。
      • 位置:点光源由于受距离的影响,不同距离显示的效果不同,所以位置起着重要的作用。与平行光的旋转类似,点光源的位移是由对应的对象所控制。
        点光源的调用关键代码如下:
      // 创建一个光源对象
      let lightObj = new Object3D();
      
      // 由于点光源的位置影响光照效果,所以附加一个球形物体用来标记位置 
      let mr = lightObj.addComponent(MeshRenderer);
      mr.geometry = new SphereGeometry(0.5, 10, 10);;
      mr.material = new HDRLitMaterial();
      
      //创建点光源组件
      this.lightLight = lightObj.addComponent(PointLight);
      // 打开阴影效果
      this.lightLight.castShadow = true;
      // 光源对象添加到场景
      this.scene.addChild(lightObj);
      // 设置光源的位置 
      lightObj.y = 10;
      
      // 点光源可以使用的参数
      const lightInfo = {
          intensity:1.0,
          range:0,
          radius:0,
          at:0,
          x:0,
          y:0,
          z:0,
          rotationX:0,
          rotationY:0,
          rotationZ:0,
          lightColor:'#fff',
      }
      
      // 对各参数进行设置
      let lightFolder = gui.addFolder("Light");
      lightFolder.add(lightInfo, "intensity", 0, 10).step(0.1).onChange((v) => {
          console.log('intensity:', v);
          this.lightLight.intensity = v;
      });
      lightFolder.add(lightInfo, "range", 0, 10).step(0.1).onChange((v) => {
          console.log('range:', v);
          this.lightLight.range = v;
      });
      lightFolder.add(lightInfo, "radius", 0, 10).step(0.1).onChange((v) => {
          console.log('radius:', v);
          this.lightLight.radius = v;
      });
      lightFolder.add(lightInfo, "at", 0, 10).step(0.1).onChange((v) => {
          console.log('at:', v);
          this.lightLight.at = v;
      });
      lightFolder.add(lightInfo, "x", -10, 10).onChange((v) => {
          console.log('x:', v);
          this.lightLight.transform.x = v;
      });
      lightFolder.add(lightInfo, "y", -10, 10).onChange((v) => {
          console.log('y:', v);
          this.lightLight.transform.y = v;
      });
      lightFolder.add(lightInfo, "z", -10, 10).onChange((v) => {
          console.log('z:', v);
          this.lightLight.transform.z = v;
      });
      lightFolder.add(lightInfo, "rotationX", -360, 360).onChange((v) => {
          console.log('rotationX:', v);
          this.lightLight.transform.rotationX = v;
      });
      lightFolder.add(lightInfo, "rotationY", -360, 360).onChange((v) => {
          console.log('rotationY:', v);
          this.lightLight.transform.rotationY = v;
      });
      lightFolder.add(lightInfo, "rotationZ", -360, 360).onChange((v) => {
          console.log('rotationZ:', v);
          this.lightLight.transform.rotationZ = v;
      });
      lightFolder.addColor(lightInfo, "lightColor").onChange((v) => {
          console.log('lightColor:', v);
          let color: Color = new Color();
          color.setHex(v);
          this.lightLight.lightColor = color;
      });
      

      以上代码运行的效果如下:
      71ddfcd4-9203-4e21-bfed-a8fe640de169-image.png
      可以看到点光源是一个球形的光照效果,一般用来模拟点状的光源,计算量大于平行光源。因为点光源向各个方向的光线强弱相同,所以调整方向不会有效果,但是位置的作用非常明显。
      建议实现这个代码,实际进行操作可以加强印象和理解。

      聚光灯

      聚光灯与点光源非常类似,与点光源相比聚光灯只显示一个椎体范围内的效果,通常用来模拟手动筒,探照灯等这些发光体。
      聚光灯所支持的参数除点光源以外还有以下几个:

      • innerAngle(内切角):光锥内切角,聚光在小于这个角度的范围内有光线
      • outerAngle(外切角):光锥外切角,光线会在内切角到外切角的范围内逐步衰减到0
      • 位置与方向:聚光灯光源受位置和方向双重影响,所以可以通过对应的对象调整位置与方向。
        关键调用示例代码如下:
      // 创建光照,应该已经很熟了吧,不做过多解释
      let sp = new SphereGeometry(0.5, 10, 10);
      
      let lightObj = new Object3D();
      
      let mr = lightObj.addComponent(MeshRenderer);
      mr.geometry = sp;
      mr.material = new HDRLitMaterial();
      
      this.spotLight = lightObj.addComponent(SpotLight);
      this.spotLight.castShadow = true;
      this.scene.addChild(lightObj);
      lightObj.y = 10;
      
      // 增加界面操作
       const lightInfo = {
          intensity:1.0,
          range:0,
          radius:0,
          innerAngle:0,
          outerAngle:0,
          at:0,
          x:0,
          y:0,
          z:0,
          rotationX:0,
          rotationY:0,
          rotationZ:0,
          lightColor:'#fff',
      }
      
      let lightFolder = gui.addFolder("Light");
      lightFolder.add(lightInfo, "intensity", 0, 10).step(0.1).onChange((v) => {
          console.log('intensity:', v);
          this.spotLight.intensity = v;
      });
      lightFolder.add(lightInfo, "range", 0, 100).step(0.1).onChange((v) => {
          console.log('range:', v);
          this.spotLight.range = v;
      });
      lightFolder.add(lightInfo, "radius", 0, 10).step(0.1).onChange((v) => {
          console.log('radius:', v);
          this.spotLight.radius = v;
      });
      lightFolder.add(lightInfo, "at", 0, 10).step(0.1).onChange((v) => {
          console.log('at:', v);
          this.spotLight.at = v;
      });
      lightFolder.add(lightInfo, "innerAngle", 0, 100).step(0.1).onChange((v) => {
          console.log('innerAngle:', v);
          this.spotLight.innerAngle = v;
      });
      lightFolder.add(lightInfo, "outerAngle", 0, 100).step(0.1).onChange((v) => {
          console.log('outerAngle:', v);
          this.spotLight.outerAngle = v;
      });
      lightFolder.add(lightInfo, "x", -10, 10).onChange((v) => {
          console.log('x:', v);
          this.spotLight.transform.x = v;
      });
      lightFolder.add(lightInfo, "y", -10, 10).onChange((v) => {
          console.log('y:', v);
          this.spotLight.transform.y = v;
      });
      lightFolder.add(lightInfo, "z", -10, 10).onChange((v) => {
          console.log('z:', v);
          this.spotLight.transform.z = v;
      });
      lightFolder.add(lightInfo, "rotationX", -360, 360).onChange((v) => {
          console.log('rotationX:', v);
          this.spotLight.transform.rotationX = v;
      });
      lightFolder.add(lightInfo, "rotationY", -360, 360).onChange((v) => {
          console.log('rotationY:', v);
          this.spotLight.transform.rotationY = v;
      });
      lightFolder.add(lightInfo, "rotationZ", -360, 360).onChange((v) => {
          console.log('rotationZ:', v);
          this.spotLight.transform.rotationZ = v;
      });
      lightFolder.addColor(lightInfo, "lightColor").onChange((v) => {
          console.log('lightColor:', v);
          let color: Color = new Color();
          color.setHex(v);
          this.spotLight.lightColor = color;
      });
      

      运行界面效果如下:
      1850f7b0-3fe6-4e40-8e1f-a6ac07dca197-image.png

      可以将聚光灯当成一个手电筒玩耍,多操作界面操作,会很快熟悉这种光源的。

      其它

      orilluson对功能模块进行了重新划分,Stats模块需要单独安装,执行命令 npm install @orillusion/stats --save进行安装,引入代码修改为import { Stats } from "@orillusion/stats";其它不变。

      小结

      这是第一次接触光照的特性,所以从最简单易于理解的三种光源来着手,orilluson提供的这三类光源非常方便,为我接触3D的光照提供了一个很好的切入点。后面要继续熟悉环境光,全局光照,以前阴影等等。
      作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

      发布在 中文社区 orillusion引擎 engine
      O
      oldguy
    • orillusion入门系列四 | 材质

      如果说人类最难以满足的两个器官是舌头和眼睛,舌头费调料眼睛费显卡,你同意吗?今天继续学习如何愉悦我们视觉的技术,那就是材质(Material)。在上一期的示例已经可以创造几种规则的几何体了,但是看起来光秃秃的,这些几何体很难称之为物体,今天我们从认识材质开始,到使用材质使物体的外观丰富起来。

      什么是材质(Material)

      材质是由shader定义的一组特性集合,用来定义对光的处理。光对于材质来说起着决定的作用,当没有光时我们看不到任何物体,我们视觉看到的物体是由光来决定的,光照射到物体的表面会产生反射、折射,物体也会吸收一部分,当然这些反射又会照射到其它物体上又会产生反复的吸收以及折射,那么如何在计算性能和逼真度上做取舍呢,这里不做原理上的探究,只聊一聊感性上的体会。一个立方体看起来是一块砖头还是一块金锭,是由不同的材质定义的,材质可以使物体看起来更像什么,以及是否足够逼真。

      材质、贴图、纹理的关系

      如果只有材质还是比较好理解的,忽然面向三个相近的概念需要在理解上做个区分。
      笼统的来说材质包含贴图,贴图包含纹理。材质是一个集合,内部包含了光学特性,贴图资源等等。贴图是物理的表面,可以对图片视频等效果的处理,例如拉伸,缩放,与物体的贴合参数等。纹理可以简单理解成图片视频等原材料。

      材质类型

      材质的类型是引擎给我们提供的又一有力工具,类同于前面引擎封装的几款几何体,引擎同样为实际应用中的几种典型材质做了封装。在使用引擎开发时选择与实际需要最贴近类型的材质组件就可以,材质类型是可以自定义的,我们先从熟悉引擎提供的材质开始。
      还记得前文介绍的网格(Mesh)吗,每个网格至少由几何数据和材质数据组成,不同的类型对应不同的材质类,需要对应的材质类型直接实例化该类型的材质,赋值于网络的材质属性。

      UnLitMaterial(无光材质)

      这种材质可以简单称为无光材质,忽略所有的光照的计算,所以计算性能非常高,可以作为背景使用,由于没有光的影响也可以作为一些不受光影响的特殊效果。

      支持的参数

      这里试用几个参数,更多的参数还要看官方文档。

      • baseColor(基础颜色):对材质的基础颜色进行设置,设置后覆盖了该材质的物体看起来会叠加上基础颜色。
      let color:Color = new Color();
      let material:UnLitMaterial = new UnLitMaterial();
      material.baseColor = color;
      
      • baseMap(基础贴图):对材质的基础贴图进行设置,设置后覆盖了该材质的物体会由该贴图覆盖

      完成代码

      前几次我们对一个立方体非常熟悉了,今天我们仍然创建一个立方体,但是使用UnLitMaterial材质,并覆盖一张贴图,同时可以动态的调整基础颜色。

      import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, MeshRenderer, BoxGeometry, HDRLitMaterial, ForwardRenderJob, Stats, UnLitMaterial, Color } from "@orillusion/core";
      import * as dat from 'dat.gui';
      
      export default class Unlit {
      
          cameraObj: Object3D;
      
          camera: Camera3D;
      
          scene: Scene3D;
      
          boxObj: Object3D;
      
          material:UnLitMaterial;
      
          async run() {
      
              await this.init();
      
              await this.setup();
      
              await this.start();
          }
      
          /***
           * 配置并初始化引擎
           */
          private async init() {
              // 初始化引擎
              await Engine3D.init();
          }
      
          /**
           * 引擎功能代码
           */
          private async setup() {
              // 创建一个场景
              this.scene = new Scene3D();
      
              // 添加性能监控面板
              this.scene.addComponent(Stats);
              // 创建一个相机
              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);
      
              this.createBox();
      
              this.addGUI();
      
          }
      
          /**
           * 启动渲染
           */
          private async start() {
              // 创建前向渲染
              let renderJob: ForwardRenderJob = new ForwardRenderJob(this.scene);
              // 开始渲染
              Engine3D.startRender(renderJob);
          }
      
          /**
           * 创建立方体
           */
          private async createBox() {
              // 创建一个对象
              this.boxObj = new Object3D();
              // 挂载脚本
              // 创建渲染组件
              let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
              // 设置形状
              mr.geometry = new BoxGeometry(5, 5, 5);
      
              this.material = new UnLitMaterial();
      
              // 设置基础贴图
              this.material.baseMap = await Engine3D.res.loadTexture('textures/diffuse.jpg');
              // 设置材质
              mr.material = this.material;
              // 添加到场景
              this.scene.addChild(this.boxObj);
      
          }
      
          private async addGUI(){
              // 创建 dat 实例
              const gui = new dat.GUI();
              // 创建保存属性值对象
              const materialInfo = {
                  baseColor:'#fff',
              };
      
              // 设置材质颜色
              gui.addColor(materialInfo, "baseColor").onChange((v) => {
                  console.log('baseColor:', v);
                  let color:Color = new Color();
                  color.setHex(v);
                  this.material.baseColor = color;
              });
          }
      
      }
      

      运行效果如下:
      e3720fc2-d2f9-478a-bf76-9c1f4139a3ff-image.png
      调整颜色会看到颜色会叠加到物体上。

      LambertMaterial(兰伯特材质)

      材质是与光分不开的,Lambert材质是与Lambert光照模型对应的,简单说一句Lambert是一种经验性质的简化的光照模型,据说只处理漫反射,其它类型全忽略,因此Lambert只能模拟粗糙的物体表面,像镜面类的完全不适合,如果是衣料,石头之类的表面粗糙的非常适用。

      支持的参数

      与Unlit类同。

      完成代码

      代码与Unlit也是类同的,只是把材质的类型由UnlitMaterial换成LambertMaterial,这里就不占用篇幅了。
      看一下基础的运行效果:
      31a588c4-04ef-46f5-a597-48c7733a52b1-image.png

      HDRLitMaterial(物理材质)

      能看到Lmabert比Unlit更进了一步,尽管只考虑了一种光,至少有光的参与了,可见材质也是不断的丰富起来,计算的因素多了起来,与实际效果更接近了。看到各大引擎都是主推物理材质,所以除非计算机性能实在拉垮,否则还是多熟悉使用物理材质吧,大势所趋。过去我们的例子默认也是用的这种材质。

      支持参数

      物理材质支持的参数比较多,这里我们只验证个别几个属性,更多的属性可以对着文档一一验证。

      • baseColor(基础颜色):材质本身的颜色,可以使用一个Color类型的变量设置
      • roughness(粗糙度):模拟物体表面粗糙的程度,随着数值变大物体会看起来更粗糙
      • metallic(金属度):模拟物体表面金属的程度,随着数值的变化物体会看起来更光滑

      运行效果

      使用这三个参数调节显示效果如下:
      7f4b687d-e333-4f87-9e7e-f9a0b9bf0608-image.png

      综合示例

      不同的材质主要还是对光的处理不同,这里我们写一个综合的实例,将三种材质放到一个场景下,在一个聚光灯照射下观察效果,这里我们提前借用一下光源的组件,可以暂时不必在意,后续会专门介绍。

      示例代码

      import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, MeshRenderer, BoxGeometry, HDRLitMaterial, ForwardRenderJob, Stats, Color, LambertMaterial, UnLitMaterial, Vector3, MaterialBase, SpotLight, SphereGeometry, defaultTexture } from "@orillusion/core";
      import * as dat from 'dat.gui';
      
      export default class Materials {
      
          spotLight: SpotLight;
      
          cameraObj: Object3D;
      
          camera: Camera3D;
      
          scene: Scene3D;
      
          boxObj: Object3D;
      
          hdrMaterial: HDRLitMaterial;
          lambertMaterial: LambertMaterial;
          unlitMaterial: UnLitMaterial;
      
          async run() {
      
              await this.init();
              await this.setup();
      
              await this.start();
          }
      
          /***
           * 配置并初始化引擎
           */
          private async init() {
              // 初始化引擎
              await Engine3D.init();
          }
      
          /**
           * 引擎功能代码
           */
          private async setup() {
              // 创建一个场景
              this.scene = new Scene3D();
      
              // 添加性能监控面板
              this.scene.addComponent(Stats);
              // 创建一个相机
              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);
      
              this.hdrMaterial = new HDRLitMaterial();
              this.lambertMaterial = new LambertMaterial();
              this.unlitMaterial = new UnLitMaterial();
      
              this.createBox(new Vector3(-10, 0, 0), this.unlitMaterial);
              this.createBox(new Vector3(0, 0, 0), this.lambertMaterial);
              this.createBox(new Vector3(10, 0, 0), this.hdrMaterial);
      
              this.createLight();
              this.createFloor();
      
              this.addGUI();
      
          }
      
          /**
           * 启动渲染
           */
          private async start() {
              // 创建前向渲染
              let renderJob: ForwardRenderJob = new ForwardRenderJob(this.scene);
              // 开始渲染
              Engine3D.startRender(renderJob);
          }
      
          private async createLight() {
              let sp = new SphereGeometry(0.5, 10, 10);
      
              let spotLightObj = new Object3D();
              let mr = spotLightObj.addComponent(MeshRenderer);
              mr.geometry = sp;
              mr.material = new HDRLitMaterial();
      
              this.spotLight = spotLightObj.addComponent(SpotLight);
              this.scene.addChild(spotLightObj);
              spotLightObj.y = 10;
      
          }
      
          /**
           * 创建立方体
           */
          private async createBox(location: Vector3, matrix: MaterialBase) {
              // 创建一个对象
              this.boxObj = new Object3D();
              this.boxObj.localPosition = location;
              // 创建渲染组件
              let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
              // 设置形状
              mr.geometry = new BoxGeometry(5, 5, 5);
      
              // 设置材质
              mr.material = matrix;
              // 添加到场景
              this.scene.addChild(this.boxObj);
      
          }
      
          private async createFloor(){
              let floor = new Object3D();
              floor.y = -2.5;
              let mat = new HDRLitMaterial();
              mat.baseMap = defaultTexture.grayTexture;
              let mr = floor.addComponent(MeshRenderer);
              mr.geometry = new BoxGeometry(40, 1, 30);
              mr.material = mat;
              this.scene.addChild(floor);
          }
      
          private async addGUI() {
              // 创建 dat 实例
              const gui = new dat.GUI();
      
              const lightInfo = {
                  x:0,
                  y:0,
                  z:0,
                  rotationX:0,
                  rotationY:0,
                  rotationZ:0,
                  lightColor:'#fff',
              }
      
              let lightFolder = gui.addFolder("Light");
              lightFolder.add(lightInfo, "x", -10, 10).onChange((v) => {
                  console.log('x:', v);
                  this.spotLight.transform.x = v;
              });
              lightFolder.add(lightInfo, "y", -10, 10).onChange((v) => {
                  console.log('y:', v);
                  this.spotLight.transform.y = v;
              });
              lightFolder.add(lightInfo, "z", -10, 10).onChange((v) => {
                  console.log('z:', v);
                  this.spotLight.transform.z = v;
              });
              lightFolder.add(lightInfo, "rotationX", -360, 360).onChange((v) => {
                  console.log('rotationX:', v);
                  this.spotLight.transform.rotationX = v;
              });
              lightFolder.add(lightInfo, "rotationY", -360, 360).onChange((v) => {
                  console.log('rotationY:', v);
                  this.spotLight.transform.rotationY = v;
              });
              lightFolder.add(lightInfo, "rotationZ", -360, 360).onChange((v) => {
                  console.log('rotationZ:', v);
                  this.spotLight.transform.rotationZ = v;
              });
              lightFolder.addColor(lightInfo, "lightColor").onChange((v) => {
                  console.log('lightColor:', v);
                  let color: Color = new Color();
                  color.setHex(v);
                  this.spotLight.lightColor = color;
              });
      
              // 创建保存属性值对象
              const unlitInfo = {
                  baseColor: '#fff'
      
              };
      
              let unlitFolder = gui.addFolder("UnlitMaterial");
              // 设置材质颜色
              unlitFolder.addColor(unlitInfo, "baseColor").onChange((v) => {
                  console.log('baseColor:', v);
                  let color: Color = new Color();
                  color.setHex(v);
                  this.unlitMaterial.baseColor = color;
              });
      
              // 创建保存属性值对象
              const lambertInfo = {
                  baseColor: '#fff'
      
              };
      
              let lambertFolder = gui.addFolder("LambertMaterial");
              // 设置材质颜色
              lambertFolder.addColor(lambertInfo, "baseColor").onChange((v) => {
                  console.log('baseColor:', v);
                  let color: Color = new Color();
                  color.setHex(v);
                  this.lambertMaterial.baseColor = color;
              });
      
              // 创建保存属性值对象
              const pdrInfo = {
                  baseColor: '#fff',
                  roughness: 0.01,
                  metallic: 0.01,
      
              };
      
              let hdrFolder = gui.addFolder("PDRMaterial");
              // 设置材质颜色
              hdrFolder.addColor(pdrInfo, "baseColor").onChange((v) => {
                  console.log('baseColor:', v);
                  let color: Color = new Color();
                  color.setHex(v);
                  this.hdrMaterial.baseColor = color;
              });
      
              // 材质粗糙程度
              hdrFolder.add(pdrInfo, "roughness", 0, 1).onChange((v) => {
                  console.log('roughness:', v);
                  this.hdrMaterial.roughness = v;
              });
      
              // 材质粗糙程度
              hdrFolder.add(pdrInfo, "metallic", 0, 1).onChange((v) => {
                  console.log('metallic:', v);
                  this.hdrMaterial.metallic = v;
              });
      
          }
      
      }
      

      运行效果

      718ac9eb-6100-49cb-b7a6-34446ab03a37-image.png

      小结

      继几何体后,引擎对常用材质做了封装,使用户可以不必自己编写shader,创建一个对应的类,设置参数就可以得到一个可用的材质,用来丰富物体。材质的种类还有一些,不过当前只发现了三种,期待后续会有更多的材质可以使用。
      作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

      发布在 中文社区 orillusion引擎 engine
      O
      oldguy
    • orillusion入门系列三 | 几何体

      前两次的学习总结中,成功运行了一个最基础的功能,对引擎的的整体代码进行了归纳,根据我的习惯提供了一个并不严谨的开发步骤,并且介绍了两个工具。主要解决了认识引擎以及如何开始使用引擎。今天我准备学习使用引擎开发真正的功能了,在这里继续使用大白话(水平有限只能如此)介绍学习过程。
      专业的3D系统开发需要符合软件工程实践,只以像我这样的小白视角以入门为目的提供一个思路,我们前面了解到首先要通过场景开辟一个空间,加上相机开启上帝视角,那么之后呢?我认为是“造物”,一个3D空间无论复杂还是简单,都可以看成是一个虚拟世界,是对真实世界的理解的反映,在真实世界中我们能看到的物体是由点、线、面组成的,在3D场景中最基础的是由点组成的,3D中的点由三个坐标(x、y、z)分量构成的,GPU在处理时一般是按照三角型处理的,这些点我们一般称为顶点。在3D中的一座山、一条路、一个篮球,都是由若干的顶点构成的。但是只有顶点是不够的,每个物体还会有不同的外观,我们一般用材质来表示,材质对物体的外观进行装饰成不同的样式。所以顶点+材质就组成了一个物体,这个物体我们一般称为网格(mesh),今天我们先忽略材质,专注于造物。

      几何体

      面向3D的世界我还是学前班阶段,所以我们从最基础的几何体开始。orillusion目前提供了四种几何体可以直接使用,并不算多,实践中肯定会需要更多的类型吧,期待后续会增加,这里先熟悉这四种,对于了解几何体是足够的了。

      长方体

      长方体是我的最爱,我们在第一天就熟悉了如何使用长方体,一个长方体由宽度、高度、深度三个分量组成,这里我们复习一下如何使用

      // 创建一个容器对象
      this.boxObj = new Object3D();
      // 创建渲染组件
      let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
      // 创建一个长方体
      mr.geometry = new BoxGeometry(5, 5, 5);
      // 设置材质
      mr.material = new HDRLitMaterial();
      // 添加到场景
      this.scene.addChild(this.boxObj);
      

      99816cf9-e104-441d-aaba-f88ab72545b9-image.png
      这里需要注意,几何体的属性在创建时通过构造函数参数指定,不支持动态的修改,动态改变形状建议使用Object3D对象的缩放属性。

      球体

      球体也是我们非常熟悉的一种几何体,创建球体必须要指定半径,以及水平和垂直分段数。

      // 创建一个对象
      this.boxObj = new Object3D();
      // 创建渲染组件
      let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
      // 设置球体的实例
      mr.geometry = new SphereGeometry(3, 100, 100);
      // 设置材质
      mr.material = new HDRLitMaterial();
      // 添加到场景
      this.scene.addChild(this.boxObj);
      

      运行效果如下
      a3cd9adc-1d7f-4789-9e22-1081a398f3f0-image.png

      平面

      创建一个平台我们至少需要指定长和宽两个属性

      // 创建一个对象
      this.boxObj = new Object3D();
      // 创建渲染组件
      let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
      // 设置形状
      mr.geometry = new PlaneGeometry(10, 10);
      // 设置材质
      mr.material = new HDRLitMaterial();
      // 添加到场景
      this.scene.addChild(this.boxObj);
      

      运行效果如下
      512a8267-23ee-4f83-b539-0e4c71d95128-image.png

      圆柱体

      我们这里创建一个半径为5,高为10的圆柱体

      // 创建一个对象
      this.boxObj = new Object3D();
      // 创建渲染组件
      let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
      // 设置形状
      mr.geometry = new CylinderGeometry(5, 5, 10);
      // 设置材质
      mr.material = new HDRLitMaterial();
      // 添加到场景
      this.scene.addChild(this.boxObj);
      

      运行效果如下
      99c32a9a-92a1-4819-84a9-a8e30506b1b0-image.png

      Object3D 对象

      我们不是第一次接触Object3D对象了,这里我们通过实例用一下这个组件,我们以长方体为例,长方体是通过组件的方式添加到一个Object3D容器中的,所以通过Object3D可以操作立方体的属性。

      位置

      分别可以直接读写对象的x、y、z坐标。

      this.boxObj.x = v;
      this.boxObj.y = v;
      this.boxObj.z = v;
      

      也可以通过 Vector3 类型一次性设置三个分量,这里不作演示了

      旋转

      分别可以直接读写对象x、y、z三个方向的旋转角度。

      this.boxObj.rotationX = v;
      this.boxObj.rotationy = v;
      this.boxObj.rotationz = v;
      

      也可以通过 Vector3 类型一次性设置三个分量,这里不作演示了

      缩放

      分别可以直接读写对象x、y、z三个方向的缩放。

      this.boxObj.scaleX = v;
      this.boxObj.scaleY = v;
      this.boxObj.scaleZ = v;
      

      也可以通过 Vector3 类型一次性设置三个分量,这里不作演示了

      动态演示或隐藏

      设置对象的 visible 属性可以对对象演示或隐藏

      this.boxObj.visible = v;
      

      使用脚本

      前面创建的物体看起来很死板,用鼠标操作一下才会动,如何让这个自己有一定的行为呢。那要请出组件了。组件一般用来封装可复用的功能,添加至不同的Object3D中,引擎内部会自动调用该组件的功能作用在这个容器对象中。
      这里我们看一个最基本的组件脚本的模板:

      class Script extends ComponentBase {
        // 覆写 初始化
        public init(){
          // 该函数在组件被创建时调用,可以用来初始化内部的变量
          // 注意,此时组件被挂载到 Object3D 上,所以无法访问 this.object3D
        }
        // 覆写 渲染开始
        public start(){
          // 该函数在组件开始渲染前被调用,
          // 一颗访问 this.object3D, 通常用来获取节点的属性或其他组件
        }
        // 覆写 update
        public update() {
          // 每帧渲染循环调用,通常定义节点的循环逻辑
          // 例如,每一帧更新物体旋转角度
          this.object3D.rotationY += 1;
        }
      }
      

      以上模板的注释说明了用法,这里我准备做一个自动旋转的功能,只要实现 update 函数就可以做到。

      update(): void {
        // 旋转
        this.transform.rotationX = Math.sin(Time.time * 0.001) * 100.0;
      }
      

      之后将该组件添加至boxObj中,看来需要熟识上面的脚本模板,这几个生命周期函数非常重要,在后面定制功能时必不可少。而且除了用户代码,引擎内部也定义了一些组件,后面我们要不断去了解。

      完成代码

      这里以立方体为例,将代码罗列一下。

      import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, MeshRenderer, BoxGeometry, HDRLitMaterial, ForwardRenderJob, Stats } from "@orillusion/core";
      import * as dat from 'dat.gui';
      import { RotationScript } from "./rotationScript";
      
      export default class Geometry {
      
          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();
          }
      
          /**
           * 引擎功能代码
           */
          private async setup() {
              // 创建一个场景
              this.scene = new Scene3D();
      
              // 添加性能监控面板
              this.scene.addComponent(Stats);
              // 创建一个相机
              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);
      
              this.createBox();
      
              this.addGUI();
      
          }
      
          /**
           * 启动渲染
           */
          private async start() {
              // 创建前向渲染
              let renderJob: ForwardRenderJob = new ForwardRenderJob(this.scene);
              // 开始渲染
              Engine3D.startRender(renderJob);
          }
      
          /**
           * 创建立方体
           */
          private async createBox() {
              // 创建一个对象
              this.boxObj = new Object3D();
              // 挂载脚本
              this.boxObj.addComponent(RotationScript);
              // 创建渲染组件
              let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
              // 设置形状
              mr.geometry = new BoxGeometry(5, 5, 5);
              // 设置材质
              mr.material = new HDRLitMaterial();
              // 添加到场景
              this.scene.addChild(this.boxObj);
      
          }
      
          private async addGUI(){
              // 创建 dat 实例
              const gui = new dat.GUI();
              // 创建保存属性值对象
              const geometryInfo = {
                  enable:true,
                  visible:true,
                  rotationX:0,
                  rotationY:0,
                  rotationZ:0,
                  scaleX:0,
                  scaleY:0,
                  scaleZ:9,
                  x: 0,
                  y: 0,
                  z: 0,
              };
      
              // 创建一个x坐标事件监听,当修改x值时,直接修改立方体坐标。
              gui.add(geometryInfo, "enable").onChange((v) => {
                  console.log('enable:', v);
                  this.boxObj.enable = v;
              });
              gui.add(geometryInfo, "visible").onChange((v) => {
                  console.log('visible:', v);
                  this.boxObj.visible = v;
              });
              gui.add(geometryInfo, "x", -100, 100).step(0.1).onChange((v) => {
                  console.log('x:', v);
                  this.boxObj.x = v;
              });
              gui.add(geometryInfo, "y", -100, 100).step(0.1).onChange((v) => {
                  console.log('y:', v);
                  this.boxObj.y = v;
              });
              gui.add(geometryInfo, "z", -100, 100).step(0.1).onChange((v) => {
                  console.log('z:', v);
                  this.boxObj.z = v;
              });
              gui.add(geometryInfo, "rotationX", -100, 100).step(0.1).onChange((v) => {
                  console.log('rotationX:', v);
                  this.boxObj.rotationX = v;
              });
              gui.add(geometryInfo, "rotationY", -100, 100).step(0.1).onChange((v) => {
                  console.log('rotationY:', v);
                  this.boxObj.rotationY = v;
              });
              gui.add(geometryInfo, "rotationZ", -100, 100).step(0.1).onChange((v) => {
                  console.log('rotationZ:', v);
                  this.boxObj.rotationZ = v;
              });
      
              gui.add(geometryInfo, "scaleX", 0, 10).step(0.1).onChange((v) => {
                  console.log('scaleX:', v);
                  this.boxObj.scaleX = v;
              });
              gui.add(geometryInfo, "scaleY", 0, 10).step(0.1).onChange((v) => {
                  console.log('scaleY:', v);
                  this.boxObj.scaleY = v;
              });
              gui.add(geometryInfo, "scaleZ", 0, 10).step(0.1).onChange((v) => {
                  console.log('scaleZ:', v);
                  this.boxObj.scaleZ = v;
              });
          }
      
      }
      
      

      脚本完整代码

      import { ComponentBase, Time } from "@orillusion/core";
      
      export class RotationScript extends ComponentBase {
      
          update(): void {
            // 旋转
            this.transform.rotationX = Math.sin(Time.time * 0.001) * 100.0;
          }
        }
        
      

      668c248c-7592-4935-8ed4-76b70e37e3b9-image.png

      因为加上了脚本,运行时能够看到这个方立体在自动旋转。右侧的操作面板做对应的操作也可以看到变化。

      小结

      今天学习了最基本的功能,没有按照通常的3D学习顺序,这里只按照最朴素(土)的习惯性思维来介绍。现在我们学会了造物,后续要继续学习如何装扮这些物体。
      看到了社区的反馈,有大佬对使用vue框架持提出质疑,瑟瑟发抖中,后面略去界面库部分。
      作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

      发布在 中文社区 orillusion引擎 engine
      O
      oldguy
    • orillusion入门系列二 | 快速入门

      前文我们自己动手使用引擎体验到了3D的效果,今天我们尝试梳理一下使用引擎做3D开发从何处入手,快速的从整体上了解如何使用 orillusion 开发3D项目。

      引擎的挑战

      3D是一项面向视觉的技术,为了获得更逼真、更炫酷、更流畅的体验,需要极强的理论根基和工程实践经验。建议先粗略的看看引擎所要面对的一些实际问题,聊一下对引擎浮浅的认识,以便我们能更好的理解如何使用引擎。

      • 从贵族到平民 数学是构建3D世界的支柱,3D效果的发展离不开对数学和算法的深刻理解和应用,比如为了体现光线的真实感,为了实现服装飘逸的感觉,都有许多个数学公式作为底层支撑。另一方面需要充分了解硬件的特性,这都是非常耗费时间的工作,引擎使得没有这方面基础的普通开发者集中时间在实现需求上。
      • 从贪婪到克制 无论硬件是否会继续以遵循摩尔定律的规律提升,引擎要以足够贪婪的程度榨取硬件的性能,使硬件上的投资获得回报,同时要克制自身对资源的占用,即要又要的典范。
      • 从混乱到秩序 引擎所面向的世界是多方面的,不同的硬件之间如何协调,多变的网络环境,多种资源处理,稳定灵活高效的任务调度等等,在引擎的帮助下有条不紊的组织在一起。
      • 始终面带微笑 引擎要发挥出足够的作用,必须要让用户足够容易上手,提供给用户要有足够友好的api,还有完备的工具链和文档,所以引擎要提供尽可能的友好使用体验。

      使用引擎

      因为以上罗列的一些原因,引擎需要需要做许多取舍,在使用引擎的时候也有一些固定的要求。大体上我们分为搭建环境、引擎配置、引擎初始化、开启渲染任务这几个步骤。

      搭建环境

      Orillusion 支持多种方式安装,具体可以参考官方文档,上一篇文章使用了 vue&vite 系列的开发环境,这里假设您已经进行了安装。

      引擎配置

      引擎为了满足多样的需求和不同的环境,提供了许多配置选项,一般代码的最初位置进行引擎配置,后续的初始化和运行需要依赖配置项。这里只列出两条配置示例,更多的配置项可以参考官方文档。

      // 最大实体数量
      Engine3D.engineSetting.memorySetting.doMatrixMaxCount = 100000;
      // 最大灯光数量
      Engine3D.engineSetting.lightSetting.maxLight = 1024;
      

      初始化

      引擎进行了配置之后,需要进行初始化,在初始化这一步可以根据需求申请必要的内存。这里我们可以配置 canvas变量,也可以指定渲染前回调、每一帧回调以前每一帧渲染后的回调。

      // 先配置,后初始化
      await Engine3D.init({
          canvasConfig:{
              // 配置 cavans
              canvas: document.getElementById("webGpuCanvas"),
              alpha: false,
              zIndex: 1
          },
          beforeRender: ()=>{
              // 每一帧渲染前回调
          },
          renderLoop: ()=>{
              // 每一帧回调
          },
          lateRender: ()=>{
              // 每一帧渲染后回调
          }
      });
      

      我们目前没有特别的功能需求,一般用最简化的初始化代码。

      await Engine3D.init();
      

      功能代码

      编写一个相对简单的3D程序,我们可以简单的将过程理解为几个步骤:

      1. 开辟一个3D空间:创建一个场景,相当于一个3D空间,后续的资源可以添加到这个场景中。
      // 创建一个场景
      let scene3D: Scene3D = new Scene3D();
      
      1. 设置一个观察点:这个观察点我们一般用相机组件来体现,所有看到的场景内的信息都是通过相机组件的角度实现的,相机支持一系列的配置参数和方法,后续我们会有专门的章节来学习,同时该相机也要添加至场景中。
      // 创建一个实体对象,用来管理相机组件
      let cameraObj: Object3D = new Object3D();
      // 向 相机 对象内添加相机组件 Camera3D 是我们的相机组件
      let camera = cameraObj.addComponent(Camera3D);
      
      // 相机的配置略
      ...
      
      // 添加相机至场景
      scene3D.addChild(cameraObj);
      
      1. 添加物体:这里我们为了演示,添加了一个立方体,同样需要添加至场景中。
      // 创建一个对象
      const obj = new Object3D();
      // 创建渲染组件
      let mr = obj.addComponent(MeshRenderer);
      // 设置形状
      mr.geometry = new BoxGeometry(5, 5, 5);
      // 设置材质
      mr.material = new HDRLitMaterial();
      // 添加到场景
      scene3D.addChild(obj);
      

      开始渲染任务

      所谓渲染形象来说是,将场景内的景物,通过相机作为观察点,生成二维图像,显示在演示设备上的过程。我们这里一般显示设备是前端的canvas,所以渲染可以理解为从场景到canvas的过程,为了保持流畅,需要高速的刷动作。
      渲染方式有多种,最常见的是前向渲染,其它模式我们遇到再聊。

      // 创建一个前向渲染器
      let renderJob = new ForwardRenderJob(this.scene);
      // 后处理
      // ...
      // 开始渲染任务
      Engine3D.startRender(renderJob);
      

      后处理

      从代码上看,后处理是要放到开始渲染任务之前,但是从逻辑上这里放到最后一步,因为后处理是在生成图片后,界面显示前所做的一次纯粹的图片处理。
      后处理需要在开始渲染任务前添加在渲染器中。

      renderJob.addPost(new SSRPost());//屏幕空间反射
      renderJob.addPost(new SSAOPost());//屏幕空间SSAO
      renderJob.addPost(new GlobalFog());//屏幕空间雾化
      renderJob.addPost(new HDRBloomPost());//HDR Bloom 泛光
      renderJob.addPost(new FXAAPost());//屏幕抗锯齿
      

      后处理是个相对独立又非常有趣的功能,后续我们会专门深入了解。

      调测工具

      为了更好的观察程序的运行状态和动态的操控功能,了解两款相对应的工具很有必要。

      性能监控(Stats)

      Orillusion 官方提供的一个性能监控工具组件,可以显示引擎的运行状态,我们可能直接添加到场景中即可,默认会显示在运行界面的左上角,显示位置可以修改,这里我们使用默认设置就可以了。

      import {Stats} from '@orillusion/core'
      scene.addComponent(Stats);
      

      界面操控(dat.gui)

      一段程序在完成时,逻辑已经通过代码固定下来了,若需动态的改变程序逻辑,脱离不开设计好的输入控制和代码本身的逻辑。但是出于调试或测试的原因,需要动态的修改某个api或变量如何做呢?我们假设只通过界面来控制逻辑,orillusion 官方提供了 GUIHelp 工具类来实现这个功能,不过我们当前推荐使用 dat.gui 来做界面操控工具。

      简介

      dat.gui 是由google工程师出品的很轻量级界面操控工具,使用 javascript 实现,可以方便的集成至基于web的系统内,实现灵活的界面操控,特别适合作为3D程序观察调试程序的工具。
      dat.gui的优点显而易见:

      • 使用简单 dat.gui可以自动的操作dom节点,同步至界面显示效果,不需要我们手动繁琐的通过js来操作dom结构。
      • 轻量级 dat.gui代码量不大,不会对系统的加载造成太大的负担。
      • 功能强大 dat.gui提供了数字、逻辑、颜色、字符串、函数等几种数据类型,不同的数据类型有与之相适配的界面元素,为修改代码的变量和逻辑提供了很大的便利。

      用法

      详细的dat.gui建议查看官方或其它资料,我们这里只是简单的介绍需要用到的部分,后续随着操控部分的增加也会不断的更新,现在我们仅需要熟悉事件监听即可。
      目前支持两种事件监听类型:

      事件 触发
      onChange 值被修改时触发
      onFinishChange 失去焦点时触发

      具体代码在后续代码中一共演示。

      编写代码

      1. 添加@别名 以便代码中可以使用 @ 指代源码路径
        a. 安装node类型 npm i @types/node
        b. 修改 vite.config.ts 文件,新加 @ 别名配置
      export default defineConfig({
        resolve: {
          alias: {
            '@': join(__dirname, "src"),
          }
        },
        plugins: [vue()]
      })
      

      c. 修改 tsconfig.ts 文件,配置TypeScript类型提示,增加 baseUrl和paths配置项

      "baseUrl": ".",
      "paths": {
        "@/*": ["src/*"]
      }
      
      1. 重构目录结构
        a. 修改 demo 目录为 orilluson
        b. 在 src目录创建 views目录
        c. 在views 目录创建 Index.vue单文件组件,编辑代码如下
      <template>
      
      </template>
      
      <script lang="ts" setup>
        import { onMounted } from 'vue';
        import Hello from '@/orillusion/hello';
        onMounted(()=>{
          new Hello().run();
        })
      </script>
      
      <style scoped>
      </style>
      

      d. 重构项目根目录的App.vue文件,编辑代码如下

      <template>
        <Index></Index>
      </template>
      
      <script lang="ts" setup>
      import Index from "@/views/Index.vue";
      </script>
      
      <style scoped>
      
      </style>
      
      1. 安装 dat.gui: npm install --save dat.gui
      2. 完善 hello.ts ,主要从以下几个方面来进行
        a. 重构 Hello 类,将 函数内的临时变量声明成类成员变量
        b. 根据我们前面的引擎步骤,增加处理函数
        c. 增加 dat.gui 操作立方体的控制项
      3. 完成 hello.ts 代码
      import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, MeshRenderer, BoxGeometry, HDRLitMaterial, ForwardRenderJob, Stats } from "@orillusion/core";
      import * as dat from 'dat.gui';
      
      export default class Hello {
          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();
          }
      
          /**
           * 引擎功能代码
           */
          private async setup() {
              // 创建一个场景
              this.scene = new Scene3D();
      
              // 添加性能监控面板
              this.scene.addComponent(Stats);
              // 创建一个相机
              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);
      
              this.createBox();
              this.addGUI();
      
          }
      
          /**
           * 启动渲染
           */
          private async start() {
              // 创建前向渲染
              let renderJob: ForwardRenderJob = new ForwardRenderJob(this.scene);
              // 开始渲染
              Engine3D.startRender(renderJob);
          }
      
          /**
           * 创建立方体
           */
          private async createBox() {
              // 创建一个对象
              this.boxObj = new Object3D();
              // 创建渲染组件
              let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer);
              // 设置形状
              mr.geometry = new BoxGeometry(5, 5, 5);
              // 设置材质
              mr.material = new HDRLitMaterial();
              // 添加到场景
              this.scene.addChild(this.boxObj);
          }
      
          private async addGUI(){
              // 创建 dat 实例
              const gui = new dat.GUI();
              // 创建保存属性值对象
              const geometryInfo = {
                  x: 0,
                  y: 0,
                  z: 0
              };
      
              // 创建一个x坐标事件监听,当修改x值时,直接修改立方体坐标。
              gui.add(this.boxObj, "x", -100, 100).step(0.1).onChange((v) => {
                  console.log('x:', v);
                  this.boxObj.x = v;
              });
              gui.add(geometryInfo, "y", -100, 100).step(0.1).onChange((v) => {
                  console.log('y:', v);
                  this.boxObj.y = v;
              });
              gui.add(geometryInfo, "z", -100, 100).step(0.1).onChange((v) => {
                  console.log('z:', v);
                  this.boxObj.z = v;
              });
          }
      
      }
      

      完成项目

      今天的代码我们主要演示了两款测试工具,在左上角是性能监控面板,在右上角是操控面板,用鼠标滑动滚动条或者直接输入数值,可以分别理性立方体的x、y、z的值,物体对应的会发生移动。
      8414362f-b932-4fa1-97d3-fd5cff4010b2-image.png

      代码解析

      今天的代码在第一篇的基础上有所增加,但是以下三个类型是第二次出现了,我们不去深究更详细的原理,需要对这三个常用的对象类型有所了解。

      • Object3D 是引擎内置的实体对象,通常被当做基本的组件容器,可以通过不同组件组合来实现不同的类型功能。我的理解是,Object3D内置一些基础功能对象,还可以是功能组件的容器,比如我们这里创建一个用于管理立方体的容器boxObj。
      • MeshRenderer 是一个用于渲染网格的组件,这个组件非常重要,包含几何属性和材质属性,每个需要渲染的物体都需要有几何属性和材质属性。
      • HDRLitMaterial PBR 材质,基于物理渲染,旨在模拟现实世界光照效果,以后我们会专门来学习材质的部分,这里只要知道这是一种材质就可以。
        以上三个类型都非常重要,首先我们要创建一个Object3D作为容器,然后在这个容器中创建MeshRenderer作为渲染组件,渲染组件中需要实例化几何体和材质。就这样由容器与功能组件相互配合完成功能,当然都需要按照一定的层级结构添加至场景中。

      小结

      今天根据我的理解总结了一个引擎所必须面对的几个难点,可以更好的理解引擎的设计,同时拆解了 orilluson 代码的几个步骤,以便能够快速的上手3D代码开发。进一步的介绍了两个非常常用的调测工具,为以后自由的调用功能提供了工具。之后继续完善项目,运行项目演示效果。最后我们熟悉了最常见的三个组件。
      作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

      发布在 中文社区 orillusion引擎 engine
      O
      oldguy
    • orillusion入门系列一 | 第一印象

      做了很多年的应用开发想要在技术上有些突破,是时候学点真正的技术了,我选择了3D技术,因为接触3D技术之前我似乎是一只二维生物,只能在一个平面上思考问题,但是3D技术难度不小,所以必须找个足够高的肩膀来踩,那就要借助引擎的力量了。经过一翻搜索,orillusion 着实让我感动,满足了我所有的要求,而且还是国产的。

      为什么选择 orillusion

      没有专业的对比,仅凭我个人的好恶盲目来选择,简单从这两个方面来考虑

      Native or Web?

      如果您是专业人士建议详细对比两种类型的发展历史和原理再做出判断,对于我一个3D新手来说,有一个简单粗暴的判断思路,那就是在应用系统上,CS架构仍在某些特定领域仍然存在,但是能够迁移到BS的,几乎都在大规模的迁移到了BS架构上,所以我挺Web渲染架构。

      足够新

      新人就要有新人的觉悟,老牌的各路引擎已经有无数高手浸淫其中多年了,冒然投入进去会被沉重的历史信息淹没,作技术还是要有点野心的新手终有一天会成老鸟,所以要选择最新推出的面向最新技术架构的引擎,那么有多新呢,标准还没正式应用的够新吧,那就是以面向WebGPU为第一图形标准实现功能,至于WebGPU是什么,我的理解是更新、更快、更强,至于具体如何做到,相信以后我们会知道的。

      准备工作

      唯一必须的的准备工作是做好心理建设,不要想一口吃成个胖子,不要过早深究原理,我们的目标是借助引擎的能力来实现3D效果,除此以外还有两件小事需要留意。

      开发环境

      请先安装nodejs,并配置好环境变量,同时假设vue3已经可以正常使用。这里我使用vite作为项目创建工具,使用vscode做编辑器,没有其它的前置条件,轻装上阵。如果这一步有什么疑问,请自行查找资料,相信聪明的你不会被难住。

      运行环境

      由于WebGPU标准并未正式推出,需要使用开发浏览器,这里推荐 chrome canary ,下载后在在canary中运行效果,由于canary仍然在变动中,需要在地址栏中输入 chrome://flags/#enable-unsafe-webgpu,然后开启 Unsafe WebGPU 选项。

      c7ae164b-15fd-46fe-b723-41491446e63c-image.png
      如果无法开启或者没有效果,需要搜索最新的开启方法,相信不久的将来在正式版就可以附带WebGPU支持了,不需要这么繁琐了。
      同时为了做两手准备,也可以考虑用 Edge Canary 来应个急,下载后正常安装,启动后在地址栏中输入edge://flags/#enable-unsafe-webgpu,开启WebGPU支持。

      创建项目

      这里我们先创建一个普通的vue3项目,再将引擎引入项目,操练起来吧,很快就能看到我们在做什么了。

      1. 创建根目录
        a. 选择一个文件夹,创建orillusion目录
        b. 进入orillusion,并在该目录下打开命令提示行
      2. 创建项目
        a. 执行命令 npm create vite@latest
        b. Project name 下输入 hello3d 作为项目名并回车
        c. 使用键盘方向键选择 vue 作为框架,并回车
        d. 仍然使用键盘方向键选择 vue-ts 作为项目类型,并回车,已经创建了 hello3d 项目目录结构
        e. 输入 cd hello3d 进入项目目录
        f. 输入 npm install 安装依赖包,至此,基本的vue3项目已经创建完成
      3. 安装 orillusion
        a. 输入 npm install @orillusion/core --save 安装 orillusion 开发包(如果安装失败,请注意分辨网络原因或安装权限)
        b. 输入 npm run dev ,可以看到在5173端口启动了服务
        c. 打开 canary 浏览器,输入完整地址可以看到显示了vue3的欢迎页,至此项目已经安装完成,后面进行开发。

      编写代码

      1. 在 vscode 中打开 hello3d 目录
      2. 在 src 目录下新建目录 demo
      3. 在 demo 目录下创建文件 hello.ts
      import { Engine3D, Scene3D, Object3D, Camera3D, ForwardRenderJob, HDRLitMaterial, BoxGeometry, MeshRenderer, DirectLight, HoverCameraController, Color, Vector3 } from "@orillusion/core";
      
      export default class Hello {
          async run() {
              console.log('hello 3d');
              // 初始化引擎
              await Engine3D.init();
              // 创建一个场景
              let scene3D: Scene3D = new Scene3D();
              // 创建一个相机
              let cameraObj: Object3D = new Object3D();
              let camera = cameraObj.addComponent(Camera3D);
              // 设置相机类型
              camera.perspective(60, window.innerWidth / window.innerHeight, 1, 5000.0);
              // 设置相机控制器
              let controller = cameraObj.addComponent(HoverCameraController);
              controller.setCamera(20, -20, 25);
              // 添加相机至场景
              scene3D.addChild(cameraObj);
              // 创建一个对象
              const obj: Object3D = new Object3D();
              // 创建渲染组件
              let mr: MeshRenderer = obj.addComponent(MeshRenderer);
              // 设置形状
              mr.geometry = new BoxGeometry(5, 5, 5);
              // 设置材质
              mr.material = new HDRLitMaterial();
              // 添加到场景
              scene3D.addChild(obj);
              // 创建前向渲染
              let renderJob: ForwardRenderJob = new ForwardRenderJob(scene3D);
              // 开始渲染
              Engine3D.startRender(renderJob);
          }
      }
      

      完成项目

      可以看到,创建完一个项目后,只添加了hello.ts文件和修改了App.vue。这样一个真正的3D项目开发完成了,再在浏览器里面看一下运行效果(不要忘记在canary)。

      215b515d-ee86-4b19-847c-f968b6fab428-image.png

      你得到了一个可以用鼠标操作的立方体,按住左键拖拽可以任意旋转,滑动滚动可以调整距离,按住右键拖拽可以快速调整立方体位置,可以多操作一下熟悉一下这种最常见的操作方式。
      回忆我刚运行这一步时还是有点激动的,终于迈进了3D技术的大门,尽管是借用了引擎的助力,不过我们程序员就要擅长找到适合自己的工具。

      代码解析

      作为3D小白,我的目标是始于orillusion但不会止于某一引擎,通过充分熟悉一个引擎的用法来积累3D知识。可以看到每一个类都对应着3D世界的基础概念,在这里做一个简单的对应,顺便看看一个3D世界由哪些部分组成的,当然这里只有一个基础印象即可,不必深究。

      • 立方体:我们看到的立方体,是一个基础几何体,由类BoxGeometry来创建,实例化这个类在构造函数中指定长 宽 高,引擎就会为我们绘制出一个立方体,就像我们看到的这样。
      • 相机:相机是一个比较抽象但是在3D中无处不在,可以理解成我们的眼睛,我们是通过相机来观察3D内景物的,比如前面的立方体要在相机的可视范围内我们才能看到,在这里用组件Camera3D来定义。
      • 场景:是一个容器,前面的立方体、相机等等还有其它的对象、组件都是要添加到场景内才能被引擎组织和使用的,我们创建一个3D程序必须有一个场景,我们可以通过Scene3D类创建场景

      这里只介绍了三个最基础的组成部分,几何体、相机、场景,这三个部分在每一个3D程序中都是最基础的不可少的,以后我们会逐渐深入了解他们,也将会慢慢知道更多的工具。

      小结

      这篇文章是个开篇,快速上手了一个入门级别的3D示例,主要的作用并不是学习3D的基本技能,可以说更重要的是消除疑惑,最简单的上手3D项目。不得不说orillusion没有让我失望,以我目前这么简单的诉求也不会让我失望吧。

      作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

      发布在 中文社区 orillusion引擎 engine
      O
      oldguy