Skip to content

精灵 Sprite

精灵(Sprite)是引擎中用于在 3D 场景里绘制 2D 贴图四边形 的渲染组件,常用于标签、图标、地标(POI)、血条、广告牌、特效贴片等。

版本说明

精灵系统在 @orillusion/core 0.9.0 引入,取代了早期版本的 GUI 组件体系。早期的 UIPanel / UIImage / UITextField / UIButton 等 GUI 组件已移除,相关需求统一通过 SpriteRenderer(配合 公告板 等)实现。

基本用法

给一个 Object3D 添加 SpriteRenderer 组件,设置贴图即可在其所在位置绘制一张面片:

ts
import { Engine3D, Scene3D, View3D, CameraUtil, HoverCameraController,
         Object3D, SpriteRenderer, BitmapTexture2D, Vector2 } from '@orillusion/core';

const engine = await Engine3D.init();
const scene = new Scene3D();

const camera = CameraUtil.createCamera3DObject(scene);
camera.perspective(60, engine.aspect, 0.1, 5000);
camera.object3D.addComponent(HoverCameraController).setCamera(0, -15, 10);

const view = new View3D();
view.scene = scene;
view.camera = camera;
engine.startRenderView(view);

// 加载贴图
const tex = new BitmapTexture2D();
tex.flipY = true;
await tex.load('textures/logo.png');

// 创建精灵
const spriteObj = new Object3D();
const sprite = spriteObj.addComponent(SpriteRenderer);
sprite.texture = tex;
scene.addChild(spriteObj);

// 注意:部分属性需在 addChild 之后设置(addChild 会触发材质初始化)
sprite.size = new Vector2(2, 2);   // 面片世界尺寸(米)

常用属性

属性类型说明
textureTexture精灵贴图,可用 sprite.texture = texsprite.setTexture(tex) 设置
sizeVector2面片在世界空间的宽高(米)
pivotVector2锚点(0~1)。(0.5, 0.5) 居中,(0.5, 0) 底边中点(标签常用)
colorColor叠加颜色 / 透明度(与贴图相乘)
cornerRadiusnumber圆角半径(世界单位),基于 SDF 圆角,0 为直角
uvRectVector4采样的 UV 子区域 (x, y, w, h),用于图集
renderOrdernumber透明排序顺序,数值大的后绘制(叠在上层)
distanceInvariantSizeboolean是否保持屏幕尺寸恒定,详见距离恒定
ts
import { Color, Vector2 } from '@orillusion/core';

sprite.size = new Vector2(2, 2);
sprite.pivot = new Vector2(0.5, 0.5);
sprite.color = new Color(1, 0.85, 0.4, 1);
sprite.cornerRadius = 0.2;

渲染顺序

多个精灵重叠时,可用 renderOrder 控制透明桶内的绘制先后(数值越大越靠上层):

ts
cardA.renderOrder = 3000;
cardB.renderOrder = 3001; // 叠在 A 之上
cardC.renderOrder = 3002; // 最上层

示例

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

<
ts
import {AtmosphericComponent, BillboardComponent, BillboardType, BitmapTexture2D, CameraUtil, DirectLight, Engine3D, HoverCameraController, Object3D, Scene3D, SpriteRenderer, Vector2, View3D } from "@orillusion/core";
import * as dat from 'dat.gui';

class Sample_Basic {
    private sprite!: SpriteRenderer;
    private spriteObj!: Object3D;
    private billboard!: BillboardComponent;
    private gui!: dat.GUI;

    private readonly s = {
        width: 2,
        height: 2,
        x: 0,
        y: 2,
        z: 0,
        cornerRadius: 0,
        billboard: BillboardType.None,
    };

    async run() {
        const engine = await Engine3D.init({});
        const scene = new Scene3D();
        const sky = scene.addComponent(AtmosphericComponent);
        
        this.gui = new dat.GUI();

        const camera = CameraUtil.createCamera3DObject(scene);
        camera.perspective(60, engine.aspect, 0.1, 5000);
        camera.object3D.addComponent(HoverCameraController).setCamera(0, -15, 10);

        const view = new View3D();
        view.scene = scene;
        view.camera = camera;
        engine.startRenderView(view);

        // Sun light for the atmospheric sky
        const lightObj = new Object3D();
        lightObj.rotationX = 45; lightObj.rotationY = 110;
        lightObj.addComponent(DirectLight).intensity = 3;
        scene.addChild(lightObj);
        sky.relativeTransform = lightObj.transform;

        // Load a texture
        const tex = new BitmapTexture2D();
        tex.flipY = true;
        await tex.load('https://cdn.orillusion.com/textures/KB3D_NTT_Ads_basecolor.png');

        // The sprite
        this.spriteObj = new Object3D();
        this.sprite = this.spriteObj.addComponent(SpriteRenderer);
        this.sprite.texture = tex;
        this.spriteObj.localPosition.set(this.s.x, this.s.y, this.s.z);
        scene.addChild(this.spriteObj);

        // Apply the initial state values to the sprite AFTER addChild
        // (addChild triggers material init which recomputes renderOrder
        // and any pre-attach sprite state you'd set through the renderer).
        this.sprite.size = new Vector2(this.s.width, this.s.height);
        this.sprite.cornerRadius = this.s.cornerRadius;

        // Billboard is composed as a separate component
        this.billboard = this.spriteObj.addComponent(BillboardComponent);
        this.billboard.type = this.s.billboard;

        this.initGUI();
    }

    private initGUI() {
        const s = this.s;
        const folder = this.gui.addFolder('Basic');
        folder.add(s, 'width',  0.2, 10, 0.1).onChange(v => this.sprite.size = new Vector2(v, s.height));
        folder.add(s, 'height', 0.2, 10, 0.1).onChange(v => this.sprite.size = new Vector2(s.width, v));
        folder.add(s, 'x', -10, 10, 0.1).onChange(v => this.spriteObj.x = v);
        folder.add(s, 'y', -10, 10, 0.1).onChange(v => this.spriteObj.y = v);
        folder.add(s, 'z', -10, 10, 0.1).onChange(v => this.spriteObj.z = v);
        folder.add(s, 'cornerRadius', 0, 1, 0.01).onChange(v => this.sprite.cornerRadius = v);
        folder.add(s, 'billboard', {
            None: BillboardType.None,
            'Billboard Y': BillboardType.BillboardY,
            'Billboard XYZ': BillboardType.BillboardXYZ,
        }).onChange(v => this.billboard.type = Number(v));
        folder.open();
    }
}

new Sample_Basic().run();

下一步