Orillusion

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

    orillusion入门系列五 | 光照02

    中文社区
    orillusion引擎 engine
    1
    1
    91
    正在加载更多帖子
    • 从旧到新
    • 从新到旧
    • 最多赞同
    回复
    • 在新帖中回复
    登录后回复
    此主题已被删除。只有拥有主题管理权限的用户可以查看。
    • O
      oldguy 最后由 编辑

      上一次了解到了光照的基本三类光源,这三类光源似乎在现实中不存在的,只是一种理想抽象,今天以光源为基础学习更多的光照功能,体验一下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新手,后续会不断的记录学习过程,期待与你一起学习一起飞!

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

      Recent Post

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

        • 阅读更多
      • B

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

        • 阅读更多
      • @StephenChips 同求

        • 阅读更多
      • A

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

        • 阅读更多
      • O

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

        输入系统

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

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

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

        世界坐标

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

        屏幕坐标

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

        相互转换

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

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

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

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

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

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

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

        KeyEvent

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

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

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

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

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

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

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

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

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

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

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

        小结

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

        • 阅读更多

      Copyright © 2022 Orillusion | Contact US