Skip to content

多面板示例

下面这个例子,使用两个 WorldPanel 演示在实际3D场景中两种经典的面板使用场景:

  1. 其中一个面板,固定在3D场景中,面板内置有一个文本组件,文本信息可以动态实时更新内容
  2. 另一个面板,它被绑定在另一个3D节点上,可以随着父节点实时更新位置;且可以通过设置 billboard 类型来锁定其旋转角度,让面板始终朝向相机视角;配合 depthTest 可以实现不会被遮挡的标注效果

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

<
ts
import { Scene3D, PropertyAnimation, Engine3D, Object3D, Object3DUtil, PropertyAnimClip, WrapMode, WorldPanel, BillboardType, TextAnchor, UIImage, UIShadow, UITextField, Vector3, Color, Time, AtmosphericComponent, Camera3D, GPUCullMode, HoverCameraController, UIPanel, View3D, DirectLight } from '@orillusion/core';
import * as dat from 'dat.gui';

class Sample_POI {
    scene: Scene3D;
    panel: WorldPanel;
    position: Vector3;
    gui: dat.GUI;

    async run() {
        Engine3D.setting.shadow.autoUpdate = true;
        Engine3D.setting.shadow.updateFrameRate = 1;
        Engine3D.setting.shadow.shadowBound = 20;
        Engine3D.setting.shadow.shadowBias = 0.001;

        // initializa engine
        await Engine3D.init({ renderLoop: () => this.loop() });
        // 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, 15);
        // add camera node
        scene3D.addChild(cameraObj);

        let lightObj = new Object3D();
        lightObj.rotationX = 45;
        let dl = lightObj.addComponent(DirectLight);
        dl.intensity = 2;
        dl.castShadow = true;
        scene3D.addChild(lightObj);

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

        this.scene = scene3D;

        await this.initScene();
        this.gui = new dat.GUI();
        this.initDuckPOI();
        this.initScenePOI();
    }

    private modelContainer: Object3D;

    async initScene() {
        // floor
        let floor: Object3D = Object3DUtil.GetSingleCube(16, 0.1, 16, 1, 1, 1);
        this.scene.addChild(floor);
        await Engine3D.res.loadFont('https://cdn.orillusion.com/fnt/0.fnt');

        // load external model
        let model = (await Engine3D.res.loadGltf('https://cdn.orillusion.com/PBR/Duck/Duck.gltf')) as Object3D;
        model.rotationY = 180;
        this.modelContainer = new Object3D();
        this.modelContainer.addChild(model);
        this.scene.addChild(this.modelContainer);
        model.scaleX = model.scaleY = model.scaleZ = 0.01;
        await this.initPropertyAnim(this.modelContainer);

        let chair = (await Engine3D.res.loadGltf('https://cdn.orillusion.com/PBR/SheenChair/SheenChair.gltf')) as Object3D;
        chair.scaleX = chair.scaleY = chair.scaleZ = 8;
        this.scene.addChild(chair);
    }

    private async initPropertyAnim(owner: Object3D) {
        // add PropertyAnimation
        let animation = owner.addComponent(PropertyAnimation);

        //load a animation clip
        let json: any = await Engine3D.res.loadJSON('https://cdn.orillusion.com/json/anim_0.json');
        let animClip = new PropertyAnimClip();
        animClip.parse(json);
        animClip.wrapMode = WrapMode.Loop;
        animation.defaultClip = animClip.name;
        animation.autoPlay = true;

        // register clip to animation
        animation.appendClip(animClip);
        animation.play(animation.defaultClip);
        return animation;
    }

    private initDuckPOI() {
        let canvas = this.scene.view.enableUICanvas();
        //panel
        this.panel = new Object3D().addComponent(WorldPanel);
        this.panel.billboard = BillboardType.BillboardXYZ;
        //add to canvas
        canvas.addChild(this.panel.object3D);
        this.panel.object3D.localScale = new Vector3(0.1, 0.1, 0.1);

        //poi
        let panelRoot = new Object3D();

        this.panel.object3D.addChild(panelRoot);

        let image = panelRoot.addComponent(UIImage);
        image.uiTransform.resize(32, 6);
        image.uiTransform.setXY(20, 20);

        image.color = new Color(1, 1, 1, 0.5);
        image.isShadowless = true;
        let text = panelRoot.addComponent(UITextField);

        text.text = 'Happy Duck';
        text.fontSize = 4;
        text.color = new Color(0, 0, 0, 1);
        text.alignment = TextAnchor.MiddleCenter;
        this.renderUIPanel(this.panel, 'Duck Panel');
    }

    private sceneText: UITextField;
    private initScenePOI() {
        let canvas = this.scene.view.enableUICanvas();
        //panel
        let panel = new Object3D().addComponent(WorldPanel);
        panel.cullMode = 'none';
        //add to canvas
        canvas.addChild(panel.object3D);
        panel.object3D.localScale = new Vector3(0.1, 0.1, 0.1);

        //poi
        let panelRoot = new Object3D();
        panel.transform.rotationX = -30;
        panel.transform.y = 3.1;
        panel.transform.x = 1;

        panel.object3D.addChild(panelRoot);
        let text = panelRoot.addComponent(UITextField);
        text.uiTransform.resize(80, 16);
        text.text = this.title;
        text.fontSize = 10;
        text.color = new Color(0.5, 1.0, 0.5, 1.0);
        text.alignment = TextAnchor.MiddleLeft;

        panelRoot.addComponent(UIShadow).shadowOffset.multiplyScaler(0.2);
        this.sceneText = text;

        this.renderUIPanel(panel, 'Chair Panel');
    }

    private charCount = 0;
    private title: string = 'Hello, Orillusion';
    private lastTitle = this.title;
    private loop(): void {
        if (this.panel) {
            this.position ||= new Vector3();
            this.position.copyFrom(this.modelContainer.transform.worldPosition);
            this.panel.object3D.localPosition = this.position;
        }
        if (this.sceneText) {
            let count = 1 + (Math.floor(Time.frame * 0.1) % 30);
            if (this.charCount != count) {
                this.charCount = count;
                let newTitle = this.title.slice(0, this.charCount);
                if (newTitle != this.lastTitle) {
                    this.sceneText.text = newTitle;
                    this.lastTitle = newTitle;
                }
            }
        }
    }

    renderUIPanel(panel: WorldPanel, name: string = 'GUI Panel') {
        let f = this.gui.addFolder(name);
        //cull mode
        let cullMode = {};
        cullMode[GPUCullMode.none] = GPUCullMode.none;
        cullMode[GPUCullMode.front] = GPUCullMode.front;
        cullMode[GPUCullMode.back] = GPUCullMode.back;

        // change cull mode by click dropdown box
        f.add({ cullMode: GPUCullMode.none }, 'cullMode', cullMode).onChange((v) => {
            panel.cullMode = v;
        });

        //billboard
        let billboard = {};
        billboard['None'] = BillboardType.None;
        billboard['Y'] = BillboardType.BillboardY;
        billboard['XYZ'] = BillboardType.BillboardXYZ;

        // change billboard by click dropdown box
        f.add({ billboard: panel.billboard }, 'billboard', billboard).onChange((v) => {
            panel.billboard = v;
        });

        //depth test
        f.add(panel, 'depthTest');
        f.open();
    }
}

new Sample_POI().run();

下面这个例子,集中展示了多种 GUI 组件组合以及多 UIPanel 混合渲染:

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

<
ts
import { AtmosphericComponent, BillboardType, CEvent, CEventDispatcher, Camera3D, Color, DirectLight, Engine3D, HoverCameraController, ImageType, Object3D, Object3DUtil, PointerEvent3D, Scene3D, TextAnchor, Time, UIImage, UIInteractive, UITextField, Vector3, View3D, WorldPanel, clamp } from '@orillusion/core';

class GUIPanelPOI {
    private readonly alpha = 0.8;
    private objUI: Object3D;
    private index: number;
    private _originColor: Color = new Color();
    private _remainTime: number = -1;
    private _backImage: UIImage;
    private _outColor = new Color(0, 0.5, 0.75, this.alpha);

    constructor(obj, index: number) {
        this.objUI = obj;
        this.index = index;
        this.displayUIDetail();
    }

    update(delta: number): void {
        if (this._remainTime > 0) {
            this._remainTime -= delta;
            let progress = clamp(this._remainTime, 0, 500);
            progress = 1 - progress / 500;
            let color = this._backImage.color;
            color.r = this._originColor.r * progress + (1.0 - progress) * 0.2;
            color.g = this._originColor.g * progress + (1.0 - progress) * 0.2;
            color.b = this._originColor.b * progress + (1.0 - progress) * 0.2;
            this._backImage.color = color;
        }
        this.updateFrame();
    }

    private lastIndex: number = -1;
    private frame: number = Math.floor(Math.random() * 10000);
    private frameStart = 65; //65~77
    private frameCount = 13;
    private _icon: UIImage;

    private _frameSpeed = 0.05 + 0.1 * Math.random();

    updateFrame() {
        this.frame++;
        let newIndex = Math.floor(this.frame * this._frameSpeed) % this.frameCount;
        if (newIndex != this.lastIndex) {
            this.lastIndex = newIndex;
            let frameKey = (this.lastIndex + this.frameStart).toString().padStart(5, '0');
            this._icon.sprite = Engine3D.res.getGUISprite(frameKey);
        }
    }

    private displayUIDetail(): void {
        let uiChild = this.objUI.addChild(new Object3D()) as Object3D;

        let r = Math.random() * 0.25 + 0.2;
        let b = Math.random() * 0.25 + 0.2;
        let g = Math.random() * 0.25 + 0.2;
        this._originColor.setTo(r, g, b, this.alpha);
        //back
        this._backImage = this.addImage(uiChild, ' ', 200, 120, r, g, b, this.alpha);
        this._backImage.uiTransform.x = 100;
        this._backImage.uiTransform.y = -60;

        uiChild.addEventListener(
            PointerEvent3D.PICK_CLICK_GUI,
            () => {
                this._remainTime = 500;
                sampleUIPanelClick.data = this.objUI;
                sampleUIPanelDispatcher.dispatchEvent(sampleUIPanelClick);
            },
            this
        );

        uiChild.addEventListener(
            PointerEvent3D.PICK_OVER_GUI,
            () => {
                this._backImage.color = this._outColor;
            },
            this
        );

        uiChild.addEventListener(
            PointerEvent3D.PICK_OUT_GUI,
            () => {
                this._backImage.color = this._originColor;
            },
            this
        );

        let button = uiChild.addComponent(UIInteractive);
        button.interactive = true;

        //icon
        {
            let iconNode = uiChild.addChild(new Object3D()) as Object3D;
            let icon = this.addImage(iconNode, '', 100, 100, 1, 1, 1);
            icon.uiTransform.x = -75;
            icon.uiTransform.y = 25;
            this._icon = icon;
            this.updateFrame();
        }

        //text
        {
            let textChild = this.objUI.addChild(new Object3D()) as Object3D;
            let text = textChild.addComponent(UITextField);
            text.uiTransform.resize(120, 60);
            text.uiTransform.x = 110;
            text.uiTransform.y = -48;
            text.alignment = TextAnchor.UpperLeft;
            text.text = 'Orilussion';
            text.fontSize = 22;
            text.color = new Color(0.9, 0.9, 0.9, 1.0);
        }

        //text
        {
            let textChild = this.objUI.addChild(new Object3D()) as Object3D;
            let text = textChild.addComponent(UITextField);
            text.uiTransform.resize(140, 60);
            text.uiTransform.x = 110;
            text.uiTransform.y = -100;
            text.alignment = TextAnchor.UpperLeft;
            text.text = '次时代WebGPU 3D Engine';
            text.fontSize = 18;
            text.color = new Color(0.8, 0.8, 0.8, 1.0);
        }
    }

    private addImage(obj: Object3D, texture: string, w: number, h: number, r: number, g: number, b: number, a: number = 1): UIImage {
        let image = obj.addComponent(UIImage);
        image.sprite = Engine3D.res.getGUISprite(texture);
        image.uiTransform.resize(w, h);
        image.imageType = ImageType.Sliced;
        image.color.setTo(r, g, b, a);
        return image;
    }
}
class GUIPanelBinder {
    objUI: Object3D;
    panel: GUIPanelPOI;
    ball: Object3D;

    constructor(ball: Object3D, ui: Object3D, index: number) {
        this.ball = ball;
        this.objUI = ui;
        this.objUI.name = 'panel ' + index;
        this.objUI.scaleX = this.objUI.scaleY = this.objUI.scaleZ = 0.1;
        this.panel = new GUIPanelPOI(this.objUI, index);
    }

    update(delta: number) {
        this.objUI.localPosition = this.ball.transform.worldPosition;
        this.panel.update(delta);
    }
}

let sampleUIPanelClick: CEvent = new CEvent('ClickUIPanel');
let sampleUIPanelDispatcher: CEventDispatcher = new CEventDispatcher();

class Sample_UIMultiPanel {
    camera: Camera3D;
    scene: Scene3D;
    view: View3D;

    async run() {
        Engine3D.setting.shadow.autoUpdate = true;
        Engine3D.setting.shadow.shadowBias = 0.01;
        Engine3D.setting.shadow.shadowBound = 200;

        await Engine3D.init({
            renderLoop: () => {
                this.renderUpdate();
            }
        });

        // 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, 150, new Vector3(0, 15, 0));
        // add camera node
        scene3D.addChild(cameraObj);
        // create light
        let light: Object3D = new Object3D();
        // add direct light component
        let component: DirectLight = light.addComponent(DirectLight);
        // adjust lighting
        light.rotationX = 21;
        light.rotationY = 120;
        component.lightColor = new Color(1.0, 1.0, 1.0, 1.0);
        component.intensity = 5;
        component.castShadow = true;
        // add light object
        scene3D.addChild(light);

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

        let model = await Engine3D.res.loadGltf('https://cdn.orillusion.com/gltfs/wukong/wukong.gltf');
        model.localScale = new Vector3(1, 1, 1).multiplyScalar(50);

        this.scene.addChild(model);
        this.scene.addChild(Object3DUtil.GetSingleCube(400, 1, 400, 0.2, 0.2, 0.2));

        await Engine3D.res.loadFont('https://cdn.orillusion.com/fnt/0.fnt');
        await Engine3D.res.loadAtlas('https://cdn.orillusion.com/atlas/Sheet_atlas.json');

        this.makeUIPanelList();
    }

    private nodeList: GUIPanelBinder[] = [];
    private bindTarget3DRoot: Object3D;

    private makeUIPanelList(): void {
        this.bindTarget3DRoot = new Object3D();
        this.bindTarget3DRoot.y = 50;
        this.scene.addChild(this.bindTarget3DRoot);
        let canvas = this.view.enableUICanvas();

        for (let i = 0; i < 50; i++) {
            //panel
            let panelRoot: Object3D = new Object3D();
            let panel = panelRoot.addComponent(WorldPanel);
            panel.billboard = BillboardType.BillboardXYZ;
            panel.needSortOnCameraZ = true;
            canvas.addChild(panel.object3D);

            //random position
            let angle = Math.PI * 2 * Math.random();
            let pos = new Vector3();
            pos.set(Math.sin(angle), Math.cos(angle), (Math.random() - 0.5) * 2);
            pos.multiplyScalar(50 * Math.sqrt(Math.random() + 0.25));

            let ball = this.bindTarget3DRoot.addChild(new Object3D()) as Object3D;
            ball.localPosition = pos;

            //binder
            let node = new GUIPanelBinder(ball, panelRoot, i);
            this.nodeList.push(node);
        }

        sampleUIPanelDispatcher.addEventListener(
            sampleUIPanelClick.type,
            (e) => {
                let target = e.data as Object3D;
                let targetPos = this.view.camera.worldToScreenPoint(target.transform.worldPosition);
                let orginPos = this.view.camera.worldToScreenPoint(new Vector3());
                this.isSpeedAdd = targetPos.x > orginPos.x ? 1 : -1;
                this.speedAngle += 50;
                console.log(this.isSpeedAdd);
            },
            this
        );
    }
    private speedAngle: number = 1;
    private isSpeedAdd: number = 1;

    renderUpdate() {
        if (this.bindTarget3DRoot) {
            this.speedAngle -= 0.2;
            this.speedAngle = Math.max(this.speedAngle, 1);
            this.bindTarget3DRoot.rotationY += 0.01 * this.speedAngle * this.isSpeedAdd;

            for (let binder of this.nodeList) {
                binder.update(Time.delta);
            }
        }
    }
}

new Sample_UIMultiPanel().run();