@猪子 多谢反馈,已更新
oldguy
-
orillusion入门系列二 | 快速入门 -
orillusion入门系列七 | 媒体扩展本节和 orillusion入门系列四 | 材质 一节息息相关,请先了解材质的思路和基础用法。通用的材质用法非常广泛,本节介绍的媒体扩展实际上是一些特殊的材质,只不过通过扩展包的方式提示,具有专有的比较单一的用途,在某一方面也可以非常强大和灵活。
环境准备
我们仅以 npm 包管理为例,回忆一下引擎的安装,是通过安装
@orillusion/core
包实现的,媒体扩展在@orillusion/media-extention
包中,因此安装媒体扩展,需要在安装了@orillusion/core
包的基础上再执行npm install @orillusion/media-extention --save
。图片材质
因此图片材质也是一种材质,回忆一下之前介绍的两种通用材质。图片材质与 UnLitMaterial 类似,都不对光照做出处理,但是图片材质提供了更多图片的裁剪、位移等功能。
基本用法
图片材质用法与通用材质类似,首先引入图片材质,然后在渲染组件中创建图片材质就可以了。
下面是关键代码:// 从扩展包中导入图片材质,在此之前请确认已经安装了拓展包 import { ImageMaterial } from "@orillusion/media-extention" ... // 创建渲染组件 let mr: MeshRenderer = obj.addComponent(MeshRenderer); // 设置形状 mr.geometry = new PlaneGeometry(10, 10); // 设置材质 mr.material = new ImageMaterial(); ...
为了和其它两种通用模型做个对比,同时创建了三种材质,并且没有加载图片资源,效果如下:
从左至右使用的材质分别是:LitMaterial、UnLitMaterial、ImageMaterial,可以看到后两种材质不受光照的影响。如果加载的图片纹理,并且打开了引擎自带的操控面板,显示效果如下:
可以看到加载图片资源后的效果,那么ImageMaterial提供了以下个性化的参数:- baseColor 基础颜色
- uv_offsetX x方向位移
- uv_offsetY y方向位移
- uv_scaleX x方向缩放
- uv_scaleY y方向缩放
- clip_left 左方裁剪
- clip_top 上方裁剪
- clip_right 右方裁剪
- clip_bottom 下方裁剪
完整的代码
下面是最简化的代码:
import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, MeshRenderer, LitMaterial, View3D, PlaneGeometry, UnLitMaterial} from "@orillusion/core"; import { ImageMaterial } from "@orillusion/media-extention" export default class Image { cameraObj: Object3D; camera: Camera3D; scene: Scene3D; 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.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(0, -45, 25); // 添加相机至场景 this.scene.addChild(this.cameraObj); this.createSphere(); } /** * 启动渲染 */ private async start() { let view = new View3D(); // 指定渲染的场景 view.scene = this.scene; // 指定使用的相机 view.camera = this.camera; // 开始渲染 Engine3D.startRenderView(view); } /** * 创建对象 */ private async createSphere() { let texture = await Engine3D.res.loadTexture('/material_02.png'); { // 创建 LitMaterial let obj = new Object3D(); obj.x = -20; // 创建渲染组件 let mr: MeshRenderer = obj.addComponent(MeshRenderer); // 设置形状 mr.geometry = new PlaneGeometry(10, 10); // 设置材质 mr.material = new LitMaterial(); mr.material.baseMap = texture; // 添加到场景 this.scene.addChild(obj); } { // 创建 UnLitMatial let obj = new Object3D(); // 创建渲染组件 let mr: MeshRenderer = obj.addComponent(MeshRenderer); // 设置形状 mr.geometry = new PlaneGeometry(10, 10); // 设置材质 mr.material = new UnLitMaterial(); mr.material.baseMap = texture; // 添加到场景 this.scene.addChild(obj); } { // 创建 ImageMaterial let obj = new Object3D(); obj.x = 20; // 创建渲染组件 let mr: MeshRenderer = obj.addComponent(MeshRenderer); // 设置形状 mr.geometry = new PlaneGeometry(10, 10); // 设置材质 mr.material = new ImageMaterial(); mr.material.baseMap = texture; // 添加到场景 this.scene.addChild(obj); } } }
可以看到在一些以显示图片资源为主的应用场景中,推荐使用图片材质。
视频材质
视频材质的用法和图片类似,需要注意的是视频材质需要绑定视频纹理。
关键代码如下:import { VideoMaterial, VideoTexture } from "@orillusion/media-extention" ... // 创建 LitMaterial let obj = new Object3D(); // 创建渲染组件 let mr: MeshRenderer = obj.addComponent(MeshRenderer); // 设置形状 mr.geometry = new PlaneGeometry(10, 10); // 设置材质 let videoTexture = new VideoTexture(); await videoTexture.load('/bunny.mp4') // 创建视频材质 mr.material = new VideoMaterial(); mr.material.baseMap = videoTexture; // 添加到场景 scene.addChild(obj);
视频材质支持的功能参数与图片材质相同。
色键材质
色键材质是视频材质的高级形式,在输出视频内容时可以对视频进行一些修饰,除了具有视频材质的所有参数外,示加了背景关键色,羽化等参数。具体参数可以参考官方文档,使用色键材质可以定制出更高级的视频显示效果。
import { VideoMaterial, VideoTexture } from "@orillusion/media-extention" { // 创建 LitMaterial let obj = new Object3D(); // 创建渲染组件 let mr: MeshRenderer = obj.addComponent(MeshRenderer); // 设置形状 mr.geometry = new PlaneGeometry(10, 10); // 设置材质 let videoTexture = new VideoTexture(); await videoTexture.load('/bunny.mp4') // 创建视频材质 mr.material = new VideoMaterial(); mr.material.baseMap = videoTexture; // 添加到场景 this.scene.addChild(obj); }
综合示例
在多媒体实际应用中为了优化显示效果或提高性能,一般需要对UV变换进行处理和资源的利用。
uv变换
将图片或视频显示在一个物体上,资源与物体通常会有不同的宽高比例,同时物体也会有不同的缩放比例,如何使得资源显示保持原有宽高比例是常见的问题。
这里结合引擎的特点提供一个常用的函数:/** * * @param texture 资源纹理 * @param obj 目标对象 */ const computeResourceUV = (texture: Texutre, obj: Object3D) => { // 定义一个多媒体内容的二维向量 let content: Vector2 = new Vector2(texture.width, texture.height); // 获取对象的物理信息与缩放比例 let mesh: MeshRenderer = obj.getComponent(MeshRenderer); let bound = new Vector2(mesh.geometry.bounds.size.x * obj.scaleX, mesh.geometry.bounds.size.y * obj.scaleY); let uv: Vector4 = new Vector4(); let W = bound.x, H = bound.y, w = content.x, h = content.y; let R = W / H; let r = w / h; if (R >= r) { let scaleX = R / r; let offsetX = (scaleX - 1) / 2; uv.x = -offsetX; uv.z = scaleX; } else { let scaleY = r / R; let offsetY = (scaleY - 1) / 2; uv.y = -offsetY; uv.w = scaleY; } // 材质的uvTransform_1 变量与UV变换关连 mesh.material.uvTransform_1 = uv; }
在设置完材质与纹理的绑定后,调用以上函数输入原材质与目标对象即可计算出优化的显示效果,当动态的修改了对象的缩放比例,需要两次调用该函数优化。该方法仅是抛砖引玉,深度用户会有更优化的方案。
资源复用
多媒体资源对系统资源占用格外多,需要特别的注意优化,比例每种材质与纹理可以只加载一次,在材质或物体中可以利用。为了更好的利用,建议使用简单的缓存方式,类似如下方式:
// 定义缓存结构 const textureCache: { [key: string]: { texture: Texture } } = {} // 读取缓存 let texture: Texture; if (textureCache[name]) { texture = textureCache[name].texture; } // 更新缓存 if (!texture) { let textureUrl = mesh.material.baseMap.url; texture = await Engine3D.res.loadTexture(textureUrl); textureCache[name] = { texture: texture }; }
以上代码仅以纹理举例,材质的用法类似,只需要更新一下缓存结构,其它的部分基本没有变化。类似的优化技巧非常多,可以一起来挖掘。
小结
本节介绍了引擎扩展包中的几种媒体专用材质,为编制出丰富多彩的系统提供了支持。
作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞! -
orillusion入门系列六 | 系统交互02在前一期系统交互01中学习了最基本的键盘和鼠标输入,挂载键盘输入系统后能够获得键盘的输入按键,挂载了触控事件,可以在场景中监控到鼠标或触控的操作。今天继续了解引擎提供的系统交互功能,再根据一些项目实践,丰富应用实例。
物体拾取
欲与物体交互首先需要识别出目标对象,在一个3D场景中最方便的识别出目标物体的方式无疑是通过鼠标直接选择。引擎提供的物体拾取功能可以轻松的完成这个目标。
基本用法
Orillusion的拾取功能使用起来非常简单,只需要做一个初始化的配置和挂载事件,需要注意的是拾取模式有像素和包围盒两种,一般推荐像素。具体的用法总结一下:
- 初始化
// 开启拾取功能 Engine3D.setting.pick.enable = true; // 设置拾取模式,pixel 像素, bound 包围盒 Engine3D.setting.pick.mode = `pixel`; // 初始化引擎 await Engine3D.init(); ......
- 挂载事件
// 创建一个物理对象 let boxObj = new Object3D(); // 挂载碰撞体组件 boxObj.addComponent(ColliderComponent); // 开启拾取事件 Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_CLICK, onPick, this);
这里挂载了 PointerEvent3D.PICK_CLICK 点击事件,其它还有更多事件可以参考官方文档。
- 响应事件
对拾取功能的使用关键在响应事件的处理,例如 onPick的响应类型是 PointerEvent3D 类型的变量
onPick(e: PointerEvent3D) { console.log('onPick', 'pickInfo:',e.data.pickInfo) console.log('onPick', 'target:',e.target) }
在以上代码可以看到,e.target是一个被点击的 Object3D类型的物理对象,e.data.pickInfo是点击的具体信息,例如世界坐标和屏幕坐标。
完整示例
写一个完整的示例,在该示例中创建一个简单的场景,添加两个立方体,其中一个添加了碰撞体组件,在挂载事件中响应事件,将该物理对象颜色设置成一个随机色,所在能够达到点击第一个物体后变色,而第二个立方体因为没有挂载碰撞体所以不会有效果。
import { Camera3D, Collider, ColliderComponent, Engine3D, ForwardRenderJob, LitMaterial, HoverCameraController, MeshRenderer, Object3D, PointerEvent3D, Scene3D, webGPUContext, BoxGeometry, } from '@orillusion/core'; export default class PixelPick { cameraObj: Object3D; camera: Camera3D; scene: Scene3D; controller: HoverCameraController; async run() { await this.init(); await this.setup(); await this.start(); } /*** * 配置并初始化引擎 */ private async init() { Engine3D.setting.pick.enable = true; Engine3D.setting.pick.mode = `pixel`; // 初始化引擎 await Engine3D.init(); // 创建一个场景 this.scene = new Scene3D(); // // 创建一个相机 this.cameraObj = new Object3D(); this.camera = this.cameraObj.addComponent(Camera3D); // // 设置相机类型 this.camera.perspective(60, webGPUContext.aspect, 1, 5000.0); // // 设置相机控制器 this.controller = this.cameraObj.addComponent(HoverCameraController); this.controller.setCamera(20, -20, 25); // 添加相机至场景 this.scene.addChild(this.cameraObj); } /** * 引擎功能代码 */ private async setup() { { let boxObj = new Object3D(); // 添加碰撞体检测 boxObj.addComponent(ColliderComponent); let mr: MeshRenderer = boxObj.addComponent(MeshRenderer); mr.geometry = new BoxGeometry(3, 3, 3); // 设置材质 mr.material = new LitMaterial(); // 添加对象至场景 this.scene.addChild(boxObj); } { let boxObj = new Object3D(); boxObj.x = 10; let mr: MeshRenderer = boxObj.addComponent(MeshRenderer); mr.geometry = new BoxGeometry(3, 3, 3); // 设置材质 mr.material = new LitMaterial(); // 添加对象至场景 this.scene.addChild(boxObj); } } /** * 启动渲染 */ private async start() { let view = new View3D(); // 指定渲染的场景 view.scene = this.scene; // 指定使用的相机 view.camera = this.camera; // 开始渲染 Engine3D.startRenderView(view); // 开启拾取事件 view.pickFire.addEventListener(PointerEvent3D.PICK_CLICK, this.onPick, this); } private onPick(e: PointerEvent3D) { console.log('onPick', 'pickInfo:', e.data.pickInfo) console.log('onPick', 'target:', e.target) let obj = e.target as Object3D; let mr = obj.getComponent(MeshRenderer); mr.material.baseColor = Color.random() } }
需要注意开启拾取事件必须在开始渲染之后,否则会有一个异常。
该示例运行效果如下:点击第一个物体会变色,第二个不会有响应
包围盒拾取
使用包围盒拾取不够精确,简单介绍一下使用方法,和像素拾取做一个对比说明。
- 设置包围盒类型为bound,即: Engine3D.setting.pick.mode =
bound
; - 创建包围盒
// 设置包围盒尺寸 let size: number = 2; // 创建包围盒形状 let shape: BoxColliderShape = new BoxColliderShape().setFromCenterAndSize(new Vector3(0, 0, 0), new Vector3(size, size, size)); //加一个碰撞盒子。 let collider = boxObj.addComponent(ColliderComponent); // 设置碰撞盒子形状 collider.shape = shape;
其它方面用法与像素用法无异。
自定义事件
在ECS架构下不同功能模块做到了方便的解耦和组合,但是组件间的通信的通信是不可缺少的,引擎提供了自定义事件主要用于不同组件间的通信。
自定义事件的和系统类事件(键盘、触控、拾取)类似。- 挂载事件
// 与其它类似事件类似,直接将一个定定义名称作为事件名挂载 let name = "customerEvent"; Engine3D.inputSystem.addEventListener(name, OnCustomerEvent, this); // 定义响应函数 OnCustomerEvent(e: CEvent) { console.log(e.type, e.data); let params = e.data; console.log(params); }
- 发送事件
// 定义事件参数 let params = { }; // 与响应事件定义相同的事件名称 let name = "customerEvent"; // 定义一个事件 let e = new CEvent(name, params) // 触发消息 Engine3D.inputSystem.dispatchEvent(e);
- 删除事件
// 挂载的事件类型在不需要时可以删除该事件挂载 let name = "customerEvent"; // 删除事件 Engine3D.inputSystem.removeEventListener(name, OnCustomerEvent, this);
以上定义了一个名称为customerEvent的事件,响应函数为OnCustomerEvent,在需要发送事件时可以通过Engine3D.inputSystem.dispatchEvent触发,响应函数会自动得到调用。
综合示例
引擎提供了灵活的底层支持功能,但是实际项目中的需求是多变的,这里总结一下常用的经典用法。
单击与双击
许多情况下需要处理鼠标的双击或单击事件,无论是通过 Engine3D.inputSystem.addEventListener 挂载的全场景事件还是通过 Engine3D.pickFire.addEventListener挂载的拾取事件,要实现区分单击与双击的思路都是相同的。
只需要在击事件中判断间隔时间与是否同一目标一致,当点击的时间在一定间隔(300ms)内只有一次时可断定为单击,当在间隔时间内有第二次点击,则清理掉定时器并判定为单击。// 记录操作时间 let timer; // 记录目标 let target; Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_CLICK, (e: PointerEvent3D) => { if (e.target === target && timer) { clearTimeout(timer); // 双击 timer = undefined; } else { target = e.target timer = setTimeout(() => { timer = undefined; // 单击 }, 300); } }, this);
监控对象
在某些情况下,物体的变换参数需要实时的监控,当监控到变化时触发某些业务场景。场景没有提供直接的功能,但是我们可以通过脚本来实现。
具体思路是创建一个用于监控的脚本,在该脚本中记录参数的变化,在每一帧中进行判断。再将该脚本挂载到需要监控的物理对象中。下面我们定义一个这样的脚本,本脚本仅以 position为例,需要其它参数可以类似增加。class ActionAttachScript extends ComponentBase { _position: Vector3 = new Vector3(); start() { console.log('start ActionAttachScript') this._position = this.object3D.localPosition.clone(); } onNotify(){ // 处理变化业务 } update() { let position = this.transform.localPosition; if (!this._position.equals(position)) { this.onNotify(); this._position.set(position.x, position.y, position.z); } } destroy(): void { console.log('destroy ActionAttachScript') } }
小结
本节学习了系统交互的两个重要的功能:拾取和事件。在普通的业务系统中一般不需要直接处理拾取功能,已经由业务框架自动处理了,但是无论在2D还是3D系统中都需要面对拾取的问题,特别是在3D系统中,不同的对象形状不同的位置,如果物体正处于运动中会更加复杂,所以引擎提供了基础的功能后,还需要手动的处理拾取事件。自定义事件更好的封装业务,配合自定义组件,对于开发复杂大规模的系统提供了比较完备的支持。
作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞! -
orillusion入门系列六 | 系统交互01经过几次的学习已经能够构建出一个空间(场景),并在空间中创建物体(几何体),物体可以有不同的外观(材质),与现实的效果足够逼真(光照),终于把最重要的相关性最强的几部分3D功能用起来了。不过面对这块空间想做点什么,又感觉缺少了点什么,是的,只能观看不能操作,如果我要通过键盘、鼠标对场景进行实时的干预该如何做呢,经过了解输入系统可以满足我们的要求。
输入系统是个比较杂乱的部分,不同平台都有对应的封装,我们可以回忆一下Win32编程将键盘和鼠标的输入集成到了事件系统,用户操作按键或操作鼠标会触发对应的消息码,指示消息,附带参数包含具体的按键信息或鼠标信息,按键信息一般包含按键码或鼠标键位。再回忆一下DOM的事件系统,使用addEventListener将click或mouse类的事件挂载,然后在回调函数中获得结果……
回忆结束我们可以总结出来几个输入系统的特点:1、挂载感兴趣的事件;2、回调函数得到触发时处理业务逻辑。需要注意的是,键盘需要有按键表进行区分按键,对应的是鼠标需要区分不同按键,以及屏幕坐标,辅助键等一些附属信息。键盘输入
使用键盘输入,首先需要熟悉两种键盘事件:
- KEY_DOWN:键盘按下事件,使用输入系统挂载该事件,将会得到按下键盘事件通知;
- KEY_UP:键盘弹起事件,使用输入系统挂载该事件,将会得到弹起键盘事件通知;
下面来一起梳理一下使用流程:
- 初始化:必要的引擎初始化;
// 创建一个场景 let scene = new Scene3D(); // // 创建一个相机 let cameraObj = new Object3D(); let camera = this.cameraObj.addComponent(Camera3D); // // 设置相机类型 camera.perspective(60, window.innerWidth / window.innerHeight, 1, 5000.0);
- 输入挂载:使用键盘挂载系统指定事件和回调;
Engine3D.inputSystem.addEventListener(KeyEvent.KEY_UP, keyUp, this); Engine3D.inputSystem.addEventListener(KeyEvent.KEY_DOWN, keyDown, this);
- 处理回调:在回调中获取参数。
private keyDown(e: KeyEvent) { console.log('keyDown:', e.keyCode, e); } private keyUp(e: KeyEvent) { console.log('keyUp:', e.keyCode, e); }
以上是最简单的能够获得键盘输入的步骤。
完整示例
上一节的示例似乎没有实际的使用价值,键盘一定是对应着具体的业务目标,这里我们写一个完全的例子,在场景中创建一个立方体,通过键盘来控制视角的拉远与接近。
import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, ForwardRenderJob, KeyEvent, KeyCode, MeshRenderer, BoxGeometry, LitMaterial, Vector3 } from "@orillusion/core"; export default class Keyboard { cameraObj: Object3D; camera: Camera3D; scene: Scene3D; controller: HoverCameraController; 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); // // 设置相机控制器 this.controller = this.cameraObj.addComponent(HoverCameraController); this.controller.setCamera(20, -20, 25); // 添加相机至场景 this.scene.addChild(this.cameraObj); let boxObj = new Object3D(); let mr: MeshRenderer = boxObj.addComponent(MeshRenderer); mr.geometry = new BoxGeometry(3, 3, 3); // 设置材质 mr.material = new LitMaterial(); // 设置位置 boxObj.localPosition = new Vector3(0, 0, 0); // 添加对象至场景 this.scene.addChild(boxObj); } /** * 引擎功能代码 */ 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 view = new View3D(); // 指定渲染的场景 view.scene = this.scene; // 指定使用的相机 view.camera = this.camera; // 开始渲染 Engine3D.startRenderView(view); } private keyDown(e: KeyEvent) { console.log('keyDown:', e.keyCode, e); if (e.keyCode == KeyCode.Key_W) { this.controller.distance -= 1; } else if (e.keyCode == KeyCode.Key_S) { this.controller.distance += 1; } } private keyUp(e: KeyEvent) { console.log('keyUp:', e.keyCode, e); } }
这是一个最基础的示例,只有场景和相机组件,运行这个示例后操作键盘,在控制台能够看到结果。在一个实际应用中,在响应事件可以实现对应的业务需求。
运行这个示例能够看到只有当键盘按下时才会有效果,那如何实现持续的按键功能呢,可以通过脚本来实现。创建一个脚本,键盘按下时开启变量,键盘弹起时关闭变量,在更新函数中根据变量来调整视距。class KeyboardScript extends ComponentBase { private front: boolean = false; private back: boolean = false; protected start() { Engine3D.inputSystem.addEventListener(KeyEvent.KEY_UP, this.keyUp, this); Engine3D.inputSystem.addEventListener(KeyEvent.KEY_DOWN, this.keyDown, this); } private keyDown(e: KeyEvent) { // console.log('keyDown:', e.keyCode); if (e.keyCode == KeyCode.Key_W) { this.front = true; } else if (e.keyCode == KeyCode.Key_S) { this.back = true; } } private keyUp(e: KeyEvent) { // console.log('keyUp:', e.keyCode); if (e.keyCode == KeyCode.Key_W) { this.front = false; } else if (e.keyCode == KeyCode.Key_S) { this.back = false; } } onUpdate() { if (!this.enable) return; let transform = this.object3D.transform; if (this.front) transform.z -= 1; if (this.back) transform.z += 1; } }
将该脚本添加至对象即可
boxObj.addComponent(KeyboardScript);
再运行这个实例,在键盘持续入下时能够看到持续平滑的效果。这是这个实现是通过调整物体的坐标来实现的。系统实现
在回调函数中获得的参数类型是KeyEvent,KeyEvent是CEvent的子类,除了CEvent类的参数外,对于键盘事件的使用主要在于对该类型的解析,keyCode是引擎的宏定义,具体按键定义可以参考官方文档。
出于好奇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键的状态;
鼠标与触控
电脑端的鼠标操作与移动端的触控操作有许多共同的地方,在具体用法时如果能够合并为一,是可以节省一半的事件挂载操作的,不过需要留意触控与鼠标的事件对应关系。
有了前面键盘操作的基础,鼠标与触控使用类型,我们先看支持的事件类型:- 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.PICK_UP, this.onUp, this); Engine3D.inputSystem.addEventListener(PointerEvent3D.PICK_DOWN, this.onDown, this); Engine3D.inputSystem.addEventListener(PointerEvent3D.PICK_CLICK, this.onPick, this); Engine3D.inputSystem.addEventListener(PointerEvent3D.PICK_OVER, this.onOver, this); Engine3D.inputSystem.addEventListener(PointerEvent3D.PICK_OUT, this.onOut, this); Engine3D.inputSystem.addEventListener(PointerEvent3D.PICK_MOVE, this.onMove, this); } /** * 启动渲染 */ private async start() { let view = new View3D(); // 指定渲染的场景 view.scene = this.scene; // 指定使用的相机 view.camera = this.camera; // 开始渲染 Engine3D.startRenderView(view); } 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 LitMaterial(); // 添加到场景 this.scene.addChild(this.boxObj); boxObj.addEventListener(PointerEvent3D.PICK_CLICK, this.onClick, this); onClick(e: PointerEvent3D) { console.log('onPick:',e); }
运行后可以在控制台看到输出
关于坐标
一直以来的学习路径是以实用为主,但是现在必须要接触一点点不能称之为理论的理论了,那就是坐标系统。
世界坐标
首先要解决一个困惑的地方,过去在3D空间中的所有坐标都可以称为世界坐标,世界坐标是三维的,有三个维度(x,y,z),一般在引擎中创建可以由系统使用,开发用户程序需要遵守引擎对于世界的规划,相当于场景作为一个空间,世界坐标是对这个空间制定的规则。这里歪个楼,骇客帝国之所以叫矩阵,是不是因为在3D引擎中对空间世界的处理也是以矩阵为基础的。再拉回来,世界坐标一般以(0,0,0)为中心,我们创建的物体默认的位置也是在这里的,这里是世界的中心,一般分为右手或左手坐标系,好了关于世界坐标系这里已经够用了。
屏幕坐标
说回到屏幕坐标是我们过去所熟悉的,首先屏幕坐标是一个二维坐标,以像素为单位,屏幕的左下角为起点,向屏幕的左侧和上方依次是x和y坐标的正向。在网页开发中我们通过DOM事件系统获得的当前坐标一般都是指的屏幕坐标。在网页开发中并不是绝对的没有z轴,CSS中的z-index属性是否可以理解成一种z轴坐标呢。
相互转换
屏幕坐标是我们最终渲染到屏的最终展现形式,世界坐标是在三维空间内的标识,两者经常需要相互转换,例如今天需要讨论的输入系统的使用。假设在屏幕上点击了一个位置,需要转换到世界坐标,相似的在世界坐标内的位置或距离也需要转换为屏幕坐标。
坐标转换有标准的算法,这里我们不必如此费力,完全可以借助引擎的工具,经过一翻查找,在相机组件的实现类Camera3D,有坐标转换的工具,可以一起熟悉一下- object3DToScreenRay:世界坐标转换屏幕坐标;
// 定义一个世界坐标 let pos3D = Vector3(); // 转换为屏幕坐标 let posScreen = Engine.camera.object3DToScreenRay(pos3D);
- ScreenRayToObject3D:屏幕坐标转换为世界坐标;
// 定义一个屏幕 let posScreen = Vector3(); // 转换为屏幕坐标 let pos3D= Engine.camera.ScreenRayToObject3D(posScreen );
小结
经过前面几次的学习,已经能够完事的构建出一个空间了,但是这块空间仍然缺乏灵动的能力,不能随时响应我们的操控,输入系统是一个随时干预系统的大杀器,可以让我们获得掌控感,是否控制欲获得了满足。
今天只是一些最基础的用法,发挥想象力可以使这个空间好玩起来了。作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!
-
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; // ...
以上代码可以将一个场景的太阳进行配置,之后添加物体就会有阳光的可适配效果了。
阴影
有光自然有阴影,阴影需要配置的方面比较少,前面已经用过许多次这里专门记录一下使用思路。
阴影的配置
在尝试使用光照阴影相关的功能时,遇到过几个问题是因为对阴影的配置不了解,这里把遇到问题的配置项进行一个记录。
阴影的配置路径在Engine3D.setting.shadow下。- debug:调试开关,打开这个开关可以在控制面板操作参数值,仍然是依赖引擎的GUIHelp工具实现。
- autoUpdate:阴影是否自动更新,这个配置项非常重要,在试用光照阴影效果时尝试过移动物体但阴影不动,打开这个开会会自动更新阴影。
产生阴影
目前只有平行光源会产生阴影,平行光组件需要将castShadow打开,在物体材质上调整光照的角度会产生阴影的效果。
对比
普通阴影和全局光照的阴影效果有很大的差别,这里将两个效果做个对比,上面是普通阴影,下面是开启了全局光照的效果。
同样的材质和光照强度以及场景环境,下面加了全局光照有了更厚重逼真的感觉,这差距好比女人之间八卦产生的友谊和男人之间战斗建立的友谊一样大。小结
光照是非常核心又非常高级的部分,在3D中渲染效果的好坏以及性如何和对光的处理有直接的关系,从上一次的三种光源可以使场景内有光,根据不同的用途可不同的光源,到今天学习的环境光,使场景内有了一轮太阳,尽管这轮太阳是天空盒材质的,不过不影响我们把它当成一个光源,到最后的全局光照使的场景内的环境更真实逼真,全局光照还有许多特性和新技术,需要不断的去学习接受挑战。
作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞! -
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: Object3D = 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 中直接运行,查看效果如下:
这个示例里面除了我们重点关注的平行光以外,还有阴影,反射等等,下面会一一介绍。使用思路仍然是通过熟悉 orilluson 的api,调用不同的接口与参数即可。
这里可以看到,没有做位置的变换操作,在当前的版本调整位置是没有效果的,调整强度参数能够看到光线的强度随着数值而动态变化。点光源
有了前面最好理解的平行光作为铺垫我们已经熟悉了光源对象的使用,点光源就是另种特性的光源,类似于萤火虫、没有灯罩的白炽灯这样的发光体,本身是一个点,向四面八方发射光线。
点光源有更多的属性,除了平行光的属性外,罗列一下点光源所特有的属性:- range(距离):从光源到光线衰减到0的最远距离,该值越大能够发出光线受到影响的范围越大。
- radius(半径):光照的最亮那部分的半径。
- at(衰减系数):光照的效果根据系数有所衰减。
- 位置:点光源由于受距离的影响,不同距离显示的效果不同,所以位置起着重要的作用。与平行光的旋转类似,点光源的位移是由对应的对象所控制。
点光源的调用关键代码如下:
// 创建一个光源对象 let lightObj: Object3D = new Object3D(); // 由于点光源的位置影响光照效果,所以附加一个球形物体用来标记位置 let mr = lightObj.addComponent(MeshRenderer); mr.geometry = new SphereGeometry(0.5, 10, 10);; mr.material = new LitMaterial(); //创建点光源组件 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; });
以上代码运行的效果如下:
可以看到点光源是一个球形的光照效果,一般用来模拟点状的光源,计算量大于平行光源。因为点光源向各个方向的光线强弱相同,所以调整方向不会有效果,但是位置的作用非常明显。
建议实现这个代码,实际进行操作可以加强印象和理解。聚光灯
聚光灯与点光源非常类似,与点光源相比聚光灯只显示一个椎体范围内的效果,通常用来模拟手动筒,探照灯等这些发光体。
聚光灯所支持的参数除点光源以外还有以下几个:- innerAngle(内切角):光锥内切角,聚光在小于这个角度的范围内有光线
- outerAngle(外切角):光锥外切角,光线会在内切角到外切角的范围内逐步衰减到0
- 位置与方向:聚光灯光源受位置和方向双重影响,所以可以通过对应的对象调整位置与方向。
关键调用示例代码如下:
// 创建光照,应该已经很熟了吧,不做过多解释 let sp = new SphereGeometry(0.5, 10, 10); let lightObj: Object3D = new Object3D(); let mr = lightObj.addComponent(MeshRenderer); mr.geometry = sp; mr.material = new LitMaterial(); 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; });
运行界面效果如下:
可以将聚光灯当成一个手电筒玩耍,多操作界面操作,会很快熟悉这种光源的。
其它
orilluson对功能模块进行了重新划分,Stats模块需要单独安装,执行命令 npm install @orillusion/stats --save进行安装,引入代码修改为import { Stats } from "@orillusion/stats";其它用法不变。
小结
这是第一次接触光照的特性,所以从最简单易于理解的三种光源来着手,orilluson提供的这三类光源非常方便,为我接触3D的光照提供了一个很好的切入点。后面要继续熟悉环境光,全局光照,以前阴影等等。
作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞! -
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, View3D, BoxGeometry, 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 view = new View3D(); // 指定渲染的场景 view.scene = this.scene; // 指定使用的相机 view.camera = this.camera; // 开始渲染 Engine3D.startRenderView(view); } /** * 创建立方体 */ 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; }); } }
运行效果如下:
调整颜色会看到颜色会叠加到物体上。LitMaterial(物理材质)
LitMaterial是物理材质,计算的因素多了起来,与实际效果更接近了。看到各大引擎都是主推物理材质,所以除非计算机性能实在拉垮,否则还是多熟悉使用物理材质吧,大势所趋。过去我们的例子默认也是用的这种材质。
支持参数
物理材质支持的参数比较多,这里我们只验证个别几个属性,更多的属性可以对着文档一一验证。
- baseColor(基础颜色):材质本身的颜色,可以使用一个Color类型的变量设置
- roughness(粗糙度):模拟物体表面粗糙的程度,随着数值变大物体会看起来更粗糙
- metallic(金属度):模拟物体表面金属的程度,随着数值的变化物体会看起来更光滑
使用这三个参数调节显示效果如下:
综合示例
不同的材质主要还是对光的处理不同,这里我们写一个综合的实例,将两种材质放到一个场景下,在一个聚光灯照射下观察效果,这里我们提前借用一下光源的组件,可以暂时不必在意,后续会专门介绍。
运行效果
在右侧面板我们增加了Light操作面板,可以调整光的颜色和方向,使光照分别照射在不同的立方体上,立方体分别对应着UnLitMaterial,LitMaterial二种材质,同时提供了两种材质的基本参数操作按钮,可以自行调整观察效果。完成代码
import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, View3D, BoxGeometry, LitMaterial, ForwardRenderJob, Color, UnLitMaterial, Vector3, MaterialBase, SpotLight, SphereGeometry, defaultTexture, GUIHelp, DirectLight } from "@orillusion/core"; import { Stats } from "@orillusion/stats"; import * as dat from 'dat.gui'; export default class Materials { light: SpotLight; cameraObj: Object3D; camera: Camera3D; scene: Scene3D; boxObj: Object3D; litMaterial: LitMaterial; unlitMaterial: UnLitMaterial; async run() { await this.init(); await this.setup(); await this.start(); } /*** * 配置并初始化引擎 */ private async init() { // 初始化引擎 Engine3D.setting.shadow.enable = true; Engine3D.setting.shadow.debug = true; Engine3D.setting.shadow.shadowBound = 10; Engine3D.setting.shadow.shadowBias = -0.00016; Engine3D.setting.shadow.autoUpdate = true; Engine3D.setting.shadow.updateFrameRate = 1; Engine3D.setting.shadow.pointShadowBias = 3; await Engine3D.init(); // GUIHelp.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.litMaterial = new LitMaterial(); this.unlitMaterial = new UnLitMaterial(); this.createBox(new Vector3(-10, 0, 0), this.unlitMaterial); this.createBox(new Vector3(10, 0, 0), this.litMaterial); this.createLight(); this.createFloor(); this.addGUI(); } /** * 启动渲染 */ private async start() { let view = new View3D(); // 指定渲染的场景 view.scene = this.scene; // 指定使用的相机 view.camera = this.camera; // 开始渲染 Engine3D.startRenderView(view); } private async createLight() { let sp = new SphereGeometry(0.5, 10, 10); let lightObj = new Object3D(); lightObj.z = -20; let mr = lightObj.addComponent(MeshRenderer); mr.geometry = sp; mr.material = new LitMaterial(); this.light = lightObj.addComponent(SpotLight); this.light.intensity = 60; this.light.castShadow = true; this.scene.addChild(lightObj); lightObj.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.castShadow = true; // 设置形状 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 LitMaterial(); mat.baseMap = defaultTexture.grayTexture; let mr = floor.addComponent(MeshRenderer); mr.receiveShadow = true; 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.light.transform.x = v; }); lightFolder.add(lightInfo, "y", -10, 10).onChange((v) => { console.log('y:', v); this.light.transform.y = v; }); lightFolder.add(lightInfo, "z", -10, 10).onChange((v) => { console.log('z:', v); this.light.transform.z = v; }); lightFolder.add(lightInfo, "rotationX", -360, 360).onChange((v) => { console.log('rotationX:', v); this.light.transform.rotationX = v; }); lightFolder.add(lightInfo, "rotationY", -360, 360).onChange((v) => { console.log('rotationY:', v); this.light.transform.rotationY = v; }); lightFolder.add(lightInfo, "rotationZ", -360, 360).onChange((v) => { console.log('rotationZ:', v); this.light.transform.rotationZ = v; }); lightFolder.addColor(lightInfo, "lightColor").onChange((v) => { console.log('lightColor:', v); let color: Color = new Color(); color.setHex(v); this.light.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 litInfo = { baseColor: '#fff', roughness: 0.01, metallic: 0.01, }; let hdrFolder = gui.addFolder("LitMaterial"); // 设置材质颜色 hdrFolder.addColor(litInfo, "baseColor").onChange((v) => { console.log('baseColor:', v); let color: Color = new Color(); color.setHex(v); this.litMaterial.baseColor = color; }); // 材质粗糙程度 hdrFolder.add(litInfo, "roughness", 0, 1).onChange((v) => { console.log('roughness:', v); this.litMaterial.roughness = v; }); // 材质粗糙程度 hdrFolder.add(litInfo, "metallic", 0, 1).onChange((v) => { console.log('metallic:', v); this.litMaterial.metallic = v; }); } }
小结
继几何体后,引擎对常用材质做了封装,使用户可以不必自己编写shader,创建一个对应的类,设置参数就可以得到一个可用的材质,用来丰富物体。材质的种类还有一些,不过当前只发现了三种,期待后续会有更多的材质可以使用。
作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞! -
orillusion入门系列三 | 几何体前两次的学习总结中,成功运行了一个最基础的功能,对引擎的的整体代码进行了归纳,根据我的习惯提供了一个并不严谨的开发步骤,并且介绍了两个工具。主要解决了认识引擎以及如何开始使用引擎。今天我准备学习使用引擎开发真正的功能了,在这里继续使用大白话(水平有限只能如此)介绍学习过程。
专业的 3D 系统开发需要符合软件工程实践,只以像我这样的小白视角,入门为目的提供一个思路,我们前面了解到首先要通过场景开辟一个空间,加上相机开启上帝视角,那么之后呢?我认为是“造物”,一个3D空间无论复杂还是简单,都可以看成是一个虚拟世界,是对真实世界的理解的反映,在真实世界中我们能看到的物体是由点、线、面组成的,在3D场景中最基础的是由点组成的,3D中的点由三个坐标(x、y、z)分量构成的,GPU一般是按照三角型处理的,描绘点我们一般称为顶点。在3D中的一座山、一条路、一个篮球,都是由若干的顶点构成的。但是只有顶点是不够的,每个物体还会有不同的外观,我们一般用材质来表示,材质对物体的外观进行装饰成不同的样式。所以顶点+材质就组成了一个物体,这个物体我们一般称为网格(mesh),今天我们先忽略材质,专注于造物。几何体
面向3D的世界我还是学前班阶段,所以我们从最基础的几何体开始。orillusion目前提供了四种几何体可以直接使用,并不算多,实践中肯定会需要更多的类型吧,期待后续会增加,这里先熟悉这四种,对于了解几何体是足够的了。
长方体
长方体是我的最爱,我们在第一天就熟悉了如何使用长方体,一个长方体由宽度、高度、深度三个分量组成,这里我们复习一下如何使用
// 创建一个容器对象 let boxObj: Object3D = new Object3D(); // 创建渲染组件 let mr: MeshRenderer = boxObj.addComponent(MeshRenderer); // 创建一个立方体 mr.geometry = new BoxGeometry(5, 5, 5); // 设置材质 mr.material = new LitMaterial(); // 添加到场景 scene.addChild(boxObj);
这里需要注意,几何体的属性在创建时通过构造函数参数指定,不支持动态的修改,动态改变形状建议使用Object3D对象的缩放(scale)属性。球体
球体也是我们非常熟悉的一种几何体,创建球体必须要指定半径,以及水平和垂直分段数。
// 创建一个对象 let boxObj: Object3D = new Object3D(); // 创建渲染组件 let mr: MeshRenderer = boxObj.addComponent(MeshRenderer); // 设置球体的实例 mr.geometry = new SphereGeometry(3, 100, 100); // 设置材质 mr.material = new LitMaterial(); // 添加到场景 scene.addChild(boxObj);
运行效果如下
平面
创建一个平面我们至少需要指定长和宽两个属性
// 创建一个对象 let boxObj: Object3D = new Object3D(); // 创建渲染组件 let mr: MeshRenderer = boxObj.addComponent(MeshRenderer); // 设置形状 mr.geometry = new PlaneGeometry(10, 10); // 设置材质 mr.material = new LitMaterial(); // 添加到场景 scene.addChild(boxObj);
运行效果如下
圆柱体
我们这里创建一个半径为5,高为10的圆柱体
// 创建一个对象 let boxObj:Object3D = new Object3D(); // 创建渲染组件 let mr: MeshRenderer = boxObj.addComponent(MeshRenderer); // 设置形状 mr.geometry = new CylinderGeometry(5, 5, 10); // 设置材质 mr.material = new LitMaterial(); // 添加到场景 scene.addChild(boxObj);
运行效果如下
Object3D 对象
我们不是第一次接触Object3D对象了,这里我们通过实例用一下这个组件,我们以长方体为例,长方体是通过组件的方式添加到一个Object3D容器中的,所以通过Object3D可以操作立方体的属性。
位置
分别可以直接读写对象的x、y、z坐标。
boxObj.x = v; boxObj.y = v; boxObj.z = v;
也可以通过 Vector3 类型一次性设置三个分量,这里不作演示了
旋转
分别可以直接读写对象x、y、z三个方向的旋转角度。
this.boxObj.rotationX = v; this.boxObj.rotationy = v; this.boxObj.rotationz = v;
也可以通过 Vector3 类型一次性设置三个分量,这里不作演示了
缩放
分别可以直接读写对象x、y、z三个方向的缩放。
boxObj.scaleX = v; boxObj.scaleY = v; boxObj.scaleZ = v;
也可以通过 Vector3 类型一次性设置三个分量,这里不作演示了
动态设置开关
设置对象的 enable 属性,可以对该对象以前所有子对象以前脚本的有效性进行设置
boxObj.enable= v;
动态显示或隐藏
设置对象的 visible 属性可以对对象显示或隐藏
boxObj.visible = v;
使用脚本
前面创建的物体看起来很单调,用鼠标操作一下才会动,如何让这个物体本身有一定的行为呢。组件一般用来封装可复用的功能,添加至不同的Object3D中,引擎内部会自动调用该组件的功能作用在这个容器对象中。
这里我们看一个最基本的组件脚本的模板:class Script extends ComponentBase { // 覆写 初始化 public init(){ // 该函数在组件被创建时调用,可以用来初始化内部的变量 // 注意,此时组件被挂载到 Object3D 上,所以无法访问 this.object3D } // 覆写 渲染开始 public start(){ // 该函数在组件开始渲染前被调用, // 一般访问 this.object3D, 通常用来获取节点的属性或其他组件 } // 覆写 onUpdate public onUpdate() { // 每帧渲染循环调用,通常定义节点的循环逻辑 // 例如,每一帧更新物体旋转角度 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, LitMaterial, View3D, AtmosphericComponent } from "@orillusion/core"; import * as dat from 'dat.gui'; import { Stats } from "@orillusion/stats"; 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.scene.addComponent(AtmosphericComponent); // 创建一个相机 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 view = new View3D(); // 指定渲染的场景 view.scene = this.scene; // 指定使用的相机 view.camera = this.camera; // 开始渲染 Engine3D.startRenderView(view); } /** * 创建立方体 */ private async createBox() { // 创建一个对象 this.boxObj = new Object3D(); // 创建渲染组件 let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer); // 设置形状 mr.geometry = new BoxGeometry(5, 5, 5); // 设置材质 mr.material = new LitMaterial(); // 添加到场景 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:0, 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); 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 { onUpdate(): void { // 旋转 this.transform.rotationX = Math.sin(Time.time * 0.001) * 100.0; } }
脚本不停的更新帧数据,运行时能够看到这个方立体在自动旋转。右侧操作面板对应的操作也可以看到变化。
小结
今天学习了最基本的功能,没有按照通常的3D学习顺序,这里只按照最朴素的习惯性思维来介绍。现在我们学会了造物,后续要继续学习如何装扮这些物体。
作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!
-
orillusion入门系列二 | 快速入门前文我们自己动手使用引擎体验到了3D的效果,今天我们尝试梳理一下使用引擎做3D开发从何处入手,快速的从整体上了解如何使用 orillusion 开发3D项目。
引擎的挑战
3D是一项面向视觉的技术,为了获得更逼真、更炫酷、更流畅的体验,需要极强的理论根基和工程实践经验。建议先粗略的看看引擎所要面对的一些实际问题,了解一下对引擎浮浅的认识,以便我们能更好的理解如何使用引擎。
- 从贵族到平民 数学是构建3D世界的支柱,3D效果的发展离不开对数学和算法的深刻理解和应用,比如为了体现光线的真实感,为了实现服装飘逸的感觉,都有许多个数学公式作为底层支撑。另一方面需要充分了解硬件的特性,这都是非常耗费时间的工作,引擎使得没有这方面基础的普通开发者可以集中时间在实现需求上。
- 从贪婪到克制 无论硬件是否会继续以遵循摩尔定律的规律提升,引擎要以足够贪婪的程度榨取硬件的性能,使硬件上的投资获得回报,同时要克制自身对资源的占用,即要又要的典范。
- 从混乱到秩序 引擎所面向的世界是多方面的,不同的硬件之间如何协调,多变的网络环境,多种资源处理,稳定灵活高效的任务调度等等,在引擎的帮助下有条不紊的组织在一起。
- 始终面带微笑 引擎要发挥出足够的作用,必须要让用户足够容易上手,提供给用户要有足够友好的api,还有完备的工具链和文档,所以引擎要提供尽可能的友好使用体验。
使用引擎
因为以上罗列的一些原因,引擎需要做许多取舍,在使用引擎的时候也有一些固定的要求。大体上我们分为搭建环境、引擎配置、引擎初始化、开启渲染任务这几个步骤。
搭建环境
Orillusion
支持多种方式安装,具体可以参考官方文档,上一篇文章使用了 cdn引用和 vue&vite 系列的开发环境,这里假设您已经进行了安装。引擎配置
引擎为了满足多样的需求和不同的环境,提供了许多配置选项,在代码的最初位置进行引擎配置,后续的初始化和运行需要依赖配置项。这里只列出两条配置示例,更多的配置项可以参考官方文档。
// 最大实体数量 Engine3D.setting.memory.doMatrixMaxCount = 100000; // 最大灯光数量 Engine3D.setting.light.maxLight = 1024;
初始化
引擎进行了配置之后,需要进行初始化,在初始化这一步可以根据需求申请必要的内存。这里我们可以配置 canvas变量,也可以指定渲染前回调、每一帧回调以前每一帧渲染后的回调。
// 先配置,后初始化 await Engine3D.init({ canvasConfig:{ // 配置 cavans canvas: document.getElementById("webGpuCanvas"), alpha: false, zIndex: 1 }, beforeRender: ()=>{ // 每一帧渲染前回调 }, renderLoop: ()=>{ // 每一帧回调 }, lateRender: ()=>{ // 每一帧渲染后回调 } });
我们目前没有特别的功能需求,一般用最简化的初始化代码。
await Engine3D.init();
功能代码
编写一个相对简单的3D程序,我们可以简单的将过程理解为几个步骤:
- 开辟一个3D空间:创建一个场景,相当于一个3D空间,后续的资源可以添加到这个场景中。
// 创建一个场景 let scene3D: Scene3D = new Scene3D();
- 设置一个观察点:这个观察点我们一般用相机组件来体现,所有看到的场景内的信息都是通过相机组件的角度实现的,相机支持一系列的配置参数和方法,后续我们会有专门的章节来学习,同时该相机也要添加至场景中。
// 创建一个实体对象,用来管理相机组件 let cameraObj: Object3D = new Object3D(); // 向 相机 对象内添加相机组件 Camera3D 是我们的相机组件 let camera = cameraObj.addComponent(Camera3D); // 相机的配置略 ... // 添加相机至场景 scene3D.addChild(cameraObj);
- 添加物体:这里我们为了演示,添加了一个立方体,同样需要添加至场景中。
// 创建一个对象 const obj = new Object3D(); // 创建渲染组件 let mr = obj.addComponent(MeshRenderer); // 创建一个几何体,这里是box mr.geometry = new BoxGeometry(5, 5, 5); // 设置材质 mr.material = new LitMaterial(); // 添加到场景 scene3D.addChild(obj);
开始渲染任务
所谓渲染形象来说是,将场景内的景物,通过相机作为观察点,生成二维图像,显示在演示设备上的过程。我们这里一般显示设备是前端的canvas,所以渲染可以理解为从场景到canvas的过程,为了保持流畅,需要高速的刷动作。
渲染任务是通过3D视图将场景与相机组织到一起,提供给引擎进行渲染。// 创建View3D对象 let view = new View3D(); // 指定渲染的场景 view.scene = scene3D; // 指定使用的相机 view.camera = camera; // 开始渲染 Engine3D.startRenderView(view);
后处理
从代码上看,后处理是要放到开始渲染任务之前,但是从逻辑上这里放到最后一步,因为后处理是在生成图片后,界面显示前所做的一次纯粹的图片处理。
得益于组件化的架构设计,尽管后处理的类型比较多,但是可以使用统一的组件系统,后续会非常熟悉组件系统的用法,暂时可以有个印象即可。... // 创建一个后处理组件 let postProcessing = this.scene.addComponent(PostProcessingComponent); // 向后处理组件中添加效果,有这么多内置效果,可以用同样的方式添加 postProcessing.addPost(DepthOfFieldPost); //景深效果。 ...
后处理是个相对独立又非常有趣的功能,后续我们会专门深入了解。
调测工具
为了更好的观察程序的运行状态和动态的操控功能,了解两款相对应的工具很有必要。
性能监控(Stats)
Orillusion
官方提供的一个性能监控工具组件 Stats,可以显示引擎的运行状态,我们可能直接添加到场景中即可,默认会显示在运行界面的左上角,显示位置可以修改,这里我们使用默认设置就可以了。Stats 不在引擎的核心包中,需要从扩展包中安装:npm install @orillusion/stats --save
,安装后按照以下代码使用import { Stats } from "@orillusion/stats" 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 失去焦点时触发 具体代码在后续代码中一共演示,这里以vue&vite环境为例
编写代码
- 添加@别名 以便代码中可以使用 @ 指代源码路径
a. 安装node类型 npm i @types/node
b. 修改 vite.config.ts 文件,新加 @ 别名配置
export default defineConfig({ resolve: { alias: { '@': join(__dirname, "src"), } }, plugins: [vue()] })
c. 修改 tsconfig.json 文件,配置TypeScript类型提示,增加 baseUrl和paths配置项
"baseUrl": ".", "paths": { "@/*": ["src/*"] }
- 重构目录结构
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>
- 安装 dat.gui:
npm install --save dat.gui
- 完善 hello.ts ,主要从以下几个方面来进行
a. 重构 Hello 类,将 函数内的临时变量声明成类成员变量
b. 根据我们前面的引擎步骤,增加处理函数
c. 增加 dat.gui 操作立方体的控制项 - 完成 hello.ts 代码
import { Engine3D, Scene3D, Object3D, Camera3D, HoverCameraController, MeshRenderer, BoxGeometry, LitMaterial, View3D, AtmosphericComponent } from "@orillusion/core"; import { Stats } from "@orillusion/stats"; 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(AtmosphericComponent); // 添加性能监控面板 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 view = new View3D(); // 指定渲染的场景 view.scene = this.scene; // 指定使用的相机 view.camera = this.camera; // 开始渲染 Engine3D.startRenderView(view); } /** * 创建立方体 */ private async createBox() { // 创建一个对象 this.boxObj = new Object3D(); // 创建渲染组件 let mr: MeshRenderer = this.boxObj.addComponent(MeshRenderer); // 设置形状 mr.geometry = new BoxGeometry(5, 5, 5); // 设置材质 mr.material = new LitMaterial(); // 添加到场景 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(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; }); } }
完成项目
今天的代码我们主要演示了两款测试工具,在左上角是性能监控面板,在右上角是操控面板,用鼠标滑动滚动条或者直接输入数值,可以分别理性立方体的x、y、z的值,物体对应的会发生移动。
可以看到这一个示例比上一节的有了背景环境效果,因为我们向场景中添加了AtmosphericComponent
天空盒组件,所以可以看到天空盒效果。代码解析
今天的代码在第一篇的基础上有所增加,但是以下三个类型已经第二次出现了,我们不去深究更详细的原理,需要对这三个常用的对象类型有所了解。
- Object3D 是引擎内置的实体对象,通常被当做基本的组件容器,可以通过不同组件组合来实现不同的类型功能。我的理解是,Object3D内置一些基础功能对象,还可以是功能组件的容器,比如我们这里创建一个用于管理立方体的容器boxObj。
- MeshRenderer 是一个用于渲染网格的组件,这个组件非常重要,包含几何属性和材质属性,每个需要渲染的物体都需要有几何属性和材质属性。
- LitMaterial Light材质,基于物理渲染,旨在模拟现实世界光照效果,以后我们会专门来学习材质的部分,这里只要知道这是一种材质就可以。
以上三个类型都非常重要,首先我们要创建一个Object3D作为容器,然后在这个容器中创建MeshRenderer作为渲染组件,渲染组件中需要实例化几何体和材质。就这样由容器与功能组件相互配合完成功能,当然都需要按照一定的层级结构添加至场景中。
小结
今天根据我的理解总结了一个引擎所必须面对的几个难点,可以更好的理解引擎的设计,同时拆解了 orilluson 代码的几个步骤,以便能够快速的上手3D代码开发。进一步的介绍了两个非常常用的调测工具,为以后自由的调用功能提供了工具。之后继续完善项目,运行项目演示效果。最后我们熟悉了最常见的三个组件。
作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞! -
orillusion入门系列一 | 第一印象做了很多年的应用开发想要在技术上有些突破,是时候学点真正的技术了,我选择了3D技术,因为接触3D技术之前我似乎是一只二维生物,只能在一个平面上思考问题,但是3D技术难度不小,所以必须找个足够高的肩膀来踩,那就要借助引擎的力量了。经过一番搜索,orillusion 着实让我感动,满足了我所有的要求,并且国产引擎能提供全面的中文资料支持和更小的交流成本。
为什么选择 orillusion
没有专业的对比,仅凭我个人的好恶盲目来选择,简单从这两个方面来考虑
Native or Web?
如果您是专业人士建议详细对比两种类型的发展历史和原理再做出判断,对于我一个3D新手来说,有一个简单粗暴的判断思路,那就是在应用系统上,CS架构仍在某些特定领域仍然存在,但是能够迁移到BS的,几乎都在大规模的迁移到了BS架构上,所以我挺Web渲染架构。
足够新
新人就要有新人的觉悟,老牌的各路引擎已经有无数高手浸淫其中多年了,冒然投入进去会被沉重的历史信息淹没,作技术还是要有点野心的新手终有一天会成老鸟,所以要选择最新推出的面向最新技术架构的引擎,那么有多新呢,标准还没正式应用的够新吧,那就是以面向WebGPU为第一图形标准实现功能,至于WebGPU是什么,我的理解是更新、更快、更强,至于具体如何做到,相信以后我们会知道的。
准备工作
唯一必须的的准备工作是做好心理建设,不要想一口吃成个胖子,不要过早深究原理,我们的目标是借助引擎的能力来实现3D效果,除此以外还有两件小事需要留意。
开发环境
理论可以不借助任何第三方库运行orillusion框架,所以我们先体验一下最简化的版本。
但是随着功能的增多,推荐node+vite+vue这套工具链,当然可以用其它任何熟悉的开发工具完全可以,所以建议请安装好nodejs,并配置好环境变量,同时假设vue3已经可以正常使用。这里我使用vite作为项目创建工具,使用vscode做编辑器,没有其它的前置条件,轻装上阵。如果这一步有什么疑问,请自行查找资料,相信聪明的你不会被难住。运行环境
由于WebGPU标准并未正式推出,需要使用开发浏览器,这里推荐 chrome canary ,下载后在在canary中运行效果,由于canary仍然在变动中,需要在地址栏中输入chrome://flags/#enable-unsafe-webgpu
,然后开启Unsafe WebGPU
选项。
如果无法开启或者没有效果,需要搜索最新的开启方法,相信不久的将来在正式版就可以附带WebGPU支持了,不需要这么繁琐了。
同时为了做两手准备,也可以考虑用 Edge Canary 来应个急,下载后正常安装,启动后在地址栏中输入edge://flags/#enable-unsafe-webgpu
,开启WebGPU支持。
在浏览器正式版本支持WebGPU之前,关于浏览器的支持情况会经常变化,如果不能正确访问,建议多寻找一下当下的信息。
chrome最新版 113 已经正式支持 WebGPU 特性,可以直接使用了,终于可以告别混乱的史前时代了。Hello3D
参照官方文档,使用CDN直接引用Orullision引擎库是最简单的方式,为了做最简单的体验不引用任何第三方。创建一个html文件,使用任何一款支持资源加载的服务软件都可以直接运行。比如nginx、tomcat、live Server等。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Hello Orillusion</title> <script src="https://cdn.orillusion.com/orillusion.umd.js"></script> </head> <body> <script type="importmap"> { "imports": { "@orillusion/core": "https://cdn.orillusion.com/orillusion.es.js" } } </script> <script type="module"> import { Engine3D, Camera3D, Object3D, Scene3D, HoverCameraController, MeshRenderer, BoxGeometry, LitMaterial, View3D } from "@orillusion/core" // 初始化引擎 await Engine3D.init(); // 创建场景对象 let scene = new Scene3D(); //创建相机对象 let cameraObj = 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); // 添加相机对象至场景 scene.addChild(cameraObj); // 创建一个实体对象 let boxObj = new Object3D(); // 创建渲染组件 let mr = boxObj.addComponent(MeshRenderer); // 创建几何体,并添加至网格 mr.geometry = new BoxGeometry(5, 5, 5); // 创建一个物理材质并添加至 mr.material = new LitMaterial(); // 将实体对象添加到场景 scene.addChild(boxObj); // 创建View3D对象 let view = new View3D(); // 指定渲染的场景 view.scene = scene; // 指定使用的相机 view.camera = camera; // 开始渲染 Engine3D.startRenderView(view); </script> </body> </html>
运行后可以在浏览器直接查看效果,如果出现问题请查看控制台日志,比较容易出错的是WebGPU支持问题,请注意查看准备工作部分。
第一个demo力求简单化,所以显示效果会看起来比较单调,在一个白色的场景下一个黑色的立方体,用鼠标可以调整视角。在后续的示例中可以不断的完善丰富示例效果。创建项目
以一个用户数比较多的vue3作为示例(当然用react或完全不用框架完全可行),使用从头开始搭建一个Orillusion项目。
这里我们先创建一个普通的vue3项目,再将引擎引入项目,操练起来吧,很快就能看到我们在做什么了。- 创建根目录
a. 选择一个文件夹,创建orillusion
目录
b. 进入orillusion
,并在该目录下打开命令提示行 - 创建项目
a. 执行命令npm create vite@latest
b. Project name 下输入hello3d
作为项目名并回车
c. 使用键盘方向键选择vue
作为框架,并回车
d. 仍然使用键盘方向键选择vue-ts
作为项目类型,并回车,已经创建了hello3d
项目目录结构
e. 输入cd hello3d
进入项目目录
f. 输入npm install
安装依赖包,至此,基本的vue3项目已经创建完成 - 安装 orillusion
a. 输入npm install @orillusion/core --save
安装orillusion
开发包(如果安装失败,请注意分辨网络原因或安装权限)
b. 输入npm run dev
,可以看到在5173端口启动了服务
c. 打开 canary 浏览器,输入完整地址可以看到显示了vue3的欢迎页,至此项目已经安装完成,后面进行开发。
编写代码
- 在 vscode 中打开
hello3d
目录 - 在
src
目录下新建目录demo
- 在 demo 目录下创建文件
hello.ts
import { Engine3D, Scene3D, Object3D, Camera3D, ForwardRenderJob, LitMaterial, 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 LitMaterial(); // 添加到场景 scene3D.addChild(obj); let view = new View3D(); // 指定渲染的场景 view.scene = scene3D; // 指定使用的相机 view.camera = camera; // 开始渲染 Engine3D.startRenderView(view); } }
- 修改App.vue文件
<template> </template> <script lang="ts" setup> import { onMounted } from 'vue'; import Hello from './demo/hello'; onMounted(()=>{ new Hello().run(); }) </script> <style scoped> </style>
可以看到核心代码和cdn引入方式基本相同,这里请允许有一些冗余的存在,以便对与我一样的小白更友好一些。
完成项目
可以看到,创建完一个项目后,只添加了hello.ts文件和修改了App.vue。这样一个真正的3D项目开发完成了,再在浏览器里面看一下运行效果(不要忘记在chrome 113 以上版本)。
你得到了一个可以用鼠标操作的立方体,按住左键拖拽可以任意旋转,滑动滚动可以调整距离,按住右键拖拽可以快速调整立方体位置,可以多操作一下熟悉一下这种最常见的操作方式。
回忆我刚运行这一步时还是有点激动的,终于迈进了3D技术的大门,尽管是借用了引擎的助力,不过我们程序员就要擅长找到适合自己的工具。代码解析
作为3D小白,我的目标是始于orillusion但不会止于某一引擎,通过充分熟悉一个引擎的用法来积累3D知识。可以看到每一个类都对应着3D世界的基础概念,在这里做一个简单的对应,顺便看看一个3D世界由哪些部分组成的,当然这里只有一个基础印象即可,不必深究。
- 立方体:我们看到的立方体,是一个基础几何体,由类
BoxGeometry
来创建,实例化这个类在构造函数中指定长 宽 高
,引擎就会为我们绘制出一个立方体,就像我们看到的这样。 - 相机:相机是一个比较抽象但是在3D中无处不在,可以理解成我们的眼睛,我们是通过相机来观察3D内景物的,比如前面的立方体要在相机的可视范围内我们才能看到,在这里用组件
Camera3D
来定义。 - 场景:是一个容器,前面的立方体、相机等等还有其它的对象、组件都是要添加到场景内才能被引擎组织和使用的,我们创建一个3D程序必须有一个场景,我们可以通过
Scene3D
类创建场景
这里只介绍了三个最基础的组成部分,几何体、相机、场景,这三个部分在每一个3D程序中都是最基础的不可少的,以后我们会逐渐深入了解他们,也将会慢慢知道更多的工具。
小结
这篇文章是个开篇,快速上手了一个入门级别的3D示例,主要的作用并不是学习3D的基本技能,可以说更重要的是消除疑惑,最简单的上手3D项目。不得不说orillusion没有让我失望,以我目前这么简单的诉求也不会让我失望吧。
作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!
- 创建根目录