Skip to content

用户界面(GPU GUI)

Orillusion 提供了高性能的用户界面(GUI)组件供开发者使用。 通过合理搭配使用 GUI 组件,即可在项目中灵活展示 2D/3D 的 GUI 内容。本章我们先了解几个 GUI 的基本概念:

GUI 空间模式

目前 GUI 支持两种模式渲染 ViewSpaceWorldSpace

  • ViewSpace 模式:在这种模式下,GUI 组件被渲染在屏幕空间中,不随相机的视角更改而变动,也不会与其它物体产生 3D 空间遮挡关系;
  • WorldSpace 模式:在这种模式下,GUI 组件可看做三维空间的一块画布,拥有3D属性(旋转、缩放、平移),能够参与深度检测等,实现与其他对象遮挡和被遮挡关系。
ts
import { ViewPanel, WorldPanel } from '@orillusion/core'

// 创建一个面板对象
let panelRoot: Object3D = new Object3D()
// 添加 ViewPanel,设定为 ViewSpace 模式
panelRoot.addComponent(ViewPanel)
// 或 添加 WorldPanel,设定为 WorldSpace 模式
panelRoot.addComponent(WorldPanel)

下面这个例子展示了 ViewPanelWorldPanel 的区别:

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Scene3D, Object3D, Camera3D, View3D, ViewPanel, TextAnchor, UITextField, HoverCameraController, AtmosphericComponent, BitmapTexture2D, UIImage, makeAloneSprite, WorldPanel, GPUCullMode, UIPanel } from '@orillusion/core'
import * as dat from 'dat.gui'

// initializa engine
await Engine3D.init()
// create new scene as root node
let scene3D: Scene3D = new Scene3D()
scene3D.addComponent(AtmosphericComponent)
// create camera
let cameraObj: Object3D = new Object3D()
let camera = cameraObj.addComponent(Camera3D)
// adjust camera view
camera.perspective(60, Engine3D.aspect, 1, 5000.0)
// set camera controller
let controller = cameraObj.addComponent(HoverCameraController)
controller.setCamera(0, -20, 100)
// add camera node
scene3D.addChild(cameraObj)

let view = new View3D()
view.scene = scene3D
view.camera = camera
Engine3D.startRenderView(view)

// create a UICanvas
let canvas = view.enableUICanvas()
// create view sapce panel
let viewPanel: Object3D = new Object3D()
viewPanel.addComponent(ViewPanel)
// add to UICanvas
canvas.addChild(viewPanel)

// create world sapce panel
let worldPanel: Object3D = new Object3D()
worldPanel.localScale.set(0.15, 0.15, 0.15)
let panel: UIPanel = worldPanel.addComponent(WorldPanel)
// render double side
panel.cullMode = GPUCullMode.none
// add to UICanvas
canvas.addChild(worldPanel)

// load a BitmapTexture2D
let bitmapTexture2D = new BitmapTexture2D()
bitmapTexture2D.flipY = true
await bitmapTexture2D.load('https://cdn.orillusion.com/images/webgpu.png')

// create image node
let imageQuad = new Object3D()
viewPanel.addChild(imageQuad)
// create UIImage component
let image: UIImage = imageQuad.addComponent(UIImage)
// set image size
image.uiTransform.resize(320, 320)
// set image source
image.sprite = makeAloneSprite('webgpu', bitmapTexture2D)

let GUIHelp = new dat.GUI()
let f = GUIHelp.addFolder('GUI Space')
let params = {
    ViewSpace: () => {
        viewPanel.addChild(imageQuad)
    },
    WorldSpace: () => {
        worldPanel.addChild(imageQuad)
    }
}
f.add(params, 'ViewSpace')
f.add(params, 'WorldSpace')
f.open()

UICanvas

GUI 组件同样需要画布进行绘制,引擎中每个 View3D 中都内置有 Canvas 的数组,我们可以通过指定 enableUICanvas 来主动激活 UICanvas 对象:

ts
let view = new View3D()
...
let canvas:UICanvas = view.enableUICanvas();

默认情况下,我们只需要一个 UICanvas 即可, 如果需要多个画布绘制,我们可以通过设置不同的 index 来激活多个 UICanvas,它们互相独立:

ts
let canvas0:UICanvas = view.enableUICanvas(0);
let canvas1:UICanvas = view.enableUICanvas(1);
let canvas2:UICanvas = view.enableUICanvas(2);
//...

以下示例展示了多个 UICanvas 共存的表现:

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Object3D, UIImage, ImageType, Color, UIPanel, ViewPanel, Scene3D, Vector2, UITextField, UIShadow, AtmosphericComponent, Camera3D, HoverCameraController, View3D } from '@orillusion/core'

class Sample_UIMultiCanvas {
    async run() {
        Engine3D.setting.shadow.autoUpdate = true

        // initializa engine
        await Engine3D.init()
        // load fnt
        await Engine3D.res.loadFont('https://cdn.orillusion.com/fnt/0.fnt')
        // create new scene as root node
        let scene3D: Scene3D = new Scene3D()
        scene3D.addComponent(AtmosphericComponent)
        // create camera
        let cameraObj: Object3D = new Object3D()
        let camera = cameraObj.addComponent(Camera3D)
        // adjust camera view
        camera.perspective(60, Engine3D.aspect, 1, 5000.0)
        // set camera controller
        let controller = cameraObj.addComponent(HoverCameraController)
        controller.setCamera(0, -20, 50)
        // add camera node
        scene3D.addChild(cameraObj)

        let view = new View3D()
        view.scene = scene3D
        view.camera = camera
        Engine3D.startRenderView(view)

        let total: number = 4
        for (let i = 0; i < total; i++) {
            let size: Vector2 = new Vector2()
            size.x = 500 - i * 100
            size.y = 400 - i * 100
            this.createPanel(scene3D, i, size)
        }
    }

    private createPanel(scene: Scene3D, index: number, size: Vector2): UIPanel {
        let panelRoot: Object3D = new Object3D()
        // enable ui canvas at index
        let canvas = scene.view.enableUICanvas(index)
        let panel = panelRoot.addComponent(ViewPanel)
        canvas.addChild(panel.object3D)
        // create image
        let obj3D = new Object3D()
        panelRoot.addChild(obj3D)
        let image = obj3D.addComponent(UIImage)
        image.isShadowless = true
        image.imageType = ImageType.Sliced
        image.uiTransform.resize(size.x, size.y)
        image.color = Color.random()

        //text
        let text = obj3D.addComponent(UITextField)
        text.text = 'Canvas index: ' + index
        text.fontSize = 24

        //shadow
        let shadow = obj3D.addComponent(UIShadow)
        shadow.shadowOffset.multiplyScaler(0.4)
        return panel
    }
}

new Sample_UIMultiCanvas().run()

UIPanel

面板 UIPanel 用于承载具体的 GUI 组件渲染,需要添加到 UICanvas 中;

ts
let panelObj = new Object3D();
let panel:UIPanel = panelObj.addComponent(ViewPanel) // 创建一个屏幕空间面板组件
let canvas:UICanvas = view.enableUICanvas(); // 启用默认的 UICanvas
canvas.addChild(panel.object3D); // 添加面板

每个 UIPanel 可以视为 GUI 组件的根容器,在 UIPanel 内可以添加其它类型的 GUI 组件:

ts
// 创建一个 UIImage 组件
let imageQuad = new Object3D();
let image:UIImage = imageQuad.addComponent(UIImage);
// 创建一个 UIPanel
let panel:UIPanel = new Object3D().addComponent(ViewPanel); // 创建一个屏幕空间面板组件
// 将 UIImage 的 Object3D 添加到 UIPanel 的 Object3D 中
panel.object3D.addChild(imageQuad);

渲染顺序

在同一个 UICanvas 下,可以允许有多个 ViewPanel 或者 WorldPanel 共存,它们的渲染层级满足以下规则:

  1. ViewPanel 总是会显示在 WorldPanel 之上。
  2. ViewPanel 之间通过属性 panelOrder 控制绘制优先级。相同 panelOrder 下,根据它们所挂载的 Object3D 在场景树中的顺序为准。
  3. WorldPanel 之间通过属性 panelOrder 控制绘制优先级。相同 panelOrder 下,可以通过 needSortOnCameraZ 来让 UIPanel 根据相机远近距离自动排序。
ts
let panel1 = new Object3D().addComponent(ViewPanel);
let panel2 = new Object3D().addComponent(ViewPanel);
let panel3 = new Object3D().addComponent(WorldPanel);
let panel4 = new Object3D().addComponent(WorldPanel);

// 手动设置 panelOrder, panel2 遮挡 panel1
panel1.panelOrder = 1
panel2.panelOrder = 2

// ViewPanel 的 panel1/2 永远遮挡 WorldPanel 的 panel3/4
panel3.panelOrder = 3
panel4.panelOrder = 4 // panel4 优先遮挡 panel3

// 若 panelOrder 相同,自动根据相机位置排序
panel3.panelOrder = panel4.panelOrder = 3
panel3.needSortOnCameraZ = true;
panel4.needSortOnCameraZ = true;

WorldPanel

WorldPanel 组件相较于 ViewPanel 拥有更多的属性和功能:

相机锁定

我们可以通过设置面板的 billboard 属性来控制面板的渲染角度:

ts
let panel = new Object3D().addComponent(WorldPanel);
panel.billboard = BillboardType.None;           //默认视角,保持物体本身的渲染角度
panel.billboard = BillboardType.BillboardY;     //锁定Y轴,面板的XZ平面始终朝向相机方向
panel.billboard = BillboardType.BillboardXYZ;   //面板始终朝向相机

深度测试

设置面板是否参与深度排序:

ts
let panel = new Object3D().addComponent(WorldPanel);
panel.depthTest = true;      //参与深度排序,获得遮挡关系
panel.depthTest = false;     //不参与深度排序,始终悬浮于所有物体的表面

剔除模式

材质剔除 类似,我们也可以设置 UIPanel 渲染材质球的 cullMode 来切换剔除方式:

ts
let panel = new Object3D().addComponent(WorldPanel);
panel.cullMode = GPUCullMode.none; // 双面显示
panel.cullMode = GPUCullMode.front; // 前面剔除,背面显示
panel.cullMode = GPUCullMode.back; // 默认背面剔除,前面显示

下面这个例子,集中展示了面板之间的空间关系和 WorldPanel 的渲染特性:

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Object3DUtil, Object3D, UIImage, ImageType, Color, WorldPanel, UIPanel, GUICanvas, BillboardType, AtmosphericComponent, Camera3D, HoverCameraController, Scene3D, View3D, GPUCullMode, ViewPanel } from '@orillusion/core'
import * as dat from 'dat.gui'

class Sample_UIPanelOrder {
    GUIHelp: dat.GUI
    async run() {
        // initializa engine
        await Engine3D.init()
        // create new scene as root node
        let scene3D: Scene3D = new Scene3D()
        scene3D.addComponent(AtmosphericComponent)
        // create camera
        let cameraObj: Object3D = new Object3D()
        let camera = cameraObj.addComponent(Camera3D)
        // adjust camera view
        camera.perspective(60, Engine3D.aspect, 1, 5000.0)
        // set camera controller
        let controller = cameraObj.addComponent(HoverCameraController)
        controller.setCamera(0, -10, 100)
        // add camera node
        scene3D.addChild(cameraObj)

        let view = new View3D()
        view.scene = scene3D
        view.camera = camera
        Engine3D.startRenderView(view)

        // create floor
        let floor = Object3DUtil.GetSingleCube(100, 2, 50, 0.5, 0.5, 0.5)
        scene3D.addChild(floor)
        floor.y = -40

        // enable ui canvas at index 0
        let canvas = scene3D.view.enableUICanvas()
        //create UI root
        let panelRoot: Object3D = new Object3D()
        panelRoot.name = 'WorldPanel red'
        panelRoot.scaleX = panelRoot.scaleY = panelRoot.scaleZ = 0.1

        let panelRoot2: Object3D = new Object3D()
        panelRoot2.name = 'WorldPanel blue'
        panelRoot2.z = 20
        panelRoot2.y = -10
        panelRoot2.x = -10
        panelRoot2.scaleX = panelRoot2.scaleY = panelRoot2.scaleZ = 0.1

        let panelRoot3: Object3D = new Object3D()
        panelRoot3.name = 'ViewPanel Green'

        this.GUIHelp = new dat.GUI()
        this.createPanel(panelRoot, canvas, new Color(1.0, 0, 0.0, 0.8), 'world')
        this.createPanel(panelRoot2, canvas, new Color(0, 0, 1, 0.8), 'world')
        this.createPanel(panelRoot3, canvas, new Color(0, 1, 0, 0.5), 'view')
    }

    private createPanel(panelRoot: Object3D, canvas: GUICanvas, color: Color, type: string) {
        let f = this.GUIHelp.addFolder(panelRoot.name)
        if (type === 'world') {
            let panel = panelRoot.addComponent(WorldPanel)
            f.add(panel, 'panelOrder', 0, 10, 1)
            panel.billboard = BillboardType.BillboardXYZ
            panel.needSortOnCameraZ = true
            f.add(panel, 'needSortOnCameraZ')
            f.add({ cullMode: GPUCullMode.none }, 'cullMode', {
                none: GPUCullMode.none,
                front: GPUCullMode.front,
                back: GPUCullMode.back
            }).onChange((v) => {
                panel.cullMode = v
            })
            f.add({ billboard: panel.billboard }, 'billboard', {
                None: BillboardType.None,
                Y: BillboardType.BillboardY,
                XYZ: BillboardType.BillboardXYZ
            }).onChange((v) => {
                panel.billboard = v
            })
            f.add(panel, 'depthTest')
        } else {
            let panel = panelRoot.addComponent(ViewPanel)
            f.add(panel, 'panelOrder', 0, 10, 1)
            canvas
        }
        f.open()

        // create a UIImage
        let obj3D = new Object3D()
        panelRoot.addChild(obj3D)
        let image = obj3D.addComponent(UIImage)
        image.imageType = ImageType.Sliced
        image.uiTransform.resize(400, 300)
        image.color = color

        canvas.addChild(panelRoot)
    }
}

new Sample_UIPanelOrder().run()