chore: cleanup

This commit is contained in:
GuoLei1990
2025-11-29 00:43:01 +08:00
parent 89dff9a8d7
commit 6ae10cf0ed
102 changed files with 0 additions and 9227 deletions

View File

@@ -1,34 +0,0 @@
## Installation
To install, use:
```sh
npm install @galacean/engine-physics-lite
```
This will allow you to import engine entirely using:
```javascript
import * as PHYSICS_LITE from "@galacean/engine-physics-lite";
```
or individual classes using:
```javascript
import { LitePhysics } from "@galacean/engine-physics-lite";
```
## Usage
```typescript
// Create engine by passing in the HTMLCanvasElement id and adjust canvas size
const engine = await WebGLEngine.create({ canvas: "canvas-id" });
// Initialize physics manager with LitePhysics.
engine.physicsManager.initialize(LitePhysics);
......
// Run engine.
engine.run();
```

View File

@@ -1,37 +0,0 @@
{
"name": "@galacean/engine-physics-lite",
"version": "0.0.0-experimental-backup.4",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"repository": {
"url": "https://github.com/galacean/engine.git"
},
"license": "MIT",
"main": "dist/main.js",
"module": "dist/module.js",
"debug": "src/index.ts",
"browser": "dist/browser.js",
"types": "types/index.d.ts",
"scripts": {
"b:types": "tsc"
},
"umd": {
"name": "Galacean.PhysicsLite",
"globals": {
"@galacean/engine": "Galacean"
}
},
"files": [
"dist/**/*",
"types/**/*"
],
"devDependencies": {
"@galacean/engine-design": "workspace:*",
"@galacean/engine": "workspace:*"
},
"peerDependencies": {
"@galacean/engine": "workspace:*"
}
}

View File

@@ -1,100 +0,0 @@
import { ICollider } from "@galacean/engine-design";
import { Layer, Quaternion, Ray, Vector3 } from "@galacean/engine";
import { LiteHitResult } from "./LiteHitResult";
import { LiteColliderShape } from "./shape/LiteColliderShape";
import { LiteTransform } from "./LiteTransform";
import { LitePhysicsScene } from "./LitePhysicsScene";
import { LitePhysics } from "./LitePhysics";
/**
* Abstract class of physical collider.
*/
export abstract class LiteCollider implements ICollider {
/** @internal */
abstract readonly _isStaticCollider: boolean;
private _litePhysics: LitePhysics;
/** @internal */
_scene: LitePhysicsScene;
/** @internal */
_shapes: LiteColliderShape[] = [];
/** @internal */
_transform: LiteTransform = new LiteTransform();
/** @internal */
_collisionLayer: number;
protected constructor(litePhysics: LitePhysics) {
this._transform.owner = this;
this._litePhysics = litePhysics;
}
/**
* {@inheritDoc ICollider.addShape }
*/
addShape(shape: LiteColliderShape): void {
const oldCollider = shape._collider;
if (oldCollider !== this) {
if (oldCollider) {
oldCollider.removeShape(shape);
}
this._shapes.push(shape);
shape._collider = this;
this._scene?._addColliderShape(shape);
}
}
/**
* {@inheritDoc ICollider.removeShape }
*/
removeShape(shape: LiteColliderShape): void {
const index = this._shapes.indexOf(shape);
if (index !== -1) {
this._shapes.splice(index, 1);
shape._collider = null;
this._scene?._removeColliderShape(shape);
}
}
/**
* {@inheritDoc ICollider.setWorldTransform }
*/
setWorldTransform(position: Vector3, rotation: Quaternion): void {
this._transform.setPosition(position.x, position.y, position.z);
this._transform.setRotationQuaternion(rotation.x, rotation.y, rotation.z, rotation.w);
}
/**
* {@inheritDoc ICollider.getWorldTransform }
*/
getWorldTransform(outPosition: Vector3, outRotation: Quaternion): void {
const { position, rotationQuaternion } = this._transform;
outPosition.set(position.x, position.y, position.z);
outRotation.set(rotationQuaternion.x, rotationQuaternion.y, rotationQuaternion.z, rotationQuaternion.w);
}
/**
* {@inheritDoc ICollider.setCollisionLayer }
*/
setCollisionLayer(collisionLayer: Layer): void {
this._litePhysics.setColliderLayer(this, collisionLayer);
}
/**
* {@inheritDoc ICollider.destroy }
*/
destroy(): void {}
/**
* @internal
*/
_raycast(ray: Ray, onRaycast: (obj: number) => boolean, hit: LiteHitResult): boolean {
hit.distance = Number.MAX_VALUE;
const shapes = this._shapes;
for (let i = 0, n = shapes.length; i < n; i++) {
const shape = shapes[i];
onRaycast(shape._id) && shape._raycast(ray, hit);
}
return hit.distance != Number.MAX_VALUE;
}
}

View File

@@ -1,207 +0,0 @@
import { LiteCollider } from "./LiteCollider";
import { IDynamicCollider } from "@galacean/engine-design";
import { Logger, Quaternion, Vector3 } from "@galacean/engine";
import { LitePhysics } from "./LitePhysics";
/**
* A dynamic collider can act with self-defined movement or physical force
*/
export class LiteDynamicCollider extends LiteCollider implements IDynamicCollider {
/** @internal */
readonly _isStaticCollider: boolean = false;
/**
* Initialize dynamic actor.
* @param position - The global position
* @param rotation - The global rotation
*/
constructor(litePhysics: LitePhysics, position: Vector3, rotation: Quaternion) {
super(litePhysics);
this._transform.setPosition(position.x, position.y, position.z);
this._transform.setRotationQuaternion(rotation.x, rotation.y, rotation.z, rotation.w);
}
/**
* {@inheritDoc IDynamicCollider.getInertiaTensor }
*/
getInertiaTensor(out: Vector3): Vector3 {
Logger.error("Physics-lite don't support getInertiaTensor. Use Physics-PhysX instead!");
return out;
}
/**
* {@inheritDoc IDynamicCollider.getCenterOfMass }
*/
getCenterOfMass(out: Vector3): Vector3 {
Logger.error("Physics-lite don't support getCenterOfMass. Use Physics-PhysX instead!");
return out;
}
/**
* {@inheritDoc IDynamicCollider.setMassAndUpdateInertia }
*/
setMassAndUpdateInertia(mass: number): void {
Logger.error("Physics-lite don't support setMassAndUpdateInertia. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.addForce }
*/
addForce(force: Vector3): void {
throw "Physics-lite don't support addForce. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IDynamicCollider.addTorque }
*/
addTorque(torque: Vector3): void {
throw "Physics-lite don't support addTorque. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IDynamicCollider.move }
*/
move(positionOrRotation: Vector3 | Quaternion, rotation?: Quaternion): void {
throw "Physics-lite don't support move. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IDynamicCollider.sleep }
*/
sleep(): void {
throw "Physics-lite don't support putToSleep. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IDynamicCollider.isSleeping }
*/
isSleeping(): boolean {
throw "Physics-lite don't support isSleeping. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IDynamicCollider.setAngularDamping }
*/
setAngularDamping(value: number): void {
Logger.error("Physics-lite don't support setAngularDamping. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.getAngularVelocity }
*/
getAngularVelocity(out: Vector3): Vector3 {
Logger.error("Physics-lite don't support getAngularVelocity. Use Physics-PhysX instead!");
return out;
}
/**
* {@inheritDoc IDynamicCollider.setAngularVelocity }
*/
setAngularVelocity(value: Vector3): void {
Logger.error("Physics-lite don't support setAngularVelocity. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setCenterOfMass }
*/
setCenterOfMass(value: Vector3): void {
Logger.error("Physics-lite don't support setCenterOfMass. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setCollisionDetectionMode }
*/
setCollisionDetectionMode(value: number): void {
Logger.error("Physics-lite don't support setCollisionDetectionMode. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setConstraints }
*/
setConstraints(flags: number): void {
Logger.error("Physics-lite don't support setConstraints. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setInertiaTensor }
*/
setInertiaTensor(value: Vector3): void {
Logger.error("Physics-lite don't support setInertiaTensor. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setUseGravity }
*/
setUseGravity(value: boolean): void {
Logger.error("Physics-lite don't support setUseGravity. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setIsKinematic }
*/
setIsKinematic(value: boolean): void {
Logger.error("Physics-lite don't support setIsKinematic. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setLinearDamping }
*/
setLinearDamping(value: number): void {
Logger.error("Physics-lite don't support setLinearDamping. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.getLinearVelocity }
*/
getLinearVelocity(out: Vector3): Vector3 {
Logger.error("Physics-lite don't support getLinearVelocity. Use Physics-PhysX instead!");
return out;
}
/**
* {@inheritDoc IDynamicCollider.setLinearVelocity }
*/
setLinearVelocity(value: Vector3): void {
Logger.error("Physics-lite don't support setLinearVelocity. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setMass }
*/
setMass(value: number): void {
Logger.error("Physics-lite don't support setMass. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setMaxAngularVelocity }
*/
setMaxAngularVelocity(value: number): void {
Logger.error("Physics-lite don't support setMaxAngularVelocity. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setMaxDepenetrationVelocity }
*/
setMaxDepenetrationVelocity(value: number): void {
Logger.error("Physics-lite don't support setMaxDepenetrationVelocity. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setSleepThreshold }
*/
setSleepThreshold(value: number): void {
Logger.error("Physics-lite don't support setSleepThreshold. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.setSolverIterations }
*/
setSolverIterations(value: number): void {
Logger.error("Physics-lite don't support setSolverIterations. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IDynamicCollider.wakeUp }
*/
wakeUp(): void {
throw "Physics-lite don't support wakeUp. Use Physics-PhysX instead!";
}
}

View File

@@ -1,16 +0,0 @@
import { Vector3 } from "@galacean/engine";
/**
* Structure used to get information back from a raycast or a sweep.
* @internal
*/
export class LiteHitResult {
/** The collider that was hit. */
shapeID: number = -1;
/** The distance from the origin to the hit point. */
distance: number = 0;
/** The hit point of the collider that was hit in world space. */
point: Vector3 = new Vector3();
/** The hit normal of the collider that was hit in world space. */
normal: Vector3 = new Vector3();
}

View File

@@ -1,202 +0,0 @@
import { Quaternion, Vector3, Layer } from "@galacean/engine";
import {
IBoxColliderShape,
ICapsuleColliderShape,
ICharacterController,
ICollider,
ICollision,
IDynamicCollider,
IFixedJoint,
IHingeJoint,
IPhysics,
IPhysicsManager,
IPhysicsMaterial,
IPlaneColliderShape,
ISphereColliderShape,
ISpringJoint,
IStaticCollider
} from "@galacean/engine-design";
import { LiteCollider } from "./LiteCollider";
import { LiteDynamicCollider } from "./LiteDynamicCollider";
import { LitePhysicsMaterial } from "./LitePhysicsMaterial";
import { LitePhysicsScene } from "./LitePhysicsScene";
import { LiteStaticCollider } from "./LiteStaticCollider";
import { LiteBoxColliderShape } from "./shape/LiteBoxColliderShape";
import { LiteSphereColliderShape } from "./shape/LiteSphereColliderShape";
import { LitePhysicsManager } from "./LitePhysicsManager";
export class LitePhysics implements IPhysics {
private _layerCollisionMatrix: boolean[] = [];
/**
* {@inheritDoc IPhysics.initialize }
*/
initialize(): Promise<void> {
return Promise.resolve();
}
/**
* {@inheritDoc IPhysics.createPhysicsManager }
*/
createPhysicsManager(): IPhysicsManager {
return null;
}
/**
* {@inheritDoc IPhysics.createPhysicsScene }
*/
createPhysicsScene(
physicsManager: LitePhysicsManager,
onContactBegin?: (collision: ICollision) => void,
onContactEnd?: (collision: ICollision) => void,
onContactPersist?: (collision: ICollision) => void,
onTriggerBegin?: (obj1: number, obj2: number) => void,
onTriggerEnd?: (obj1: number, obj2: number) => void,
onTriggerPersist?: (obj1: number, obj2: number) => void
): LitePhysicsScene {
return new LitePhysicsScene(
this,
onContactBegin,
onContactEnd,
onContactPersist,
onTriggerBegin,
onTriggerEnd,
onTriggerPersist
);
}
/**
* {@inheritDoc IPhysics.createStaticCollider }
*/
createStaticCollider(position: Vector3, rotation: Quaternion): IStaticCollider {
return new LiteStaticCollider(this, position, rotation);
}
/**
* {@inheritDoc IPhysics.createDynamicCollider }
*/
createDynamicCollider(position: Vector3, rotation: Quaternion): IDynamicCollider {
return new LiteDynamicCollider(this, position, rotation);
}
/**
* {@inheritDoc IPhysics.createCharacterController }
*/
createCharacterController(): ICharacterController {
throw "Physics-lite don't support createCharacterController. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IPhysics.createPhysicsMaterial }
*/
createPhysicsMaterial(
staticFriction: number,
dynamicFriction: number,
bounciness: number,
frictionCombine: number,
bounceCombine: number
): IPhysicsMaterial {
return new LitePhysicsMaterial(staticFriction, dynamicFriction, bounciness, frictionCombine, bounceCombine);
}
/**
* {@inheritDoc IPhysics.createBoxColliderShape }
*/
createBoxColliderShape(uniqueID: number, size: Vector3, material: LitePhysicsMaterial): IBoxColliderShape {
return new LiteBoxColliderShape(uniqueID, size, material);
}
/**
* {@inheritDoc IPhysics.createSphereColliderShape }
*/
createSphereColliderShape(uniqueID: number, radius: number, material: LitePhysicsMaterial): ISphereColliderShape {
return new LiteSphereColliderShape(uniqueID, radius, material);
}
/**
* {@inheritDoc IPhysics.createPlaneColliderShape }
*/
createPlaneColliderShape(uniqueID: number, material: LitePhysicsMaterial): IPlaneColliderShape {
throw new Error("Physics-lite doesn't support PlaneColliderShape. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysics.createCapsuleColliderShape }
*/
createCapsuleColliderShape(
uniqueID: number,
radius: number,
height: number,
material: LitePhysicsMaterial
): ICapsuleColliderShape {
throw new Error("Physics-lite doesn't support CapsuleColliderShape. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysics.createFixedJoint }
*/
createFixedJoint(collider: LiteCollider): IFixedJoint {
throw new Error("Physics-lite doesn't support FixedJoint. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysics.createHingeJoint }
*/
createHingeJoint(collider: LiteCollider): IHingeJoint {
throw new Error("Physics-lite doesn't support HingeJoint. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysics.createSpringJoint }
*/
createSpringJoint(collider: LiteCollider): ISpringJoint {
throw new Error("Physics-lite doesn't support SpringJoint. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysics.setColliderLayer }
*/
setColliderLayer(collider: LiteCollider, layer: Layer): void {
collider._collisionLayer = layer;
}
/**
* {@inheritDoc IPhysics.getColliderLayerCollision }
*/
getColliderLayerCollision(layer1: number, layer2: number): boolean {
const index = this._getColliderLayerIndex(layer1, layer2);
if (index > -1) {
return this._layerCollisionMatrix[index] ?? true;
}
// If either layer is Layer.Nothing, they cant collide
return false;
}
/**
* {@inheritDoc IPhysics.setColliderLayerCollision }
*/
setColliderLayerCollision(layer1: number, layer2: number, collide: boolean): void {
const index = this._getColliderLayerIndex(layer1, layer2);
if (index > -1) {
this._layerCollisionMatrix[index] = collide;
}
}
/**
* {@inheritDoc IPhysics.destroy }
*/
destroy(): void {}
private _getColliderLayerIndex(layer1: number, layer2: number): number {
if (layer1 === 32 || layer2 === 32) {
return -1;
}
const min = Math.min(layer1, layer2);
const max = Math.max(layer1, layer2);
// Calculate a unique index for the layer pair using the triangular number formula
// This ensures that each layer combination maps to a unique index in the collision matrix
return (max * (max + 1)) / 2 + min;
}
}

View File

@@ -1,3 +0,0 @@
import { IPhysicsManager } from "@galacean/engine-design";
export class LitePhysicsManager implements IPhysicsManager {}

View File

@@ -1,54 +0,0 @@
import { IPhysicsMaterial } from "@galacean/engine-design";
/**
* Physics material describes how to handle colliding objects (friction, bounciness).
*/
export class LitePhysicsMaterial implements IPhysicsMaterial {
constructor(
staticFriction: number,
dynamicFriction: number,
bounciness: number,
frictionCombine: number,
bounceCombine: number
) {}
/**
* {@inheritDoc IPhysicsMaterial.setBounciness }
*/
setBounciness(value: number): void {
throw "Physics-lite don't support physics material. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IPhysicsMaterial.setDynamicFriction }
*/
setDynamicFriction(value: number): void {
throw "Physics-lite don't support physics material. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IPhysicsMaterial.setStaticFriction }
*/
setStaticFriction(value: number): void {
throw "Physics-lite don't support physics material. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IPhysicsMaterial.setBounceCombine }
*/
setBounceCombine(value: number): void {
throw "Physics-lite don't support physics material. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IPhysicsMaterial.setFrictionCombine }
*/
setFrictionCombine(value: number): void {
throw "Physics-lite don't support physics material. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IPhysicsMaterial.destroy }
*/
destroy(): void {}
}

View File

@@ -1,518 +0,0 @@
import {
BoundingBox,
BoundingSphere,
CollisionUtil,
DisorderedArray,
Quaternion,
Ray,
Vector3
} from "@galacean/engine";
import { ICharacterController, ICollision, IPhysicsScene } from "@galacean/engine-design";
import { LiteCollider } from "./LiteCollider";
import { LiteDynamicCollider } from "./LiteDynamicCollider";
import { LiteHitResult } from "./LiteHitResult";
import { LiteStaticCollider } from "./LiteStaticCollider";
import { LiteBoxColliderShape } from "./shape/LiteBoxColliderShape";
import { LiteColliderShape } from "./shape/LiteColliderShape";
import { LiteSphereColliderShape } from "./shape/LiteSphereColliderShape";
import { LitePhysics } from "./LitePhysics";
/**
* A manager is a collection of colliders and constraints which can interact.
*/
export class LitePhysicsScene implements IPhysicsScene {
private static _tempSphere: BoundingSphere = new BoundingSphere();
private static _tempBox: BoundingBox = new BoundingBox();
private static _currentHit: LiteHitResult = new LiteHitResult();
private static _hitResult: LiteHitResult = new LiteHitResult();
private readonly _onContactEnter?: (collision: ICollision) => void;
private readonly _onContactExit?: (collision: ICollision) => void;
private readonly _onContactStay?: (collision: ICollision) => void;
private readonly _onTriggerEnter?: (obj1: number, obj2: number) => void;
private readonly _onTriggerExit?: (obj1: number, obj2: number) => void;
private readonly _onTriggerStay?: (obj1: number, obj2: number) => void;
private _staticColliders: LiteStaticCollider[] = [];
private _dynamicColliders: LiteDynamicCollider[] = [];
private _sphere: BoundingSphere = new BoundingSphere();
private _box: BoundingBox = new BoundingBox();
private _currentEvents: DisorderedArray<TriggerEvent> = new DisorderedArray<TriggerEvent>();
private _eventMap: Record<number, Record<number, TriggerEvent>> = {};
private _eventPool: TriggerEvent[] = [];
private _physics: LitePhysics;
constructor(
physics: LitePhysics,
onContactEnter?: (collision: ICollision) => void,
onContactExit?: (collision: ICollision) => void,
onContactStay?: (collision: ICollision) => void,
onTriggerEnter?: (obj1: number, obj2: number) => void,
onTriggerExit?: (obj1: number, obj2: number) => void,
onTriggerStay?: (obj1: number, obj2: number) => void
) {
this._physics = physics;
this._onContactEnter = onContactEnter;
this._onContactExit = onContactExit;
this._onContactStay = onContactStay;
this._onTriggerEnter = onTriggerEnter;
this._onTriggerExit = onTriggerExit;
this._onTriggerStay = onTriggerStay;
}
overlapBox(
center: Vector3,
orientation: Quaternion,
halfExtents: Vector3,
onOverlap: (obj: number) => boolean,
outHitResult?: (shapeUniqueID: number) => void
): boolean {
throw new Error("Method not implemented.");
}
overlapSphere(
center: Vector3,
radius: number,
onOverlap: (obj: number) => boolean,
outHitResult?: (shapeUniqueID: number) => void
): boolean {
throw new Error("Method not implemented.");
}
overlapCapsule(
center: Vector3,
radius: number,
height: number,
orientation: Quaternion,
onOverlap: (obj: number) => boolean,
outHitResult?: (shapeUniqueID: number) => void
): boolean {
throw new Error("Method not implemented.");
}
/**
* {@inheritDoc IPhysicsScene.setGravity }
*/
setGravity(value: Vector3): void {
console.log("Physics-lite don't support gravity. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysicsScene.addCollider }
*/
addCollider(actor: LiteCollider): void {
actor._scene = this;
const colliders = actor._isStaticCollider ? this._staticColliders : this._dynamicColliders;
colliders.push(actor);
const shapes = actor._shapes;
for (let i = 0, n = shapes.length; i < n; i++) {
this._addColliderShape(shapes[i]);
}
}
/**
* {@inheritDoc IPhysicsScene.removeCollider }
*/
removeCollider(collider: LiteCollider): void {
collider._scene = null;
const colliders = collider._isStaticCollider ? this._staticColliders : this._dynamicColliders;
const index = colliders.indexOf(collider);
index > -1 && colliders.splice(index, 1);
const shapes = collider._shapes;
for (let i = 0, n = shapes.length; i < n; i++) {
this._removeColliderShape(shapes[i]);
}
}
/**
* {@inheritDoc IPhysicsScene.update }
*/
update(deltaTime: number): void {
const dynamicColliders = this._dynamicColliders;
for (let i = 0, len = dynamicColliders.length; i < len; i++) {
const collider = dynamicColliders[i];
this._collisionDetection(collider, this._staticColliders);
this._collisionDetection(collider, dynamicColliders);
}
this._fireEvent();
}
/**
* {@inheritDoc IPhysicsScene.raycast }
*/
raycast(
ray: Ray,
distance: number,
onRaycast: (obj: number) => boolean,
hit?: (shapeUniqueID: number, distance: number, position: Vector3, normal: Vector3) => void
): boolean {
if (!hit) {
return (
this._raycast(ray, distance, onRaycast, this._staticColliders, hit) ||
this._raycast(ray, distance, onRaycast, this._dynamicColliders, hit)
);
} else {
const raycastStaticRes = this._raycast(ray, distance, onRaycast, this._staticColliders, hit);
if (raycastStaticRes) {
distance = LitePhysicsScene._currentHit.distance;
}
const raycastDynamicRes = this._raycast(ray, distance, onRaycast, this._dynamicColliders, hit);
const isHit = raycastStaticRes || raycastDynamicRes;
const hitResult = LitePhysicsScene._hitResult;
if (!isHit) {
hitResult.shapeID = -1;
hitResult.distance = 0;
hitResult.point.set(0, 0, 0);
hitResult.normal.set(0, 0, 0);
} else {
hit(hitResult.shapeID, hitResult.distance, hitResult.point, hitResult.normal);
}
return isHit;
}
}
/**
* {@inheritDoc IPhysicsScene.addCharacterController }
*/
addCharacterController(characterController: ICharacterController): void {
throw new Error("Physics-lite doesn't support addCharacterController. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysicsScene.removeCharacterController }
*/
removeCharacterController(characterController: ICharacterController): void {
throw new Error("Physics-lite doesn't support removeCharacterController. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysicsScene.boxCast }
*/
boxCast(
center: Vector3,
orientation: Quaternion,
halfExtents: Vector3,
direction: Vector3,
distance: number,
onSweep: (obj: number) => boolean,
outHitResult?: (shapeUniqueID: number, distance: number, position: Vector3, normal: Vector3) => void
): boolean {
throw new Error("Physics-lite doesn't support boxCast. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysicsScene.sphereCast }
*/
sphereCast(
center: Vector3,
radius: number,
direction: Vector3,
distance: number,
onSweep: (obj: number) => boolean,
outHitResult?: (shapeUniqueID: number, distance: number, position: Vector3, normal: Vector3) => void
): boolean {
throw new Error("Physics-lite doesn't support sphereCast. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysicsScene.capsuleCast }
*/
capsuleCast(
center: Vector3,
radius: number,
height: number,
orientation: Quaternion,
direction: Vector3,
distance: number,
onSweep: (obj: number) => boolean,
outHitResult?: (shapeUniqueID: number, distance: number, position: Vector3, normal: Vector3) => void
): boolean {
throw new Error("Physics-lite doesn't support capsuleCast. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysicsScene.overlapBoxAll }
*/
overlapBoxAll(
center: Vector3,
orientation: Quaternion,
halfExtents: Vector3,
onOverlap: (obj: number) => boolean
): number[] {
throw new Error("Physics-lite doesn't support overlapBoxAll. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysicsScene.overlapSphereAll }
*/
overlapSphereAll(center: Vector3, radius: number, onOverlap: (obj: number) => boolean): number[] {
throw new Error("Physics-lite doesn't support overlapSphereAll. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysicsScene.overlapCapsuleAll }
*/
overlapCapsuleAll(
center: Vector3,
radius: number,
height: number,
orientation: Quaternion,
onOverlap: (obj: number) => boolean
): number[] {
throw new Error("Physics-lite doesn't support overlapCapsuleAll. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IPhysicsScene.destroy }
*/
destroy(): void {}
/**
* @internal
*/
_addColliderShape(colliderShape: LiteColliderShape): void {
this._eventMap[colliderShape._id] = {};
}
/**
* @internal
*/
_removeColliderShape(colliderShape: LiteColliderShape): void {
const { _eventPool: eventPool, _currentEvents: currentEvents, _eventMap: eventMap } = this;
const { _id: id } = colliderShape;
currentEvents.forEach((event, i) => {
if (event.index1 == id) {
currentEvents.deleteByIndex(i);
eventPool.push(event);
} else if (event.index2 == id) {
currentEvents.deleteByIndex(i);
eventPool.push(event);
// If the shape is big index, should clear from the small index shape subMap
eventMap[event.index1][id] = undefined;
}
});
delete eventMap[id];
}
/**
* Calculate the bounding box in world space from boxCollider.
* @param boxCollider - The boxCollider to calculate
* @param out - The calculated boundingBox
*/
private static _updateWorldBox(boxCollider: LiteBoxColliderShape, out: BoundingBox): void {
const mat = boxCollider._transform.worldMatrix;
out.min.copyFrom(boxCollider._boxMin);
out.max.copyFrom(boxCollider._boxMax);
BoundingBox.transform(out, mat, out);
}
/**
* Get the sphere info of the given sphere collider in world space.
* @param sphereCollider - The given sphere collider
* @param out - The calculated boundingSphere
*/
private static _upWorldSphere(sphereCollider: LiteSphereColliderShape, out: BoundingSphere): void {
Vector3.transformCoordinate(sphereCollider._transform.position, sphereCollider._transform.worldMatrix, out.center);
out.radius = sphereCollider.worldRadius;
}
private _getTrigger(index1: number, index2: number): TriggerEvent {
let event: TriggerEvent;
if (this._eventPool.length) {
event = this._eventPool.pop();
event.index1 = index1;
event.index2 = index2;
} else {
event = new TriggerEvent(index1, index2);
}
this._eventMap[index1][index2] = event;
return event;
}
private _collisionDetection(myCollider: LiteCollider, colliders: LiteCollider[]): void {
const myColliderShapes = myCollider._shapes;
for (let i = 0, len = myColliderShapes.length; i < len; i++) {
const myShape = myColliderShapes[i];
if (myShape instanceof LiteBoxColliderShape) {
LitePhysicsScene._updateWorldBox(myShape, this._box);
for (let j = 0, len = colliders.length; j < len; j++) {
const collider = colliders[j];
const colliderShape = collider._shapes;
// Skip collision check if layers can't collide
if (!this._checkColliderCollide(collider, myCollider)) {
continue;
}
for (let k = 0, len = colliderShape.length; k < len; k++) {
const shape = colliderShape[k];
const index1 = shape._id;
const index2 = myShape._id;
const event = index1 < index2 ? this._eventMap[index1][index2] : this._eventMap[index2][index1];
if (event !== undefined && !event.alreadyInvoked) {
continue;
}
if (shape != myShape && this._boxCollision(shape)) {
if (event === undefined) {
const event = index1 < index2 ? this._getTrigger(index1, index2) : this._getTrigger(index2, index1);
event.state = TriggerEventState.Enter;
event.alreadyInvoked = false;
this._currentEvents.add(event);
} else if (event.state === TriggerEventState.Enter) {
event.state = TriggerEventState.Stay;
event.alreadyInvoked = false;
} else if (event.state === TriggerEventState.Stay) {
event.alreadyInvoked = false;
}
}
}
}
} else if (myShape instanceof LiteSphereColliderShape) {
LitePhysicsScene._upWorldSphere(myShape, this._sphere);
for (let j = 0, len = colliders.length; j < len; j++) {
const collider = colliders[j];
const colliderShape = collider._shapes;
// Skip collision check if layers can't collide
if (!this._checkColliderCollide(collider, myCollider)) {
continue;
}
for (let k = 0, len = colliderShape.length; k < len; k++) {
const shape = colliderShape[k];
const index1 = shape._id;
const index2 = myShape._id;
const event = index1 < index2 ? this._eventMap[index1][index2] : this._eventMap[index2][index1];
if (event !== undefined && !event.alreadyInvoked) {
continue;
}
if (shape != myShape && this._sphereCollision(shape)) {
if (event === undefined) {
const event = index1 < index2 ? this._getTrigger(index1, index2) : this._getTrigger(index2, index1);
event.state = TriggerEventState.Enter;
event.alreadyInvoked = false;
this._currentEvents.add(event);
} else if (event.state === TriggerEventState.Enter) {
event.state = TriggerEventState.Stay;
event.alreadyInvoked = false;
} else if (event.state === TriggerEventState.Stay) {
event.alreadyInvoked = false;
}
}
}
}
}
}
}
private _fireEvent(): void {
const { _eventPool: eventPool, _currentEvents: currentEvents } = this;
currentEvents.forEach((event, i) => {
if (!event.alreadyInvoked) {
if (event.state == TriggerEventState.Enter) {
this._onTriggerEnter(event.index1, event.index2);
event.alreadyInvoked = true;
} else if (event.state == TriggerEventState.Stay) {
this._onTriggerStay(event.index1, event.index2);
event.alreadyInvoked = true;
}
} else {
event.state = TriggerEventState.Exit;
this._eventMap[event.index1][event.index2] = undefined;
currentEvents.deleteByIndex(i);
this._onTriggerExit(event.index1, event.index2);
eventPool.push(event);
}
});
}
private _boxCollision(other: LiteColliderShape): boolean {
if (other instanceof LiteBoxColliderShape) {
const box = LitePhysicsScene._tempBox;
LitePhysicsScene._updateWorldBox(other, box);
return CollisionUtil.intersectsBoxAndBox(box, this._box);
} else if (other instanceof LiteSphereColliderShape) {
const sphere = LitePhysicsScene._tempSphere;
LitePhysicsScene._upWorldSphere(other, sphere);
return CollisionUtil.intersectsSphereAndBox(sphere, this._box);
}
return false;
}
private _sphereCollision(other: LiteColliderShape): boolean {
if (other instanceof LiteBoxColliderShape) {
const box = LitePhysicsScene._tempBox;
LitePhysicsScene._updateWorldBox(other, box);
return CollisionUtil.intersectsSphereAndBox(this._sphere, box);
} else if (other instanceof LiteSphereColliderShape) {
const sphere = LitePhysicsScene._tempSphere;
LitePhysicsScene._upWorldSphere(other, sphere);
return CollisionUtil.intersectsSphereAndSphere(sphere, this._sphere);
}
return false;
}
private _raycast(
ray: Ray,
distance: number,
onRaycast: (obj: number) => boolean,
colliders: LiteCollider[],
hit?: (shapeUniqueID: number, distance: number, position: Vector3, normal: Vector3) => void
): boolean {
let isHit = false;
const curHit = LitePhysicsScene._currentHit;
for (let i = 0, len = colliders.length; i < len; i++) {
if (colliders[i]._raycast(ray, onRaycast, curHit) && curHit.distance < distance) {
if (hit) {
isHit = true;
const hitResult = LitePhysicsScene._hitResult;
hitResult.normal.copyFrom(curHit.normal);
hitResult.point.copyFrom(curHit.point);
hitResult.distance = distance = curHit.distance;
hitResult.shapeID = curHit.shapeID;
} else {
return true;
}
}
}
return isHit;
}
private _checkColliderCollide(collider1: LiteCollider, collider2: LiteCollider): boolean {
const group1 = collider1._collisionLayer;
const group2 = collider2._collisionLayer;
if (group1 === group2) {
return true;
}
return this._physics.getColliderLayerCollision(group1, group2);
}
}
/**
* Physics state
*/
enum TriggerEventState {
Enter,
Stay,
Exit
}
/**
* Trigger event to store interactive object ids and state.
*/
class TriggerEvent {
state: TriggerEventState;
index1: number;
index2: number;
alreadyInvoked: boolean = false;
constructor(index1: number, index2: number) {
this.index1 = index1;
this.index2 = index2;
}
}

View File

@@ -1,23 +0,0 @@
import { IStaticCollider } from "@galacean/engine-design";
import { LiteCollider } from "./LiteCollider";
import { Quaternion, Vector3 } from "@galacean/engine";
import { LitePhysics } from "./LitePhysics";
/**
* A static collider component that will not move.
* @remarks Mostly used for object which always stays at the same place and never moves around.
*/
export class LiteStaticCollider extends LiteCollider implements IStaticCollider {
/** @internal */
readonly _isStaticCollider: boolean = true;
/**
* Initialize static actor.
* @param position - The global position
* @param rotation - The global rotation
*/
constructor(litePhysics: LitePhysics, position: Vector3, rotation: Quaternion) {
super(litePhysics);
this._transform.setPosition(position.x, position.y, position.z);
this._transform.setRotationQuaternion(rotation.x, rotation.y, rotation.z, rotation.w);
}
}

View File

@@ -1,349 +0,0 @@
import { MathUtil, Matrix, Quaternion, Vector3 } from "@galacean/engine";
import { LiteCollider } from "./LiteCollider";
import { LiteUpdateFlag } from "./LiteUpdateFlag";
import { LiteUpdateFlagManager } from "./LiteUpdateFlagManager";
import { LiteColliderShape } from "./shape/LiteColliderShape";
/**
* Used to implement transformation related functions.
*/
export class LiteTransform {
private static _tempQuat0: Quaternion = new Quaternion();
private static _tempMat42: Matrix = new Matrix();
private _position: Vector3 = new Vector3();
private _rotation: Vector3 = new Vector3();
private _rotationQuaternion: Quaternion = new Quaternion();
private _scale: Vector3 = new Vector3(1, 1, 1);
private _worldRotationQuaternion: Quaternion = new Quaternion();
private _localMatrix: Matrix = new Matrix();
private _worldMatrix: Matrix = new Matrix();
private _updateFlagManager: LiteUpdateFlagManager = new LiteUpdateFlagManager();
private _isParentDirty: boolean = true;
private _parentTransformCache: LiteTransform = null;
private _dirtyFlag: number = TransformFlag.WmWpWeWqWs;
private _owner: LiteColliderShape | LiteCollider;
set owner(value: LiteColliderShape | LiteCollider) {
this._owner = value;
}
/**
* Local position.
* @remarks Need to re-assign after modification to ensure that the modification takes effect.
*/
get position(): Vector3 {
return this._position;
}
set position(value: Vector3) {
if (this._position !== value) {
this._position.copyFrom(value);
}
this._setDirtyFlagTrue(TransformFlag.LocalMatrix);
this._updateWorldPositionFlag();
}
/**
* Local rotation, defining the rotation by using a unit quaternion.
* @remarks Need to re-assign after modification to ensure that the modification takes effect.
*/
get rotationQuaternion(): Quaternion {
if (this._isContainDirtyFlag(TransformFlag.LocalQuat)) {
Quaternion.rotationEuler(
MathUtil.degreeToRadian(this._rotation.x),
MathUtil.degreeToRadian(this._rotation.y),
MathUtil.degreeToRadian(this._rotation.z),
this._rotationQuaternion
);
this._setDirtyFlagFalse(TransformFlag.LocalQuat);
}
return this._rotationQuaternion;
}
set rotationQuaternion(value: Quaternion) {
if (this._rotationQuaternion !== value) {
this._rotationQuaternion.copyFrom(value);
}
this._setDirtyFlagTrue(TransformFlag.LocalMatrix | TransformFlag.LocalEuler);
this._setDirtyFlagFalse(TransformFlag.LocalQuat);
this._updateWorldRotationFlag();
}
/**
* World rotation, defining the rotation by using a unit quaternion.
* @remarks Need to re-assign after modification to ensure that the modification takes effect.
*/
get worldRotationQuaternion(): Quaternion {
if (this._isContainDirtyFlag(TransformFlag.WorldQuat)) {
const parent = this._getParentTransform();
if (parent != null) {
Quaternion.multiply(parent.worldRotationQuaternion, this.rotationQuaternion, this._worldRotationQuaternion);
} else {
this._worldRotationQuaternion.copyFrom(this.rotationQuaternion);
}
this._setDirtyFlagFalse(TransformFlag.WorldQuat);
}
return this._worldRotationQuaternion;
}
set worldRotationQuaternion(value: Quaternion) {
if (this._worldRotationQuaternion !== value) {
this._worldRotationQuaternion.copyFrom(value);
}
const parent = this._getParentTransform();
if (parent) {
Quaternion.invert(parent.worldRotationQuaternion, LiteTransform._tempQuat0);
Quaternion.multiply(value, LiteTransform._tempQuat0, this._rotationQuaternion);
} else {
this._rotationQuaternion.copyFrom(value);
}
this.rotationQuaternion = this._rotationQuaternion;
this._setDirtyFlagFalse(TransformFlag.WorldQuat);
}
/**
* Local scaling.
* @remarks Need to re-assign after modification to ensure that the modification takes effect.
*/
get scale(): Vector3 {
return this._scale;
}
set scale(value: Vector3) {
if (this._scale !== value) {
this._scale.copyFrom(value);
}
this._setDirtyFlagTrue(TransformFlag.LocalMatrix);
this._updateWorldScaleFlag();
}
/**
* Local matrix.
* @remarks Need to re-assign after modification to ensure that the modification takes effect.
*/
get localMatrix(): Matrix {
if (this._isContainDirtyFlag(TransformFlag.LocalMatrix)) {
Matrix.affineTransformation(this._scale, this.rotationQuaternion, this._position, this._localMatrix);
this._setDirtyFlagFalse(TransformFlag.LocalMatrix);
}
return this._localMatrix;
}
set localMatrix(value: Matrix) {
if (this._localMatrix !== value) {
this._localMatrix.copyFrom(value);
}
this._localMatrix.decompose(this._position, this._rotationQuaternion, this._scale);
this._setDirtyFlagTrue(TransformFlag.LocalEuler);
this._setDirtyFlagFalse(TransformFlag.LocalMatrix);
this._updateAllWorldFlag();
}
/**
* World matrix.
* @remarks Need to re-assign after modification to ensure that the modification takes effect.
*/
get worldMatrix(): Matrix {
if (this._isContainDirtyFlag(TransformFlag.WorldMatrix)) {
const parent = this._getParentTransform();
if (parent) {
Matrix.multiply(parent.worldMatrix, this.localMatrix, this._worldMatrix);
} else {
this._worldMatrix.copyFrom(this.localMatrix);
}
this._setDirtyFlagFalse(TransformFlag.WorldMatrix);
}
return this._worldMatrix;
}
set worldMatrix(value: Matrix) {
if (this._worldMatrix !== value) {
this._worldMatrix.copyFrom(value);
}
const parent = this._getParentTransform();
if (parent) {
Matrix.invert(parent.worldMatrix, LiteTransform._tempMat42);
Matrix.multiply(LiteTransform._tempMat42, value, this._localMatrix);
} else {
this._localMatrix.copyFrom(value);
}
this.localMatrix = this._localMatrix;
this._setDirtyFlagFalse(TransformFlag.WorldMatrix);
}
/**
* Set local position by X, Y, Z value.
* @param x - X coordinate
* @param y - Y coordinate
* @param z - Z coordinate
*/
setPosition(x: number, y: number, z: number): void {
this._position.set(x, y, z);
this.position = this._position;
}
/**
* Set local rotation by the X, Y, Z, and W components of the quaternion.
* @param x - X component of quaternion
* @param y - Y component of quaternion
* @param z - Z component of quaternion
* @param w - W component of quaternion
*/
setRotationQuaternion(x: number, y: number, z: number, w: number): void {
this._rotationQuaternion.set(x, y, z, w);
this.rotationQuaternion = this._rotationQuaternion;
}
/**
* Set local scaling by scaling values along X, Y, Z axis.
* @param x - Scaling along X axis
* @param y - Scaling along Y axis
* @param z - Scaling along Z axis
*/
setScale(x: number, y: number, z: number): void {
this._scale.set(x, y, z);
this.scale = this._scale;
}
/**
* Register world transform change flag.
* @returns Change flag
*/
registerWorldChangeFlag(): LiteUpdateFlag {
return this._updateFlagManager.register();
}
/**
* Get worldMatrix: Will trigger the worldMatrix update of itself and all parent entities.
* Get worldPosition: Will trigger the worldMatrix, local position update of itself and the worldMatrix update of all parent entities.
* In summary, any update of related variables will cause the dirty mark of one of the full process (worldMatrix or worldRotationQuaternion) to be false.
*/
private _updateWorldPositionFlag(): void {
if (!this._isContainDirtyFlags(TransformFlag.WmWp)) {
this._worldAssociatedChange(TransformFlag.WmWp);
if (this._owner instanceof LiteCollider) {
const shapes = this._owner._shapes;
for (let i: number = 0, n: number = shapes.length; i < n; i++) {
shapes[i]._transform._updateWorldPositionFlag();
}
}
}
}
/**
* Get worldMatrix: Will trigger the worldMatrix update of itself and all parent entities.
* Get worldPosition: Will trigger the worldMatrix, local position update of itself and the worldMatrix update of all parent entities.
* Get worldRotationQuaternion: Will trigger the world rotation (in quaternion) update of itself and all parent entities.
* Get worldRotation: Will trigger the world rotation(in euler and quaternion) update of itself and world rotation(in quaternion) update of all parent entities.
* In summary, any update of related variables will cause the dirty mark of one of the full process (worldMatrix or worldRotationQuaternion) to be false.
*/
private _updateWorldRotationFlag() {
if (!this._isContainDirtyFlags(TransformFlag.WmWeWq)) {
this._worldAssociatedChange(TransformFlag.WmWeWq);
if (this._owner instanceof LiteCollider) {
const shapes = this._owner._shapes;
for (let i: number = 0, n: number = shapes.length; i < n; i++) {
shapes[i]._transform._updateWorldRotationFlag();
}
}
}
}
/**
* Get worldMatrix: Will trigger the worldMatrix update of itself and all parent entities.
* Get worldPosition: Will trigger the worldMatrix, local position update of itself and the worldMatrix update of all parent entities.
* Get worldScale: Will trigger the scaling update of itself and all parent entities.
* In summary, any update of related variables will cause the dirty mark of one of the full process (worldMatrix) to be false.
*/
private _updateWorldScaleFlag() {
if (!this._isContainDirtyFlags(TransformFlag.WmWs)) {
this._worldAssociatedChange(TransformFlag.WmWs);
if (this._owner instanceof LiteCollider) {
const shapes = this._owner._shapes;
for (let i: number = 0, n: number = shapes.length; i < n; i++) {
shapes[i]._transform._updateWorldScaleFlag();
}
}
}
}
/**
* Update all world transform property dirty flag, the principle is the same as above.
*/
private _updateAllWorldFlag(): void {
if (!this._isContainDirtyFlags(TransformFlag.WmWpWeWqWs)) {
this._worldAssociatedChange(TransformFlag.WmWpWeWqWs);
if (this._owner instanceof LiteCollider) {
const shapes = this._owner._shapes;
for (let i: number = 0, n: number = shapes.length; i < n; i++) {
shapes[i]._transform._updateAllWorldFlag();
}
}
}
}
private _getParentTransform(): LiteTransform | null {
if (!this._isParentDirty) {
return this._parentTransformCache;
}
let parentCache: LiteTransform = null;
if (this._owner instanceof LiteColliderShape) {
let parent = this._owner._collider;
parentCache = parent._transform;
}
this._parentTransformCache = parentCache;
this._isParentDirty = false;
return parentCache;
}
private _isContainDirtyFlags(targetDirtyFlags: number): boolean {
return (this._dirtyFlag & targetDirtyFlags) === targetDirtyFlags;
}
private _isContainDirtyFlag(type: number): boolean {
return (this._dirtyFlag & type) != 0;
}
private _setDirtyFlagTrue(type: number) {
this._dirtyFlag |= type;
}
private _setDirtyFlagFalse(type: number) {
this._dirtyFlag &= ~type;
}
private _worldAssociatedChange(type: number): void {
this._dirtyFlag |= type;
this._updateFlagManager.distribute();
}
}
/**
* Dirty flag of transform.
*/
enum TransformFlag {
LocalEuler = 0x1,
LocalQuat = 0x2,
WorldPosition = 0x4,
WorldEuler = 0x8,
WorldQuat = 0x10,
WorldScale = 0x20,
LocalMatrix = 0x40,
WorldMatrix = 0x80,
/** WorldMatrix | WorldPosition */
WmWp = 0x84,
/** WorldMatrix | WorldEuler | WorldQuat */
WmWeWq = 0x98,
/** WorldMatrix | WorldPosition | WorldEuler | WorldQuat */
WmWpWeWq = 0x9c,
/** WorldMatrix | WorldScale */
WmWs = 0xa0,
/** WorldMatrix | WorldPosition | WorldScale */
WmWpWs = 0xa4,
/** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale */
WmWpWeWqWs = 0xbc
}

View File

@@ -1,21 +0,0 @@
import { Utils } from "@galacean/engine";
/**
* Used to update tags.
*/
export class LiteUpdateFlag {
/** Flag. */
flag = true;
constructor(private _flags: LiteUpdateFlag[] = []) {
this._flags.push(this);
}
/**
* Destroy.
*/
destroy(): void {
Utils.removeFromArray(this._flags, this);
this._flags = null;
}
}

View File

@@ -1,19 +0,0 @@
import { LiteUpdateFlag } from "./LiteUpdateFlag";
/**
* @internal
*/
export class LiteUpdateFlagManager {
private _updateFlags: LiteUpdateFlag[] = [];
register(): LiteUpdateFlag {
return new LiteUpdateFlag(this._updateFlags);
}
distribute(): void {
const updateFlags = this._updateFlags;
for (let i = updateFlags.length - 1; i >= 0; i--) {
updateFlags[i].flag = true;
}
}
}

View File

@@ -1,6 +0,0 @@
export { LitePhysics } from "./LitePhysics";
//@ts-ignore
export const version = `__buildVersion`;
console.log(`Galacean Engine Physics Lite Version: ${version}`);

View File

@@ -1,139 +0,0 @@
import { BoundingBox, Matrix, Quaternion, Ray, Vector3, Vector4 } from "@galacean/engine";
import { IBoxColliderShape } from "@galacean/engine-design";
import { LiteHitResult } from "../LiteHitResult";
import { LitePhysicsMaterial } from "../LitePhysicsMaterial";
import { LiteColliderShape } from "./LiteColliderShape";
/**
* Box collider shape in Lite.
*/
export class LiteBoxColliderShape extends LiteColliderShape implements IBoxColliderShape {
private static _tempBox: BoundingBox = new BoundingBox();
private static _tempMatrix: Matrix = new Matrix();
private static _tempInvMatrix: Matrix = new Matrix();
private _halfSize: Vector3 = new Vector3();
private _sizeScale: Vector3 = new Vector3(1, 1, 1);
/** @internal */
_boxMin: Vector3 = new Vector3(-0.5, -0.5, -0.5);
/** @internal */
_boxMax: Vector3 = new Vector3(0.5, 0.5, 0.5);
/**
* Init Box Shape.
* @param uniqueID - UniqueID mark Shape.
* @param size - Size of Shape.
* @param material - Material of PhysXCollider.
*/
constructor(uniqueID: number, size: Vector3, material: LitePhysicsMaterial) {
super();
this._id = uniqueID;
this._halfSize.set(size.x * 0.5, size.y * 0.5, size.z * 0.5);
this._setBondingBox();
}
/**
* {@inheritDoc IColliderShape.setPosition }
*/
override setPosition(position: Vector3): void {
super.setPosition(position);
this._setBondingBox();
}
/**
* {@inheritDoc IColliderShape.setWorldScale }
*/
override setWorldScale(scale: Vector3): void {
super.setWorldScale(scale);
this._sizeScale.set(Math.abs(scale.x), Math.abs(scale.y), Math.abs(scale.z));
this._setBondingBox();
}
/**
* {@inheritDoc IBoxColliderShape.setSize }
*/
setSize(value: Vector3): void {
this._halfSize.set(value.x * 0.5, value.y * 0.5, value.z * 0.5);
this._setBondingBox();
}
/**
* {@inheritDoc IColliderShape.pointDistance }
*/
override pointDistance(point: Vector3): Vector4 {
const position = LiteColliderShape._tempPos;
const rotation = LiteColliderShape._tempRot;
this._transform.worldMatrix.decompose(position, rotation, LiteColliderShape._tempScale);
const { position: shapePosition } = this._transform;
const m = LiteBoxColliderShape._tempMatrix;
const invM = LiteBoxColliderShape._tempInvMatrix;
const p = LiteColliderShape._tempPoint;
const scale = this._sizeScale;
const boundingBox = LiteBoxColliderShape._tempBox;
const { _boxMin, _boxMax } = this;
p.copyFrom(_boxMin);
p.subtract(shapePosition);
p.divide(scale);
boundingBox.min.copyFrom(p);
p.copyFrom(_boxMax);
p.subtract(shapePosition);
p.divide(scale);
boundingBox.max.copyFrom(p);
Matrix.affineTransformation(scale, rotation, position, m);
Matrix.invert(m, invM);
Vector3.transformCoordinate(point, invM, p);
const min = boundingBox.min;
const max = boundingBox.max;
p.x = Math.max(min.x, Math.min(p.x, max.x));
p.y = Math.max(min.y, Math.min(p.y, max.y));
p.z = Math.max(min.z, Math.min(p.z, max.z));
Vector3.transformCoordinate(p, m, p);
const res = LiteColliderShape._tempVector4;
if (Vector3.equals(p, point)) {
res.set(point.x, point.y, point.z, 0);
} else {
res.set(p.x, p.y, p.z, Vector3.distanceSquared(p, point));
}
return res;
}
/**
* @internal
*/
_raycast(ray: Ray, hit: LiteHitResult): boolean {
const localRay = this._getLocalRay(ray);
const sizeScale = this._sizeScale;
const halfSize = this._halfSize;
const boundingBox = LiteBoxColliderShape._tempBox;
boundingBox.min.set(-halfSize.x * sizeScale.x, -halfSize.y * sizeScale.y, -halfSize.z * sizeScale.z);
boundingBox.max.set(halfSize.x * sizeScale.x, halfSize.y * sizeScale.y, halfSize.z * sizeScale.z);
const rayDistance = localRay.intersectBox(boundingBox);
if (rayDistance !== -1) {
this._updateHitResult(localRay, rayDistance, hit, ray.origin);
return true;
} else {
return false;
}
}
private _setBondingBox(): void {
const { position } = this._transform;
const scale = this._sizeScale;
const halfSize = this._halfSize;
this._boxMin.set(
-halfSize.x * scale.x + position.x,
-halfSize.y * scale.y + position.y,
-halfSize.z * scale.z + position.z
);
this._boxMax.set(
halfSize.x * scale.x + position.x,
halfSize.y * scale.y + position.y,
halfSize.z * scale.z + position.z
);
}
}

View File

@@ -1,166 +0,0 @@
import { MathUtil, Matrix, Quaternion, Ray, Vector3, Vector4 } from "@galacean/engine";
import { IColliderShape, IPhysicsMaterial } from "@galacean/engine-design";
import { LiteCollider } from "../LiteCollider";
import { LiteHitResult } from "../LiteHitResult";
import { LiteTransform } from "../LiteTransform";
import { LiteUpdateFlag } from "../LiteUpdateFlag";
/**
* Abstract class for collider shapes.
*/
export abstract class LiteColliderShape implements IColliderShape {
protected static _tempPos = new Vector3();
protected static _tempRot = new Quaternion();
protected static _tempScale = new Vector3();
protected static _tempPoint = new Vector3();
protected static _tempVector4 = new Vector4();
private static _ray = new Ray();
/** @internal */
_id: number;
/** @internal */
_collider: LiteCollider;
/** @internal */
_position: Vector3 = new Vector3();
/** @internal */
_worldScale: Vector3 = new Vector3(1, 1, 1);
/** @internal */
_transform: LiteTransform = new LiteTransform();
/** @internal */
_invModelMatrix: Matrix = new Matrix();
/** @internal */
_inverseWorldMatFlag: LiteUpdateFlag;
private _rotation: Vector3 = new Vector3();
protected constructor() {
this._transform.owner = this;
this._inverseWorldMatFlag = this._transform.registerWorldChangeFlag();
}
/**
* {@inheritDoc IColliderShape.setRotation }
*/
setRotation(rotation: Vector3): void {
const rotationInRadians = this._rotation.set(
MathUtil.degreeToRadian(rotation.x),
MathUtil.degreeToRadian(rotation.y),
MathUtil.degreeToRadian(rotation.z)
);
Quaternion.rotationEuler(
rotationInRadians.x,
rotationInRadians.y,
rotationInRadians.z,
this._transform.rotationQuaternion
);
}
/**
* {@inheritDoc IColliderShape.setPosition }
*/
setPosition(position: Vector3): void {
if (position !== this._position) {
this._position.copyFrom(position);
}
this._setLocalPose();
}
/**
* {@inheritDoc IColliderShape.setWorldScale }
*/
setWorldScale(scale: Vector3): void {
this._worldScale.copyFrom(scale);
this._setLocalPose();
}
/**
* {@inheritDoc IColliderShape.setContactOffset }
*/
setContactOffset(offset: number): void {
console.log("Physics-lite don't support setContactOffset. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IColliderShape.setMaterial }
*/
setMaterial(material: IPhysicsMaterial): void {
console.log("Physics-lite don't support setMaterial. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IColliderShape.setUniqueID }
*/
setUniqueID(id: number): void {
this._id = id;
}
/**
* {@inheritDoc IColliderShape.setIsTrigger }
*/
setIsTrigger(value: boolean): void {
console.log("Physics-lite don't support setIsTrigger. Use Physics-PhysX instead!");
}
/**
* {@inheritDoc IColliderShape.pointDistance }
*/
abstract pointDistance(point: Vector3): Vector4;
/**
* {@inheritDoc IColliderShape.destroy }
*/
destroy(): void {}
/**
* @internal
*/
abstract _raycast(ray: Ray, hit: LiteHitResult): boolean;
protected _updateHitResult(
ray: Ray,
rayDistance: number,
outHit: LiteHitResult,
origin: Vector3,
isWorldRay: boolean = false
): void {
const hitPoint = LiteColliderShape._tempPoint;
ray.getPoint(rayDistance, hitPoint);
if (!isWorldRay) {
Vector3.transformCoordinate(hitPoint, this._transform.worldMatrix, hitPoint);
}
const distance = Vector3.distance(origin, hitPoint);
if (distance < outHit.distance) {
outHit.point.copyFrom(hitPoint);
outHit.distance = distance;
outHit.shapeID = this._id;
}
}
protected _getLocalRay(ray: Ray): Ray {
const worldToLocal = this._getInvModelMatrix();
const outRay = LiteColliderShape._ray;
Vector3.transformCoordinate(ray.origin, worldToLocal, outRay.origin);
Vector3.transformNormal(ray.direction, worldToLocal, outRay.direction);
outRay.direction.normalize();
return outRay;
}
private _getInvModelMatrix(): Matrix {
if (this._inverseWorldMatFlag.flag) {
Matrix.invert(this._transform.worldMatrix, this._invModelMatrix);
this._inverseWorldMatFlag.flag = false;
}
return this._invModelMatrix;
}
private _setLocalPose() {
const shapePosition = LiteColliderShape._tempPoint;
Vector3.multiply(this._position, this._worldScale, shapePosition);
this._transform.position = shapePosition;
}
}

View File

@@ -1,90 +0,0 @@
import { ISphereColliderShape } from "@galacean/engine-design";
import { LiteColliderShape } from "./LiteColliderShape";
import { BoundingSphere, Ray, Vector3, Vector4 } from "@galacean/engine";
import { LiteHitResult } from "../LiteHitResult";
import { LitePhysicsMaterial } from "../LitePhysicsMaterial";
/**
* Sphere collider shape in Lite.
*/
export class LiteSphereColliderShape extends LiteColliderShape implements ISphereColliderShape {
private static _tempSphere: BoundingSphere = new BoundingSphere();
private _radius: number = 1;
private _maxScale: number = 1;
get worldRadius(): number {
return this._radius * this._maxScale;
}
/**
* Init sphere shape.
* @param uniqueID - UniqueID mark collider
* @param radius - Size of SphereCollider
* @param material - Material of PhysXCollider
*/
constructor(uniqueID: number, radius: number, material: LitePhysicsMaterial) {
super();
this._radius = radius;
this._id = uniqueID;
}
/**
* {@inheritDoc ISphereColliderShape.setRadius }
*/
setRadius(value: number): void {
this._radius = value;
}
/**
* {@inheritDoc IColliderShape.setWorldScale }
*/
override setWorldScale(scale: Vector3): void {
super.setWorldScale(scale);
this._maxScale = Math.max(Math.abs(scale.x), Math.abs(scale.y), Math.abs(scale.z));
}
/**
* {@inheritDoc IColliderShape.pointDistance }
*/
override pointDistance(point: Vector3): Vector4 {
const position = LiteColliderShape._tempPos;
const worldRadius = this.worldRadius;
this._transform.worldMatrix.decompose(position, LiteColliderShape._tempRot, LiteColliderShape._tempScale);
const p = LiteColliderShape._tempPoint;
Vector3.subtract(point, position, p);
const distanceFromCenter = p.lengthSquared();
const direction = p.normalize();
Vector3.scale(direction, worldRadius, p);
p.add(position);
const res = LiteColliderShape._tempVector4;
const distanceSquared = Vector3.distanceSquared(p, point);
if (distanceFromCenter <= worldRadius * worldRadius) {
res.set(point.x, point.y, point.z, 0);
} else {
res.set(p.x, p.y, p.z, distanceSquared);
}
return res;
}
/**
* @internal
*/
_raycast(ray: Ray, hit: LiteHitResult): boolean {
const boundingSphere = LiteSphereColliderShape._tempSphere;
Vector3.transformCoordinate(this._transform.position, this._collider._transform.worldMatrix, boundingSphere.center);
boundingSphere.radius = this.worldRadius;
const rayDistance = ray.intersectSphere(boundingSphere);
if (rayDistance !== -1) {
this._updateHitResult(ray, rayDistance, hit, ray.origin, true);
return true;
} else {
return false;
}
}
}

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"declaration": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"declarationDir": "types",
"emitDeclarationOnly": true,
"noImplicitOverride": true,
"sourceMap": true,
"incremental": false,
"skipLibCheck": true,
"stripInternal": true
},
"include": ["src/**/*"]
}

View File

@@ -1,67 +0,0 @@
# @galacean/engine-ui
`@galacean/engine-ui` is a UI library designed for the `@galacean/engine`, this library enables developers to create and manage user interfaces efficiently within their **Galacean-based** applications. It is important to note that it is currently in an **experimental version**.
## Features
- **Rendering components**: Includes `Image` and `Text`.
- **Interactive Components**: Include `Button`, as well as other planned basic rendering components to be added in the future.
- **Event Handling**: Supports the dispatch and bubbling of events such as `onPointerEnter`, `onPointerExit`,`onPointerDown`, `onPointerUp`, `onPointerClick`, `onPointerDrag` and `onPointerDrop`.
- **Optimized Performance**: Designed to run smoothly with the Galacean engine.
## Installation
To use `@galacean/engine-ui` in your project, install it via npm:
```bash
npm install @galacean/engine-ui
```
## Getting Started
Here is a simple example to help you get started:
```typescript
import { AssetType, Camera, Sprite, Texture2D, Vector3, WebGLEngine } from "@galacean/engine";
import { CanvasRenderMode, Image, ResolutionAdaptationMode, UICanvas } from "@galacean/engine-ui";
// Initialize engine and scene
const engine = await WebGLEngine.create({ canvas: "canvas" });
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// Create camera
const cameraEntity = rootEntity.createChild("Camera");
cameraEntity.transform.position = new Vector3(0, 0, 10);
const camera = cameraEntity.addComponent(Camera);
camera.farClipPlane = 200;
camera.nearClipPlane = 0.3;
// Create canvas
const canvasEntity = rootEntity.createChild("canvas");
const canvas = canvasEntity.addComponent(UICanvas);
canvas.renderMode = CanvasRenderMode.ScreenSpaceCamera;
canvas.resolutionAdaptationMode = ResolutionAdaptationMode.HeightAdaptation;
canvas.distance = 100;
canvas.renderCamera = camera;
// Create Image
const imageEntity = canvasEntity.createChild("image");
const image = imageEntity.addComponent(Image);
engine.resourceManager
.load({
url: "https://xxx.png",
type: AssetType.Texture2D
})
.then((texture) => {
image.sprite = new Sprite(engine, <Texture2D>texture);
});
// Run the engine
engine.run();
```
Easier operations in the [Editor](https://galacean.antgroup.com/editor/).
## Documentation
For detailed documentation, visit [the official documentation site](https://galacean.antgroup.com/engine).

View File

@@ -1,37 +0,0 @@
{
"name": "@galacean/engine-ui",
"version": "0.0.0-experimental-backup.4",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"repository": {
"url": "https://github.com/galacean/engine.git"
},
"license": "MIT",
"main": "dist/main.js",
"module": "dist/module.js",
"debug": "src/index.ts",
"browser": "dist/browser.js",
"types": "types/index.d.ts",
"scripts": {
"b:types": "tsc"
},
"umd": {
"name": "Galacean.UI",
"globals": {
"@galacean/engine": "Galacean"
}
},
"files": [
"dist/**/*",
"libs/**/*",
"types/**/*"
],
"devDependencies": {
"@galacean/engine": "workspace:*"
},
"peerDependencies": {
"@galacean/engine": "workspace:*"
}
}

View File

@@ -1,152 +0,0 @@
import { Entity } from "@galacean/engine";
import { RootCanvasModifyFlags, UICanvas } from "./component/UICanvas";
import { GroupModifyFlags, UIGroup } from "./component/UIGroup";
import { IElement } from "./interface/IElement";
import { IGroupAble } from "./interface/IGroupAble";
export class Utils {
static setRootCanvasDirty(element: IElement): void {
if (element._isRootCanvasDirty) return;
element._isRootCanvasDirty = true;
this._registerRootCanvas(element, null);
element._onRootCanvasModify?.(RootCanvasModifyFlags.All);
}
static setRootCanvas(element: IElement, rootCanvas: UICanvas): void {
element._isRootCanvasDirty = false;
this._registerRootCanvas(element, rootCanvas);
const fromEntity = element instanceof UICanvas ? element.entity.parent : element.entity;
const toEntity = rootCanvas?.entity.parent ?? null;
this._registerListener(fromEntity, toEntity, element._rootCanvasListener, element._rootCanvasListeningEntities);
}
static cleanRootCanvas(element: IElement): void {
this._registerRootCanvas(element, null);
this._unRegisterListener(element._rootCanvasListener, element._rootCanvasListeningEntities);
}
static searchRootCanvasInParents(element: IElement): UICanvas {
let entity = element instanceof UICanvas ? element.entity.parent : element.entity;
while (entity) {
// @ts-ignore
const components = entity._components;
for (let i = 0, n = components.length; i < n; i++) {
const component = components[i];
if (component.enabled && component instanceof UICanvas && component._isRootCanvas) {
return component;
}
}
entity = entity.parent;
}
return null;
}
static setGroupDirty(element: IGroupAble): void {
if (element._isGroupDirty) return;
element._isGroupDirty = true;
this._registerGroup(element, null);
element._onGroupModify(GroupModifyFlags.All);
}
static setGroup(element: IGroupAble, group: UIGroup): void {
element._isGroupDirty = false;
this._registerGroup(element, group);
const rootCanvas = element._getRootCanvas();
if (rootCanvas) {
const fromEntity = element instanceof UIGroup ? element.entity.parent : element.entity;
const toEntity = group?.entity ?? rootCanvas.entity.parent;
this._registerListener(fromEntity, toEntity, element._groupListener, element._groupListeningEntities);
} else {
this._unRegisterListener(element._groupListener, element._groupListeningEntities);
}
}
static cleanGroup(element: IGroupAble): void {
this._registerGroup(element, null);
this._unRegisterListener(element._groupListener, element._groupListeningEntities);
}
static searchGroupInParents(element: IGroupAble): UIGroup {
const rootCanvas = element._getRootCanvas();
if (!rootCanvas) return null;
let entity = element instanceof UIGroup ? element.entity.parent : element.entity;
const rootCanvasParent = rootCanvas.entity.parent;
while (entity && entity !== rootCanvasParent) {
// @ts-ignore
const components = entity._components;
for (let i = 0, n = components.length; i < n; i++) {
const component = components[i];
if (component.enabled && component instanceof UIGroup) {
return component;
}
}
entity = entity.parent;
}
return null;
}
private static _registerRootCanvas(element: IElement, canvas: UICanvas): void {
const preCanvas = element._rootCanvas;
if (preCanvas !== canvas) {
if (preCanvas) {
const replaced = preCanvas._disorderedElements.deleteByIndex(element._indexInRootCanvas);
replaced && (replaced._indexInRootCanvas = element._indexInRootCanvas);
element._indexInRootCanvas = -1;
}
if (canvas) {
const disorderedElements = canvas._disorderedElements;
element._indexInRootCanvas = disorderedElements.length;
disorderedElements.add(element);
}
element._rootCanvas = canvas;
}
}
private static _registerGroup(element: IGroupAble, group: UIGroup): void {
const preGroup = element._group;
if (preGroup !== group) {
if (preGroup) {
const replaced = preGroup._disorderedElements.deleteByIndex(element._indexInGroup);
replaced && (replaced._indexInGroup = element._indexInGroup);
element._indexInGroup = -1;
}
if (group) {
const disorderedElements = group._disorderedElements;
element._indexInGroup = disorderedElements.length;
disorderedElements.add(element);
}
element._group = group;
element._onGroupModify(GroupModifyFlags.All);
}
}
private static _registerListener(
entity: Entity,
root: Entity,
listener: (flag: number, param?: any) => void,
listeningEntities: Entity[]
): void {
let count = 0;
while (entity && entity !== root) {
const preEntity = listeningEntities[count];
if (preEntity !== entity) {
// @ts-ignore
preEntity?._unRegisterModifyListener(listener);
listeningEntities[count] = entity;
// @ts-ignore
entity._registerModifyListener(listener);
}
entity = entity.parent;
count++;
}
listeningEntities.length = count;
}
private static _unRegisterListener(listener: (flag: number, param?: any) => void, listeningEntities: Entity[]): void {
for (let i = 0, n = listeningEntities.length; i < n; i++) {
// @ts-ignore
listeningEntities[i]._unRegisterModifyListener(listener);
}
listeningEntities.length = 0;
}
}

View File

@@ -1,718 +0,0 @@
import {
BoolUpdateFlag,
Camera,
CameraModifyFlags,
Component,
DependentMode,
DisorderedArray,
Entity,
EntityModifyFlags,
Logger,
MathUtil,
Matrix,
Ray,
Vector2,
Vector3,
assignmentClone,
deepClone,
dependentComponents,
ignoreClone
} from "@galacean/engine";
import { Utils } from "../Utils";
import { CanvasRenderMode } from "../enums/CanvasRenderMode";
import { ResolutionAdaptationMode } from "../enums/ResolutionAdaptationMode";
import { UIHitResult } from "../input/UIHitResult";
import { IElement } from "../interface/IElement";
import { IGroupAble } from "../interface/IGroupAble";
import { UIGroup } from "./UIGroup";
import { UIRenderer } from "./UIRenderer";
import { UITransform } from "./UITransform";
import { UIInteractive } from "./interactive/UIInteractive";
/**
* The UI Canvas component serves as the root node for UI elements,
* handling rendering and events based on it.
*/
@dependentComponents(UITransform, DependentMode.AutoAdd)
export class UICanvas extends Component implements IElement {
/** @internal */
static _hierarchyCounter: number = 1;
private static _targetTempPath: number[] = [];
private static _tempGroupAbleList: IGroupAble[] = [];
private static _tempVec3: Vector3 = new Vector3();
private static _tempMat: Matrix = new Matrix();
/** @internal */
@ignoreClone
_canvasIndex: number = -1;
/** @internal */
@ignoreClone
_rootCanvas: UICanvas;
/** @internal */
@ignoreClone
_indexInRootCanvas: number = -1;
/** @internal */
@ignoreClone
_isRootCanvasDirty: boolean = false;
/** @internal */
@ignoreClone
_rootCanvasListeningEntities: Entity[] = [];
/** @internal */
@ignoreClone
_isRootCanvas: boolean = false;
/** @internal */
@ignoreClone
_renderElement: any;
/** @internal */
@ignoreClone
_sortDistance: number = 0;
/** @internal */
@ignoreClone
_orderedRenderers: UIRenderer[] = [];
/** @internal */
@ignoreClone
_realRenderMode: number = CanvasRealRenderMode.None;
/** @internal */
@ignoreClone
_disorderedElements: DisorderedArray<IElement> = new DisorderedArray<IElement>();
@ignoreClone
private _renderMode = CanvasRenderMode.WorldSpace;
@ignoreClone
private _renderCamera: Camera;
@ignoreClone
private _cameraObserver: Camera;
@assignmentClone
private _resolutionAdaptationMode = ResolutionAdaptationMode.HeightAdaptation;
@assignmentClone
private _sortOrder: number = 0;
@assignmentClone
private _distance: number = 10;
@deepClone
private _referenceResolution: Vector2 = new Vector2(800, 600);
@assignmentClone
private _referenceResolutionPerUnit: number = 100;
@ignoreClone
private _hierarchyVersion: number = -1;
@ignoreClone
private _center: Vector3 = new Vector3();
@ignoreClone
private _centerDirtyFlag: BoolUpdateFlag;
/**
* The conversion ratio between reference resolution and unit for UI elements in this canvas.
*/
get referenceResolutionPerUnit(): number {
return this._referenceResolutionPerUnit;
}
set referenceResolutionPerUnit(value: number) {
if (this._referenceResolutionPerUnit !== value) {
this._referenceResolutionPerUnit = value;
this._disorderedElements.forEach((element) => {
element._onRootCanvasModify?.(RootCanvasModifyFlags.ReferenceResolutionPerUnit);
});
}
}
/**
* The reference resolution of the UI canvas in `ScreenSpaceCamera` and `ScreenSpaceOverlay` mode.
*/
get referenceResolution(): Vector2 {
return this._referenceResolution;
}
set referenceResolution(value: Vector2) {
const referenceResolution = this._referenceResolution;
if (referenceResolution === value) return;
(referenceResolution.x !== value.x || referenceResolution.y !== value.y) && referenceResolution.copyFrom(value);
}
/**
* The rendering mode of the UI canvas.
*/
get renderMode(): CanvasRenderMode {
return this._renderMode;
}
set renderMode(mode: CanvasRenderMode) {
const preMode = this._renderMode;
if (preMode !== mode) {
this._renderMode = mode;
this._updateCameraObserver();
this._setRealRenderMode(this._getRealRenderMode());
}
}
/**
* The camera used to render the UI canvas in `ScreenSpaceCamera` mode.
* @remarks If set `ScreenSpaceCamera` but no corresponding camera is assigned, the actual rendering mode defaults to `ScreenSpaceOverlay`.
*/
get renderCamera(): Camera {
return this._renderCamera;
}
set renderCamera(value: Camera) {
const preCamera = this._renderCamera;
if (preCamera !== value) {
value &&
this._isSameOrChildEntity(value.entity) &&
Logger.warn(
"Camera entity matching or nested within the canvas entity disables canvas auto-adaptation in ScreenSpaceCamera mode."
);
this._renderCamera = value;
this._updateCameraObserver();
const preRenderMode = this._realRenderMode;
const curRenderMode = this._getRealRenderMode();
if (preRenderMode === curRenderMode) {
if (curRenderMode === CanvasRenderMode.ScreenSpaceCamera) {
this._adapterPoseInScreenSpace();
this._adapterSizeInScreenSpace();
}
} else {
this._setRealRenderMode(curRenderMode);
}
}
}
/**
* The screen resolution adaptation mode of the UI canvas in `ScreenSpaceCamera` and `ScreenSpaceOverlay` mode.
*/
get resolutionAdaptationMode(): ResolutionAdaptationMode {
return this._resolutionAdaptationMode;
}
set resolutionAdaptationMode(value: ResolutionAdaptationMode) {
if (this._resolutionAdaptationMode !== value) {
this._resolutionAdaptationMode = value;
const realRenderMode = this._realRenderMode;
if (
realRenderMode === CanvasRenderMode.ScreenSpaceCamera ||
realRenderMode === CanvasRenderMode.ScreenSpaceOverlay
) {
this._adapterSizeInScreenSpace();
}
}
}
/**
* The rendering order priority of the UI canvas in `ScreenSpaceOverlay` mode.
*/
get sortOrder(): number {
return this._sortOrder;
}
set sortOrder(value: number) {
if (this._sortOrder !== value) {
this._sortOrder = value;
this._realRenderMode === CanvasRenderMode.ScreenSpaceOverlay &&
// @ts-ignore
(this.scene._componentsManager._overlayCanvasesSortingFlag = true);
}
}
/**
* The distance between the UI canvas and the camera in `ScreenSpaceCamera` mode.
*/
get distance(): number {
return this._distance;
}
set distance(value: number) {
if (this._distance !== value) {
this._distance = value;
if (this._realRenderMode === CanvasRenderMode.ScreenSpaceCamera) {
this._adapterPoseInScreenSpace();
this._adapterSizeInScreenSpace();
}
}
}
/**
* @internal
*/
constructor(entity: Entity) {
super(entity);
this._onCanvasSizeListener = this._onCanvasSizeListener.bind(this);
this._onCameraModifyListener = this._onCameraModifyListener.bind(this);
this._onCameraTransformListener = this._onCameraTransformListener.bind(this);
this._onReferenceResolutionChanged = this._onReferenceResolutionChanged.bind(this);
// @ts-ignore
this._referenceResolution._onValueChanged = this._onReferenceResolutionChanged;
this._rootCanvasListener = this._rootCanvasListener.bind(this);
this._centerDirtyFlag = entity.registerWorldChangeFlag();
}
/**
* @internal
*/
_raycast(ray: Ray, out: UIHitResult, distance: number = Number.MAX_SAFE_INTEGER): boolean {
const renderers = this._getRenderers();
for (let i = renderers.length - 1; i >= 0; i--) {
const element = renderers[i];
if (element.raycastEnabled && element._raycast(ray, out, distance)) {
return true;
}
}
out.component = null;
out.entity = null;
out.distance = 0;
out.point.set(0, 0, 0);
out.normal.set(0, 0, 0);
return false;
}
/**
* @internal
*/
_getRootCanvas(): UICanvas {
return this._rootCanvas;
}
/**
* @internal
*/
_canRender(camera: Camera): boolean {
return this._renderMode !== CanvasRenderMode.ScreenSpaceCamera || this._renderCamera === camera;
}
/**
* @internal
*/
_prepareRender(context): void {
const { engine, _realRenderMode: mode } = this;
const { enableFrustumCulling, cullingMask, _frustum: frustum } = context.camera;
const { frameCount } = engine.time;
// @ts-ignore
const renderElement = (this._renderElement = engine._renderElementPool.get());
const virtualCamera = context.virtualCamera;
this._updateSortDistance(virtualCamera.isOrthographic, virtualCamera.position, virtualCamera.forward);
renderElement.set(this.sortOrder, this._sortDistance);
const { width, height } = engine.canvas;
const renderers = this._getRenderers();
for (let i = 0, n = renderers.length; i < n; i++) {
const renderer = renderers[i];
// Filter by camera culling mask
if (!(cullingMask & renderer.entity.layer)) {
continue;
}
// Filter by camera frustum
if (enableFrustumCulling) {
switch (mode) {
case CanvasRenderMode.ScreenSpaceOverlay:
const { min, max } = renderer.bounds;
if (min.x > width || max.x < 0 || min.y > height || max.y < 0) {
continue;
}
break;
case CanvasRenderMode.ScreenSpaceCamera:
case CanvasRenderMode.WorldSpace:
if (!frustum.intersectsBox(renderer.bounds)) {
continue;
}
break;
default:
break;
}
}
renderer._prepareRender(context);
renderer._renderFrameCount = frameCount;
}
}
/**
* @internal
*/
_updateSortDistance(isOrthographic: boolean, cameraPosition: Vector3, cameraForward: Vector3): void {
switch (this._realRenderMode) {
case CanvasRenderMode.ScreenSpaceOverlay:
this._sortDistance = 0;
break;
case CanvasRenderMode.ScreenSpaceCamera:
this._sortDistance = this._distance;
break;
case CanvasRenderMode.WorldSpace:
const boundsCenter = this._getCenter();
if (isOrthographic) {
const distance = UICanvas._tempVec3;
Vector3.subtract(boundsCenter, cameraPosition, distance);
this._sortDistance = Vector3.dot(distance, cameraForward);
} else {
this._sortDistance = Vector3.distanceSquared(boundsCenter, cameraPosition);
}
break;
}
}
// @ts-ignore
override _onEnableInScene(): void {
const entity = this.entity;
// @ts-ignore
entity._dispatchModify(EntityUIModifyFlags.CanvasEnableInScene, this);
const rootCanvas = Utils.searchRootCanvasInParents(this);
this._setIsRootCanvas(!rootCanvas);
Utils.setRootCanvas(this, rootCanvas);
}
// @ts-ignore
override _onDisableInScene(): void {
this._setIsRootCanvas(false);
Utils.cleanRootCanvas(this);
}
/**
* @internal
*/
@ignoreClone
_rootCanvasListener(flag: number, param: any): void {
if (this._isRootCanvas) {
if (flag === EntityModifyFlags.Parent) {
const rootCanvas = Utils.searchRootCanvasInParents(this);
this._setIsRootCanvas(!rootCanvas);
Utils.setRootCanvas(this, rootCanvas);
} else if (flag === EntityUIModifyFlags.CanvasEnableInScene) {
this._setIsRootCanvas(false);
Utils.setRootCanvas(this, <UICanvas>param);
}
} else {
if (flag === EntityModifyFlags.Parent) {
const rootCanvas = Utils.searchRootCanvasInParents(this);
this._setIsRootCanvas(!rootCanvas);
Utils.setRootCanvas(this, rootCanvas);
}
}
}
/**
* @internal
*/
_cloneTo(target: UICanvas, srcRoot: Entity, targetRoot: Entity): void {
target.renderMode = this._renderMode;
const renderCamera = this._renderCamera;
if (renderCamera) {
const paths = UICanvas._targetTempPath;
// @ts-ignore
const success = Entity._getEntityHierarchyPath(srcRoot, renderCamera.entity, paths);
// @ts-ignore
target.renderCamera = success
? // @ts-ignore
Entity._getEntityByHierarchyPath(targetRoot, paths).getComponent(Camera)
: renderCamera;
}
}
private _getRenderers(): UIRenderer[] {
const { _orderedRenderers: renderers, entity } = this;
const uiHierarchyVersion = entity._uiHierarchyVersion;
if (this._hierarchyVersion !== uiHierarchyVersion) {
renderers.length = this._walk(this.entity, renderers);
UICanvas._tempGroupAbleList.length = 0;
this._hierarchyVersion = uiHierarchyVersion;
++UICanvas._hierarchyCounter;
}
return renderers;
}
private _adapterPoseInScreenSpace(): void {
const transform = this.entity.transform;
const realRenderMode = this._realRenderMode;
if (realRenderMode === CanvasRenderMode.ScreenSpaceCamera) {
const cameraEntity = this._renderCamera.entity;
if (!this._isSameOrChildEntity(cameraEntity)) {
const { transform: cameraTransform } = cameraEntity;
const { worldPosition: cameraWorldPosition, worldForward: cameraWorldForward } = cameraTransform;
const distance = this._distance;
transform.setWorldPosition(
cameraWorldPosition.x + cameraWorldForward.x * distance,
cameraWorldPosition.y + cameraWorldForward.y * distance,
cameraWorldPosition.z + cameraWorldForward.z * distance
);
transform.worldRotationQuaternion.copyFrom(cameraTransform.worldRotationQuaternion);
}
} else {
const { canvas } = this.engine;
transform.setWorldPosition(canvas.width * 0.5, canvas.height * 0.5, 0);
transform.worldRotationQuaternion.set(0, 0, 0, 1);
}
}
private _adapterSizeInScreenSpace(): void {
const transform = <UITransform>this.entity.transform;
const realRenderMode = this._realRenderMode;
const { x: width, y: height } = this._referenceResolution;
let curWidth: number;
let curHeight: number;
if (realRenderMode === CanvasRenderMode.ScreenSpaceCamera) {
const renderCamera = this._renderCamera;
curHeight = renderCamera.isOrthographic
? renderCamera.orthographicSize * 2
: 2 * (Math.tan(MathUtil.degreeToRadian(renderCamera.fieldOfView * 0.5)) * this._distance);
curWidth = renderCamera.aspectRatio * curHeight;
} else {
const canvas = this.engine.canvas;
curHeight = canvas.height;
curWidth = canvas.width;
}
let expectX: number, expectY: number, expectZ: number;
switch (this._resolutionAdaptationMode) {
case ResolutionAdaptationMode.WidthAdaptation:
expectX = expectY = expectZ = curWidth / width;
break;
case ResolutionAdaptationMode.HeightAdaptation:
expectX = expectY = expectZ = curHeight / height;
break;
case ResolutionAdaptationMode.BothAdaptation:
expectX = curWidth / width;
expectY = curHeight / height;
expectZ = (expectX + expectY) * 0.5;
break;
case ResolutionAdaptationMode.ExpandAdaptation:
expectX = expectY = expectZ = Math.min(curWidth / width, curHeight / height);
break;
case ResolutionAdaptationMode.ShrinkAdaptation:
expectX = expectY = expectZ = Math.max(curWidth / width, curHeight / height);
break;
default:
break;
}
const worldMatrix = UICanvas._tempMat;
Matrix.affineTransformation(
UICanvas._tempVec3.set(expectX, expectY, expectZ),
transform.worldRotationQuaternion,
transform.worldPosition,
worldMatrix
);
transform.worldMatrix = worldMatrix;
transform.size.set(curWidth / expectX, curHeight / expectY);
}
private _walk(entity: Entity, renderers: UIRenderer[], depth = 0, group: UIGroup = null): number {
// @ts-ignore
const components: Component[] = entity._components;
const tempGroupAbleList = UICanvas._tempGroupAbleList;
let groupAbleCount = 0;
for (let i = 0, n = components.length; i < n; i++) {
const component = components[i];
if (!component.enabled) continue;
if (component instanceof UIRenderer) {
renderers[depth] = component;
++depth;
component._isRootCanvasDirty && Utils.setRootCanvas(component, this);
if (component._isGroupDirty) {
tempGroupAbleList[groupAbleCount++] = component;
}
} else if (component instanceof UIInteractive) {
component._isRootCanvasDirty && Utils.setRootCanvas(component, this);
if (component._isGroupDirty) {
tempGroupAbleList[groupAbleCount++] = component;
}
} else if (component instanceof UIGroup) {
component._isRootCanvasDirty && Utils.setRootCanvas(component, this);
component._isGroupDirty && Utils.setGroup(component, group);
group = component;
}
}
for (let i = 0; i < groupAbleCount; i++) {
Utils.setGroup(tempGroupAbleList[i], group);
}
const children = entity.children;
for (let i = 0, n = children.length; i < n; i++) {
const child = children[i];
child.isActive && (depth = this._walk(child, renderers, depth, group));
}
return depth;
}
private _updateCameraObserver(): void {
const camera =
this._isRootCanvas && this._renderMode === CanvasRenderMode.ScreenSpaceCamera ? this._renderCamera : null;
const preCamera = this._cameraObserver;
if (preCamera !== camera) {
this._cameraObserver = camera;
if (preCamera) {
// @ts-ignore
preCamera.entity._updateFlagManager.removeListener(this._onCameraTransformListener);
// @ts-ignore
preCamera._unRegisterModifyListener(this._onCameraModifyListener);
}
if (camera) {
// @ts-ignore
camera.entity._updateFlagManager.addListener(this._onCameraTransformListener);
// @ts-ignore
camera._registerModifyListener(this._onCameraModifyListener);
}
}
}
@ignoreClone
private _onCameraModifyListener(flag: CameraModifyFlags): void {
if (this._realRenderMode === CanvasRenderMode.ScreenSpaceCamera) {
switch (flag) {
case CameraModifyFlags.ProjectionType:
case CameraModifyFlags.AspectRatio:
this._adapterSizeInScreenSpace();
break;
case CameraModifyFlags.FieldOfView:
!this._renderCamera.isOrthographic && this._adapterSizeInScreenSpace();
break;
case CameraModifyFlags.OrthographicSize:
this._renderCamera.isOrthographic && this._adapterSizeInScreenSpace();
break;
case CameraModifyFlags.DisableInScene:
this._setRealRenderMode(CanvasRenderMode.ScreenSpaceOverlay);
break;
default:
break;
}
} else {
flag === CameraModifyFlags.EnableInScene && this._setRealRenderMode(CanvasRenderMode.ScreenSpaceCamera);
}
}
@ignoreClone
private _onCameraTransformListener(): void {
this._realRenderMode === CanvasRenderMode.ScreenSpaceCamera && this._adapterPoseInScreenSpace();
}
private _addCanvasListener(): void {
// @ts-ignore
this.engine.canvas._sizeUpdateFlagManager.addListener(this._onCanvasSizeListener);
}
private _removeCanvasListener(): void {
// @ts-ignore
this.engine.canvas._sizeUpdateFlagManager.removeListener(this._onCanvasSizeListener);
}
@ignoreClone
private _onCanvasSizeListener(): void {
const { canvas } = this.engine;
this.entity.transform.setWorldPosition(canvas.width * 0.5, canvas.height * 0.5, 0);
this._adapterSizeInScreenSpace();
}
@ignoreClone
private _onReferenceResolutionChanged(): void {
const realRenderMode = this._realRenderMode;
if (
realRenderMode === CanvasRenderMode.ScreenSpaceOverlay ||
realRenderMode === CanvasRenderMode.ScreenSpaceCamera
) {
this._adapterSizeInScreenSpace();
}
}
private _setIsRootCanvas(isRootCanvas: boolean): void {
if (this._isRootCanvas !== isRootCanvas) {
this._isRootCanvas = isRootCanvas;
this._updateCameraObserver();
this._setRealRenderMode(this._getRealRenderMode());
if (isRootCanvas) {
this.entity._updateUIHierarchyVersion(UICanvas._hierarchyCounter);
} else {
const { _disorderedElements: disorderedElements } = this;
disorderedElements.forEach((element: IElement) => {
if (element instanceof UICanvas) {
const rootCanvas = Utils.searchRootCanvasInParents(element);
element._setIsRootCanvas(!rootCanvas);
Utils.setRootCanvas(element, rootCanvas);
} else {
Utils.setRootCanvasDirty(this);
Utils.setGroupDirty(<IGroupAble>element);
}
});
disorderedElements.length = 0;
disorderedElements.garbageCollection();
this._orderedRenderers.length = 0;
}
}
}
private _getCenter(): Vector3 {
if (this._centerDirtyFlag.flag) {
const center = this._center;
const uiTransform = <UITransform>this.entity.transform;
const { pivot, size } = uiTransform;
center.set((0.5 - pivot.x) * size.x, (0.5 - pivot.y) * size.y, 0);
Vector3.transformCoordinate(center, uiTransform.worldMatrix, center);
this._centerDirtyFlag.flag = false;
}
return this._center;
}
private _getRealRenderMode(): number {
if (this._isRootCanvas) {
const mode = this._renderMode;
if (mode === CanvasRenderMode.ScreenSpaceCamera && !this._renderCamera?.enabled) {
return CanvasRenderMode.ScreenSpaceOverlay;
} else {
return mode;
}
} else {
return CanvasRealRenderMode.None;
}
}
private _setRealRenderMode(curRealMode: number): void {
const preRealMode = this._realRenderMode;
if (preRealMode !== curRealMode) {
this._realRenderMode = curRealMode;
// @ts-ignore
const componentsManager = this.scene._componentsManager;
switch (preRealMode) {
case CanvasRenderMode.ScreenSpaceOverlay:
this._removeCanvasListener();
case CanvasRenderMode.ScreenSpaceCamera:
case CanvasRenderMode.WorldSpace:
componentsManager.removeUICanvas(this, preRealMode === CanvasRenderMode.ScreenSpaceOverlay);
break;
default:
break;
}
switch (curRealMode) {
case CanvasRenderMode.ScreenSpaceOverlay:
this._addCanvasListener();
case CanvasRenderMode.ScreenSpaceCamera:
this._adapterPoseInScreenSpace();
this._adapterSizeInScreenSpace();
case CanvasRenderMode.WorldSpace:
componentsManager.addUICanvas(this, curRealMode === CanvasRenderMode.ScreenSpaceOverlay);
break;
default:
break;
}
}
}
private _isSameOrChildEntity(cameraEntity: Entity): boolean {
const canvasEntity = this.entity;
while (cameraEntity) {
if (cameraEntity === canvasEntity) return true;
cameraEntity = cameraEntity.parent;
}
return false;
}
}
/**
* @remarks Extends `CanvasRenderMode`.
*/
enum CanvasRealRenderMode {
None = 4
}
/**
* @remarks Extends `EntityModifyFlags`.
*/
export enum EntityUIModifyFlags {
CanvasEnableInScene = 0x4,
GroupEnableInScene = 0x8
}
export enum RootCanvasModifyFlags {
None = 0x0,
ReferenceResolutionPerUnit = 0x1,
All = 0x1
}

View File

@@ -1,228 +0,0 @@
import { Component, DisorderedArray, Entity, EntityModifyFlags, assignmentClone, ignoreClone } from "@galacean/engine";
import { Utils } from "../Utils";
import { IGroupAble } from "../interface/IGroupAble";
import { EntityUIModifyFlags, UICanvas } from "./UICanvas";
export class UIGroup extends Component implements IGroupAble {
/** @internal */
@ignoreClone
_indexInGroup: number = -1;
/** @internal */
@ignoreClone
_indexInRootCanvas: number = -1;
/** @internal */
@ignoreClone
_group: UIGroup;
/** @internal */
@ignoreClone
_rootCanvas: UICanvas;
/** @internal */
@ignoreClone
_disorderedElements: DisorderedArray<IGroupAble> = new DisorderedArray<IGroupAble>();
/** @internal */
@ignoreClone
_globalAlpha = 1;
/** @internal */
@ignoreClone
_globalInteractive = true;
@assignmentClone
private _alpha = 1;
@assignmentClone
private _interactive = true;
@assignmentClone
private _ignoreParentGroup = false;
/** @internal */
@ignoreClone
_rootCanvasListeningEntities: Entity[] = [];
/** @internal */
@ignoreClone
_groupListeningEntities: Entity[] = [];
/** @internal */
@ignoreClone
_isRootCanvasDirty: boolean = false;
/** @internal */
@ignoreClone
_isGroupDirty: boolean = false;
/** @internal */
@ignoreClone
_groupDirtyFlags: number = GroupModifyFlags.None;
/**
* Whether to ignore the parent group.
* @remarks If this parameter set to `true`,
* all settings of the parent group will be ignored.
*/
get ignoreParentGroup(): boolean {
return this._ignoreParentGroup;
}
set ignoreParentGroup(value: boolean) {
if (this._ignoreParentGroup !== value) {
this._ignoreParentGroup = value;
this._onGroupModify(GroupModifyFlags.All);
}
}
/**
* Whether the group is interactive.
*/
get interactive(): boolean {
return this._interactive;
}
set interactive(value: boolean) {
if (this._interactive !== value) {
this._interactive = value;
this._onGroupModify(GroupModifyFlags.GlobalInteractive);
}
}
/**
* The alpha value of the group.
*/
get alpha(): number {
return this._alpha;
}
set alpha(value: number) {
value = Math.max(0, Math.min(value, 1));
if (this._alpha !== value) {
this._alpha = value;
this._onGroupModify(GroupModifyFlags.GlobalAlpha);
}
}
/**
* @internal
*/
_getGlobalAlpha(): number {
if (this._isContainDirtyFlag(GroupModifyFlags.GlobalAlpha)) {
if (this._ignoreParentGroup) {
this._globalAlpha = this._alpha;
} else {
const parentGroup = this._getGroup();
this._globalAlpha = this._alpha * (parentGroup ? parentGroup._getGlobalAlpha() : 1);
}
this._setDirtyFlagFalse(GroupModifyFlags.GlobalAlpha);
}
return this._globalAlpha;
}
/**
* @internal
*/
_getGlobalInteractive(): boolean {
if (this._isContainDirtyFlag(GroupModifyFlags.GlobalInteractive)) {
if (this._ignoreParentGroup) {
this._globalInteractive = this._interactive;
} else {
const parentGroup = this._getGroup();
this._globalInteractive = this._interactive && (!parentGroup || parentGroup._getGlobalInteractive());
}
this._setDirtyFlagFalse(GroupModifyFlags.GlobalInteractive);
}
return this._globalInteractive;
}
constructor(entity: Entity) {
super(entity);
this._rootCanvasListener = this._rootCanvasListener.bind(this);
this._groupListener = this._groupListener.bind(this);
}
// @ts-ignore
override _onEnableInScene(): void {
Utils.setRootCanvasDirty(this);
Utils.setGroupDirty(this);
// @ts-ignore
this.entity._dispatchModify(EntityUIModifyFlags.GroupEnableInScene);
}
// @ts-ignore
override _onDisableInScene(): void {
Utils.cleanRootCanvas(this);
Utils.cleanGroup(this);
const disorderedElements = this._disorderedElements;
disorderedElements.forEach((element: IGroupAble) => {
Utils.setGroupDirty(element);
});
disorderedElements.length = 0;
disorderedElements.garbageCollection();
this._isRootCanvasDirty = this._isGroupDirty = false;
}
/**
* @internal
*/
_getRootCanvas(): UICanvas {
this._isRootCanvasDirty && Utils.setRootCanvas(this, Utils.searchRootCanvasInParents(this));
return this._rootCanvas;
}
/**
* @internal
*/
_getGroup(): UIGroup {
this._isGroupDirty && Utils.setGroup(this, Utils.searchGroupInParents(this));
return this._group;
}
/**
* @internal
*/
@ignoreClone
_groupListener(flag: number): void {
if (flag === EntityModifyFlags.Parent || flag === EntityUIModifyFlags.GroupEnableInScene) {
Utils.setGroupDirty(this);
}
}
/**
* @internal
*/
@ignoreClone
_rootCanvasListener(flag: number): void {
if (flag === EntityModifyFlags.Parent || flag === EntityUIModifyFlags.CanvasEnableInScene) {
Utils.setRootCanvasDirty(this);
Utils.setGroupDirty(this);
}
}
/**
* @internal
*/
_onGroupModify(flags: GroupModifyFlags, isPass: boolean = false): void {
if (isPass && this._ignoreParentGroup) return;
if (this._isContainDirtyFlags(flags)) return;
this._setDirtyFlagTrue(flags);
this._disorderedElements.forEach((element) => {
element._onGroupModify(flags, true);
});
}
private _isContainDirtyFlags(targetDirtyFlags: number): boolean {
return (this._groupDirtyFlags & targetDirtyFlags) === targetDirtyFlags;
}
private _isContainDirtyFlag(type: number): boolean {
return (this._groupDirtyFlags & type) != 0;
}
private _setDirtyFlagTrue(type: number) {
this._groupDirtyFlags |= type;
}
private _setDirtyFlagFalse(type: number) {
this._groupDirtyFlags &= ~type;
}
}
export enum GroupModifyFlags {
None = 0x0,
GlobalAlpha = 0x1,
GlobalInteractive = 0x2,
All = 0x3
}

View File

@@ -1,300 +0,0 @@
import {
BatchUtils,
Color,
DependentMode,
Entity,
EntityModifyFlags,
Matrix,
Plane,
Ray,
Renderer,
RendererUpdateFlags,
ShaderMacroCollection,
ShaderProperty,
Vector3,
Vector4,
assignmentClone,
deepClone,
dependentComponents,
ignoreClone
} from "@galacean/engine";
import { Utils } from "../Utils";
import { UIHitResult } from "../input/UIHitResult";
import { IGraphics } from "../interface/IGraphics";
import { EntityUIModifyFlags, UICanvas } from "./UICanvas";
import { GroupModifyFlags, UIGroup } from "./UIGroup";
import { UITransform } from "./UITransform";
@dependentComponents(UITransform, DependentMode.AutoAdd)
export class UIRenderer extends Renderer implements IGraphics {
/** @internal */
static _tempVec30: Vector3 = new Vector3();
/** @internal */
static _tempVec31: Vector3 = new Vector3();
/** @internal */
static _tempMat: Matrix = new Matrix();
/** @internal */
static _tempPlane: Plane = new Plane();
/** @internal */
static _textureProperty: ShaderProperty = ShaderProperty.getByName("renderer_UITexture");
/**
* Custom boundary for raycast detection.
* @remarks this is based on `this.entity.transform`.
*/
@deepClone
raycastPadding: Vector4 = new Vector4(0, 0, 0, 0);
/** @internal */
@ignoreClone
_rootCanvas: UICanvas;
/** @internal */
@ignoreClone
_indexInRootCanvas: number = -1;
/** @internal */
@ignoreClone
_isRootCanvasDirty: boolean = false;
/** @internal */
@ignoreClone
_rootCanvasListeningEntities: Entity[] = [];
/** @internal */
@ignoreClone
_group: UIGroup;
/** @internal */
@ignoreClone
_indexInGroup: number = -1;
/** @internal */
@ignoreClone
_isGroupDirty: boolean = false;
/** @internal */
@ignoreClone
_groupListeningEntities: Entity[] = [];
/** @internal */
@ignoreClone
_subChunk;
@assignmentClone
private _raycastEnabled: boolean = true;
@deepClone
protected _color: Color = new Color(1, 1, 1, 1);
/**
* Rendering color for the ui renderer.
*/
get color(): Color {
return this._color;
}
set color(value: Color) {
if (this._color !== value) {
this._color.copyFrom(value);
}
}
/**
* Whether this renderer be picked up by raycast.
*/
get raycastEnabled(): boolean {
return this._raycastEnabled;
}
set raycastEnabled(value: boolean) {
this._raycastEnabled = value;
}
/**
* @internal
*/
constructor(entity: Entity) {
super(entity);
this._dirtyUpdateFlag = RendererUpdateFlags.WorldVolume | UIRendererUpdateFlags.Color;
this._onColorChanged = this._onColorChanged.bind(this);
//@ts-ignore
this._color._onValueChanged = this._onColorChanged;
this._groupListener = this._groupListener.bind(this);
this._rootCanvasListener = this._rootCanvasListener.bind(this);
}
// @ts-ignore
override _canBatch(elementA, elementB): boolean {
return BatchUtils.canBatchSprite(elementA, elementB);
}
// @ts-ignore
override _batch(elementA, elementB?): void {
BatchUtils.batchFor2D(elementA, elementB);
}
// @ts-ignore
override _updateTransformShaderData(context, onlyMVP: boolean, batched: boolean): void {
// @ts-ignore
super._updateTransformShaderData(context, onlyMVP, true);
}
// @ts-ignore
override _prepareRender(context): void {
// Update once per frame per renderer, not influenced by batched
if (this._renderFrameCount !== this.engine.time.frameCount) {
this._update(context);
}
this._render(context);
// union camera global macro and renderer macro.
ShaderMacroCollection.unionCollection(
context.camera._globalShaderMacro,
// @ts-ignore
this.shaderData._macroCollection,
//@ts-ignore
this._globalShaderMacro
);
}
// @ts-ignore
override _onEnableInScene(): void {
// @ts-ignore
this._overrideUpdate && this.scene._componentsManager.addOnUpdateRenderers(this);
this.entity._updateUIHierarchyVersion(UICanvas._hierarchyCounter);
Utils.setRootCanvasDirty(this);
Utils.setGroupDirty(this);
}
// @ts-ignore
override _onDisableInScene(): void {
// @ts-ignore
this._overrideUpdate && this.scene._componentsManager.removeOnUpdateRenderers(this);
this.entity._updateUIHierarchyVersion(UICanvas._hierarchyCounter);
Utils.cleanRootCanvas(this);
Utils.cleanGroup(this);
}
/**
* @internal
*/
_getGlobalAlpha(): number {
return this._getGroup()?._getGlobalAlpha() ?? 1;
}
/**
* @internal
*/
_getRootCanvas(): UICanvas {
this._isRootCanvasDirty && Utils.setRootCanvas(this, Utils.searchRootCanvasInParents(this));
return this._rootCanvas;
}
/**
* @internal
*/
_getGroup(): UIGroup {
this._isGroupDirty && Utils.setGroup(this, Utils.searchGroupInParents(this));
return this._group;
}
/**
* @internal
*/
@ignoreClone
_groupListener(flag: number): void {
if (flag === EntityModifyFlags.Parent || flag === EntityUIModifyFlags.GroupEnableInScene) {
Utils.setGroupDirty(this);
}
}
/**
* @internal
*/
@ignoreClone
_rootCanvasListener(flag: number, entity: Entity): void {
switch (flag) {
case EntityModifyFlags.Parent:
Utils.setRootCanvasDirty(this);
Utils.setGroupDirty(this);
case EntityModifyFlags.Child:
entity._updateUIHierarchyVersion(UICanvas._hierarchyCounter);
break;
default:
break;
}
}
/**
* @internal
*/
_onGroupModify(flags: GroupModifyFlags): void {
if (flags & GroupModifyFlags.GlobalAlpha) {
this._dirtyUpdateFlag |= UIRendererUpdateFlags.Color;
}
}
@ignoreClone
private _onColorChanged(): void {
this._dirtyUpdateFlag |= UIRendererUpdateFlags.Color;
}
/**
* @internal
*/
_getChunkManager() {
// @ts-ignore
return this.engine._batcherManager.primitiveChunkManagerUI;
}
/**
* @internal
*/
_raycast(ray: Ray, out: UIHitResult, distance: number = Number.MAX_SAFE_INTEGER): boolean {
const plane = UIRenderer._tempPlane;
const transform = <UITransform>this._transformEntity.transform;
const normal = plane.normal.copyFrom(transform.worldForward);
plane.distance = -Vector3.dot(normal, transform.worldPosition);
const curDistance = ray.intersectPlane(plane);
if (curDistance >= 0 && curDistance < distance) {
const hitPointWorld = ray.getPoint(curDistance, UIRenderer._tempVec30);
const worldMatrixInv = UIRenderer._tempMat;
Matrix.invert(transform.worldMatrix, worldMatrixInv);
const localPosition = UIRenderer._tempVec31;
Vector3.transformCoordinate(hitPointWorld, worldMatrixInv, localPosition);
if (this._hitTest(localPosition)) {
out.component = this;
out.distance = curDistance;
out.entity = this.entity;
out.normal.copyFrom(normal);
out.point.copyFrom(hitPointWorld);
return true;
}
}
return false;
}
protected _hitTest(localPosition: Vector3): boolean {
const { x, y } = localPosition;
const uiTransform = <UITransform>this._transformEntity.transform;
const { x: width, y: height } = uiTransform.size;
const { x: pivotX, y: pivotY } = uiTransform.pivot;
const { x: paddingLeft, y: paddingBottom, z: paddingRight, w: paddingTop } = this.raycastPadding;
return (
x >= -width * pivotX + paddingLeft &&
x <= width * (1 - pivotX) - paddingRight &&
y >= -height * pivotY + paddingTop &&
y <= height * (1 - pivotY) - paddingBottom
);
}
protected override _onDestroy(): void {
if (this._subChunk) {
this._getChunkManager().freeSubChunk(this._subChunk);
this._subChunk = null;
}
super._onDestroy();
//@ts-ignore
this._color._onValueChanged = null;
this._color = null;
}
}
/**
* @remarks Extends `RendererUpdateFlags`.
*/
export enum UIRendererUpdateFlags {
Color = 0x2
}

View File

@@ -1,446 +0,0 @@
import {
Entity,
MathUtil,
Rect,
Transform,
TransformModifyFlags,
Vector2,
deepClone,
ignoreClone
} from "@galacean/engine";
import { HorizontalAlignmentMode } from "../enums/HorizontalAlignmentMode";
import { VerticalAlignmentMode } from "../enums/VerticalAlignmentMode";
/**
* The Transform component exclusive to the UI element.
*/
export class UITransform extends Transform {
@ignoreClone
private _size = new Vector2(100, 100);
@ignoreClone
private _pivot = new Vector2(0.5, 0.5);
@deepClone
private _rect = new Rect(-50, -50, 100, 100);
private _alignLeft = 0;
private _alignRight = 0;
private _alignCenter = 0;
private _alignTop = 0;
private _alignBottom = 0;
private _alignMiddle = 0;
private _horizontalAlignment = HorizontalAlignmentMode.None;
private _verticalAlignment = VerticalAlignmentMode.None;
/**
* Width and height of UI element.
*/
get size(): Vector2 {
return this._size;
}
set size(value: Vector2) {
const { _size: size } = this;
if (size === value) return;
(size.x !== value.x || size.y !== value.y) && size.copyFrom(value);
}
/**
* Pivot of UI element.
*/
get pivot(): Vector2 {
return this._pivot;
}
set pivot(value: Vector2) {
const { _pivot: pivot } = this;
if (pivot === value) return;
(pivot.x !== value.x || pivot.y !== value.y) && pivot.copyFrom(value);
}
/**
* Horizontal alignment mode.
*
* @remarks
* Controls how the element aligns horizontally within its parent:
* - `Left` - Align to parent's left edge
* - `Center` - Align to parent's horizontal center
* - `Right` - Align to parent's right edge
* - `LeftAndRight` - Align to both left and right edges (stretch to fill width)
*/
get horizontalAlignment(): HorizontalAlignmentMode {
return this._horizontalAlignment;
}
set horizontalAlignment(value: HorizontalAlignmentMode) {
if (this._horizontalAlignment === value) return;
this._horizontalAlignment = value;
switch (value) {
case HorizontalAlignmentMode.Left:
case HorizontalAlignmentMode.Right:
case HorizontalAlignmentMode.Center:
this._onPositionChanged();
break;
case HorizontalAlignmentMode.LeftAndRight:
this._onPositionChanged();
this._onSizeChanged();
break;
default:
break;
}
}
/**
* Left margin when horizontalAlignment is Left or LeftAndRight.
*
* @remarks
* Only effective when horizontalAlignment includes Left mode.
* Distance from the parent's left edge to the element's left edge.
*/
get alignLeft(): number {
return this._alignLeft;
}
set alignLeft(value: number) {
if (!Number.isFinite(value)) return;
if (MathUtil.equals(value, this._alignLeft)) return;
this._alignLeft = value;
if (this._horizontalAlignment & HorizontalAlignmentMode.Left) {
this._onPositionChanged();
this._horizontalAlignment & HorizontalAlignmentMode.Right && this._onSizeChanged();
}
}
/**
* Right margin when horizontalAlignment is Right or LeftAndRight.
*
* @remarks
* Only effective when horizontalAlignment includes Right mode.
* Distance from the parent's right edge to the element's right edge.
*/
get alignRight(): number {
return this._alignRight;
}
set alignRight(value: number) {
if (!Number.isFinite(value)) return;
if (MathUtil.equals(value, this._alignRight)) return;
this._alignRight = value;
if (this._horizontalAlignment & HorizontalAlignmentMode.Right) {
this._onPositionChanged();
this._horizontalAlignment & HorizontalAlignmentMode.Left && this._onSizeChanged();
}
}
/**
* Horizontal center offset when horizontalAlignment is Center.
*
* @remarks
* Only effective when horizontalAlignment is Center mode.
* Positive values move the element to the right, negative values to the left.
*/
get alignCenter(): number {
return this._alignCenter;
}
set alignCenter(value: number) {
if (!Number.isFinite(value)) return;
if (MathUtil.equals(value, this._alignCenter)) return;
this._alignCenter = value;
this._horizontalAlignment & HorizontalAlignmentMode.Center && this._onPositionChanged();
}
/**
* Vertical alignment mode.
*
* @remarks
* Controls how the element aligns vertically within its parent:
* - `Top` - Align to parent's top edge
* - `Middle` - Align to parent's vertical center
* - `Bottom` - Align to parent's bottom edge
* - `TopAndBottom` - Align to both top and bottom edges (stretch to fill height)
*/
get verticalAlignment(): VerticalAlignmentMode {
return this._verticalAlignment;
}
set verticalAlignment(value: VerticalAlignmentMode) {
if (this._verticalAlignment === value) return;
this._verticalAlignment = value;
switch (value) {
case VerticalAlignmentMode.Top:
case VerticalAlignmentMode.Bottom:
case VerticalAlignmentMode.Middle:
this._onPositionChanged();
break;
case VerticalAlignmentMode.TopAndBottom:
this._onPositionChanged();
this._onSizeChanged();
break;
default:
break;
}
}
/**
* Top margin when verticalAlignment is Top or TopAndBottom.
*
* @remarks
* Only effective when verticalAlignment includes Top mode.
* Used to offset the element from the parent's top edge.
*/
get alignTop(): number {
return this._alignTop;
}
set alignTop(value: number) {
if (!Number.isFinite(value)) return;
if (MathUtil.equals(value, this._alignTop)) return;
this._alignTop = value;
if (this._verticalAlignment & VerticalAlignmentMode.Top) {
this._onPositionChanged();
this._verticalAlignment & VerticalAlignmentMode.Bottom && this._onSizeChanged();
}
}
/**
* Bottom inset used in vertical alignment formulas.
*/
get alignBottom(): number {
return this._alignBottom;
}
set alignBottom(value: number) {
if (!Number.isFinite(value)) return;
if (MathUtil.equals(value, this._alignBottom)) return;
this._alignBottom = value;
if (this._verticalAlignment & VerticalAlignmentMode.Bottom) {
this._onPositionChanged();
this._verticalAlignment & VerticalAlignmentMode.Top && this._onSizeChanged();
}
}
/**
* Vertical middle offset relative to parent's middle.
*/
get alignMiddle(): number {
return this._alignMiddle;
}
set alignMiddle(value: number) {
if (!Number.isFinite(value)) return;
if (MathUtil.equals(value, this._alignMiddle)) return;
this._alignMiddle = value;
this._verticalAlignment & VerticalAlignmentMode.Middle && this._onPositionChanged();
}
/**
* @internal
*/
constructor(entity: Entity) {
super(entity);
this._onSizeChanged = this._onSizeChanged.bind(this);
this._onPivotChanged = this._onPivotChanged.bind(this);
// @ts-ignore
this._size._onValueChanged = this._onSizeChanged;
// @ts-ignore
this._pivot._onValueChanged = this._onPivotChanged;
}
/**
* @internal
*/
_parentChange(): void {
this._isParentDirty = true;
this._updateWorldFlagWithParentRectChange(TransformModifyFlags.WmWpWeWqWsWus);
}
// @ts-ignore
override _cloneTo(target: UITransform, srcRoot: Entity, targetRoot: Entity): void {
// @ts-ignore
super._cloneTo(target, srcRoot, targetRoot);
const { _size: size, _pivot: pivot } = target;
// @ts-ignore
size._onValueChanged = pivot._onValueChanged = null;
size.copyFrom(this._size);
pivot.copyFrom(this._pivot);
// @ts-ignore
size._onValueChanged = target._onSizeChanged;
// @ts-ignore
pivot._onValueChanged = target._onPivotChanged;
}
protected override _onLocalMatrixChanging(): void {
// `super._onLocalMatrixChanging()` will set `LocalMatrix` dirty flag `false`
// If there is an alignment, `position` and `localMatrix` will be reset again
if (this._horizontalAlignment || this._verticalAlignment) {
this._updatePositionByAlignment();
this._setDirtyFlagTrue(TransformModifyFlags.LocalMatrix);
} else {
super._onLocalMatrixChanging();
}
}
protected override _onWorldMatrixChanging(): void {
// `super._onWorldMatrixChanging()` will set `WorldMatrix` dirty flag `false`
// If there is an alignment, `position` and `worldMatrix` will be reset again(`worldMatrix` dirty flag is already `true`)
!this._horizontalAlignment && !this._verticalAlignment && super._onWorldMatrixChanging();
}
@ignoreClone
protected override _onPositionChanged(): void {
(this._horizontalAlignment || this._verticalAlignment) && this._updatePositionByAlignment();
super._onPositionChanged();
}
@ignoreClone
protected override _onWorldPositionChanged(): void {
super._onWorldPositionChanged();
if (this._horizontalAlignment || this._verticalAlignment) {
this._setDirtyFlagTrue(TransformModifyFlags.WorldPosition);
}
}
private _updatePositionByAlignment(): void {
const parentRect = (this._getParentTransform() as UITransform)?._rect;
if (parentRect) {
const position = this.position;
// @ts-ignore
position._onValueChanged = null;
const rect = this._rect;
switch (this._horizontalAlignment) {
case HorizontalAlignmentMode.Left:
case HorizontalAlignmentMode.LeftAndRight:
position.x = parentRect.x - rect.x + this._alignLeft;
break;
case HorizontalAlignmentMode.Center:
position.x = parentRect.x + parentRect.width * 0.5 - rect.x - rect.width * 0.5 + this._alignCenter;
break;
case HorizontalAlignmentMode.Right:
position.x = parentRect.x + parentRect.width - rect.x - rect.width - this._alignRight;
break;
default:
break;
}
switch (this._verticalAlignment) {
case VerticalAlignmentMode.Top:
position.y = parentRect.y + parentRect.height - rect.y - rect.height - this._alignTop;
break;
case VerticalAlignmentMode.Middle:
position.y = parentRect.y + parentRect.height * 0.5 - rect.y - rect.height * 0.5 + this._alignMiddle;
break;
case VerticalAlignmentMode.Bottom:
case VerticalAlignmentMode.TopAndBottom:
position.y = parentRect.y - rect.y + this._alignBottom;
break;
default:
break;
}
// @ts-ignore
position._onValueChanged = this._onPositionChanged;
}
}
private _updateSizeByAlignment(): void {
const parentRect = (this._getParentTransform() as UITransform)?._rect;
if (parentRect) {
const size = this._size;
// @ts-ignore
size._onValueChanged = null;
// The values of size must be greater than 0
if (this._horizontalAlignment === HorizontalAlignmentMode.LeftAndRight) {
size.x = Math.max(parentRect.width - this._alignLeft - this._alignRight, 0);
}
if (this._verticalAlignment === VerticalAlignmentMode.TopAndBottom) {
size.y = Math.max(parentRect.height - this._alignTop - this._alignBottom, 0);
}
// @ts-ignore
size._onValueChanged = this._onSizeChanged;
}
}
private _updateRectBySizeAndPivot(): void {
const { size, _pivot: pivot } = this;
const x = -pivot.x * size.x;
const y = -pivot.y * size.y;
this._rect.set(x, y, size.x, size.y);
}
@ignoreClone
private _onSizeChanged(): void {
if (
this._horizontalAlignment === HorizontalAlignmentMode.LeftAndRight ||
this._verticalAlignment === VerticalAlignmentMode.TopAndBottom
) {
this._updateSizeByAlignment();
}
this._updateRectBySizeAndPivot();
this._updateWorldFlagWithSelfRectChange();
// @ts-ignore
this._entity._updateFlagManager.dispatch(UITransformModifyFlags.Size);
}
@ignoreClone
private _onPivotChanged(): void {
this._updateRectBySizeAndPivot();
this._updateWorldFlagWithSelfRectChange();
// @ts-ignore
this._entity._updateFlagManager.dispatch(UITransformModifyFlags.Pivot);
}
private _updateWorldFlagWithSelfRectChange(): void {
let worldFlags = 0;
if (this._horizontalAlignment || this._verticalAlignment) {
this._updatePositionByAlignment();
this._setDirtyFlagTrue(TransformModifyFlags.LocalMatrix);
worldFlags = TransformModifyFlags.WmWp;
!this._isContainDirtyFlags(worldFlags) && this._worldAssociatedChange(worldFlags);
}
const children = this.entity.children;
for (let i = 0, n = children.length; i < n; i++) {
(children[i].transform as UITransform)?._updateWorldFlagWithParentRectChange?.(worldFlags);
}
}
private _updateWorldFlagWithParentRectChange(flags: number, parentChange: boolean = true): void {
let selfChange = false;
if (parentChange) {
const { _horizontalAlignment: horizontalAlignment, _verticalAlignment: verticalAlignment } = this;
if (horizontalAlignment || verticalAlignment) {
if (
horizontalAlignment === HorizontalAlignmentMode.LeftAndRight ||
verticalAlignment === VerticalAlignmentMode.TopAndBottom
) {
this._updateSizeByAlignment();
this._updateRectBySizeAndPivot();
selfChange = true;
}
this._updatePositionByAlignment();
this._setDirtyFlagTrue(TransformModifyFlags.LocalMatrix);
flags |= TransformModifyFlags.WmWp;
}
}
const containDirtyFlags = this._isContainDirtyFlags(flags);
!containDirtyFlags && this._worldAssociatedChange(flags);
if (selfChange || !containDirtyFlags) {
const children = this.entity.children;
for (let i = 0, n = children.length; i < n; i++) {
(children[i].transform as UITransform)?._updateWorldFlagWithParentRectChange?.(flags, selfChange);
}
}
// @ts-ignore
selfChange && this._entity._updateFlagManager.dispatch(UITransformModifyFlags.Size);
}
}
/**
* @internal
* extends TransformModifyFlags
*/
export enum UITransformModifyFlags {
Size = 0x200,
Pivot = 0x400
}

View File

@@ -1,42 +0,0 @@
import { ignoreClone, PointerEventData, SafeLoopArray } from "@galacean/engine";
import { UIInteractive } from "../interactive/UIInteractive";
export class Button extends UIInteractive {
@ignoreClone
private _listeners: SafeLoopArray<IUIListener> = new SafeLoopArray<IUIListener>();
/**
* Add a listening function for click.
* @param listener - The listening function
*/
addClicked(listener: (event: PointerEventData) => void): void {
this._listeners.push({ fn: listener });
}
/**
* Remove a listening function of click.
* @param listener - The listening function
*/
removeClicked(listener: (event: PointerEventData) => void): void {
this._listeners.findAndRemove((value) => (value.fn === listener ? (value.destroyed = true) : false));
}
override onPointerClick(event: PointerEventData): void {
if (!this._getGlobalInteractive()) return;
const listeners = this._listeners.getLoopArray();
for (let i = 0, n = listeners.length; i < n; i++) {
const listener = listeners[i];
!listener.destroyed && listener.fn(event);
}
}
override onDestroy(): void {
super.onDestroy();
this._listeners.findAndRemove((value) => (value.destroyed = true));
}
}
export interface IUIListener {
fn: (event: PointerEventData) => void;
destroyed?: boolean;
}

View File

@@ -1,326 +0,0 @@
import {
BoundingBox,
Entity,
ISpriteAssembler,
ISpriteRenderer,
MathUtil,
RenderQueueFlags,
RendererUpdateFlags,
SimpleSpriteAssembler,
SlicedSpriteAssembler,
Sprite,
SpriteDrawMode,
SpriteModifyFlags,
SpriteTileMode,
TiledSpriteAssembler,
assignmentClone,
ignoreClone
} from "@galacean/engine";
import { CanvasRenderMode } from "../../enums/CanvasRenderMode";
import { RootCanvasModifyFlags } from "../UICanvas";
import { UIRenderer, UIRendererUpdateFlags } from "../UIRenderer";
import { UITransform, UITransformModifyFlags } from "../UITransform";
/**
* UI element that renders an image.
*/
export class Image extends UIRenderer implements ISpriteRenderer {
@ignoreClone
private _sprite: Sprite = null;
@ignoreClone
private _drawMode: SpriteDrawMode;
@ignoreClone
private _assembler: ISpriteAssembler;
@assignmentClone
private _tileMode: SpriteTileMode = SpriteTileMode.Continuous;
@assignmentClone
private _tiledAdaptiveThreshold: number = 0.5;
/**
* The draw mode of the image.
*/
get drawMode(): SpriteDrawMode {
return this._drawMode;
}
set drawMode(value: SpriteDrawMode) {
if (this._drawMode !== value) {
this._drawMode = value;
switch (value) {
case SpriteDrawMode.Simple:
this._assembler = SimpleSpriteAssembler;
break;
case SpriteDrawMode.Sliced:
this._assembler = SlicedSpriteAssembler;
break;
case SpriteDrawMode.Tiled:
this._assembler = TiledSpriteAssembler;
break;
default:
break;
}
this._assembler.resetData(this);
this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor;
}
}
/**
* The tiling mode of the image. (Only works in tiled mode.)
*/
get tileMode(): SpriteTileMode {
return this._tileMode;
}
set tileMode(value: SpriteTileMode) {
if (this._tileMode !== value) {
this._tileMode = value;
if (this.drawMode === SpriteDrawMode.Tiled) {
this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor;
}
}
}
/**
* Stretch Threshold in Tile Adaptive Mode, specified in normalized. (Only works in tiled adaptive mode.)
*/
get tiledAdaptiveThreshold(): number {
return this._tiledAdaptiveThreshold;
}
set tiledAdaptiveThreshold(value: number) {
if (value !== this._tiledAdaptiveThreshold) {
value = MathUtil.clamp(value, 0, 1);
this._tiledAdaptiveThreshold = value;
if (this.drawMode === SpriteDrawMode.Tiled) {
this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor;
}
}
}
/**
* The Sprite to render.
*/
get sprite(): Sprite {
return this._sprite;
}
set sprite(value: Sprite | null) {
const lastSprite = this._sprite;
if (lastSprite !== value) {
if (lastSprite) {
this._addResourceReferCount(lastSprite, -1);
// @ts-ignore
lastSprite._updateFlagManager.removeListener(this._onSpriteChange);
}
this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor;
if (value) {
this._addResourceReferCount(value, 1);
// @ts-ignore
value._updateFlagManager.addListener(this._onSpriteChange);
this.shaderData.setTexture(UIRenderer._textureProperty, value.texture);
} else {
this.shaderData.setTexture(UIRenderer._textureProperty, null);
}
this._sprite = value;
}
}
/**
* @internal
*/
constructor(entity: Entity) {
super(entity);
this.drawMode = SpriteDrawMode.Simple;
this.setMaterial(this._engine._getUIDefaultMaterial());
this._onSpriteChange = this._onSpriteChange.bind(this);
}
/**
* @internal
*/
_onRootCanvasModify(flag: RootCanvasModifyFlags): void {
if (flag & RootCanvasModifyFlags.ReferenceResolutionPerUnit) {
const drawMode = this._drawMode;
if (drawMode === SpriteDrawMode.Tiled) {
this._dirtyUpdateFlag |= ImageUpdateFlags.All;
} else if (drawMode === SpriteDrawMode.Sliced) {
this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume;
}
}
}
/**
* @internal
*/
_cloneTo(target: Image, srcRoot: Entity, targetRoot: Entity): void {
// @ts-ignore
super._cloneTo(target, srcRoot, targetRoot);
target.sprite = this._sprite;
target.drawMode = this._drawMode;
}
protected override _updateBounds(worldBounds: BoundingBox): void {
const sprite = this._sprite;
const rootCanvas = this._getRootCanvas();
if (sprite && rootCanvas) {
const transform = <UITransform>this._transformEntity.transform;
const { size } = transform;
this._assembler.updatePositions(
this,
transform.worldMatrix,
size.x,
size.y,
transform.pivot,
false,
false,
rootCanvas.referenceResolutionPerUnit
);
} else {
const { worldPosition } = this._transformEntity.transform;
worldBounds.min.copyFrom(worldPosition);
worldBounds.max.copyFrom(worldPosition);
}
}
protected override _render(context): void {
const { _sprite: sprite } = this;
const transform = <UITransform>this._transformEntity.transform;
const { x: width, y: height } = transform.size;
if (!sprite?.texture || !width || !height) {
return;
}
let material = this.getMaterial();
if (!material) {
return;
}
// @todo: This question needs to be raised rather than hidden.
if (material.destroyed) {
material = this._engine._getUIDefaultMaterial();
}
const alpha = this._getGlobalAlpha();
if (this._color.a * alpha <= 0) {
return;
}
let { _dirtyUpdateFlag: dirtyUpdateFlag } = this;
const canvas = this._getRootCanvas();
// Update position
if (dirtyUpdateFlag & RendererUpdateFlags.WorldVolume) {
this._assembler.updatePositions(
this,
transform.worldMatrix,
width,
height,
transform.pivot,
false,
false,
canvas.referenceResolutionPerUnit
);
dirtyUpdateFlag &= ~RendererUpdateFlags.WorldVolume;
}
// Update uv
if (dirtyUpdateFlag & ImageUpdateFlags.UV) {
this._assembler.updateUVs(this);
dirtyUpdateFlag &= ~ImageUpdateFlags.UV;
}
// Update color
if (dirtyUpdateFlag & UIRendererUpdateFlags.Color) {
this._assembler.updateColor(this, alpha);
dirtyUpdateFlag &= ~UIRendererUpdateFlags.Color;
}
this._dirtyUpdateFlag = dirtyUpdateFlag;
// Init sub render element.
const { engine } = context.camera;
const subRenderElement = engine._subRenderElementPool.get();
const subChunk = this._subChunk;
subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, this.sprite.texture, subChunk);
if (canvas._realRenderMode === CanvasRenderMode.ScreenSpaceOverlay) {
subRenderElement.shaderPasses = material.shader.subShaders[0].passes;
subRenderElement.renderQueueFlags = RenderQueueFlags.All;
}
canvas._renderElement.addSubRenderElement(subRenderElement);
}
@ignoreClone
protected override _onTransformChanged(type: number): void {
if (type & UITransformModifyFlags.Size && this._drawMode === SpriteDrawMode.Tiled) {
this._dirtyUpdateFlag |= ImageUpdateFlags.All;
}
this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume;
}
protected override _onDestroy(): void {
const sprite = this._sprite;
if (sprite) {
this._addResourceReferCount(sprite, -1);
// @ts-ignore
sprite._updateFlagManager.removeListener(this._onSpriteChange);
this._sprite = null;
}
super._onDestroy();
}
@ignoreClone
private _onSpriteChange(type: SpriteModifyFlags): void {
switch (type) {
case SpriteModifyFlags.texture:
this.shaderData.setTexture(UIRenderer._textureProperty, this.sprite.texture);
break;
case SpriteModifyFlags.size:
switch (this._drawMode) {
case SpriteDrawMode.Sliced:
this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume;
break;
case SpriteDrawMode.Tiled:
this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor;
break;
default:
break;
}
break;
case SpriteModifyFlags.border:
switch (this._drawMode) {
case SpriteDrawMode.Sliced:
this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV;
break;
case SpriteDrawMode.Tiled:
this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor;
break;
default:
break;
}
break;
case SpriteModifyFlags.region:
case SpriteModifyFlags.atlasRegionOffset:
this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV;
break;
case SpriteModifyFlags.atlasRegion:
this._dirtyUpdateFlag |= ImageUpdateFlags.UV;
break;
case SpriteModifyFlags.destroy:
this.sprite = null;
break;
}
}
}
/**
* @remarks Extends `UIRendererUpdateFlags`.
*/
enum ImageUpdateFlags {
/** UV. */
UV = 0x4,
/** Automatic Size. */
AutomaticSize = 0x8,
/** WorldVolume and UV. */
WorldVolumeAndUV = 0x5,
/** WorldVolume, UV and Color. */
WorldVolumeUVAndColor = 0x7,
/** All. */
All = 0xf
}

View File

@@ -1,675 +0,0 @@
import {
BoundingBox,
CharRenderInfo,
Engine,
Entity,
Font,
FontStyle,
ITextRenderer,
OverflowMode,
RenderQueueFlags,
RendererUpdateFlags,
ShaderData,
ShaderDataGroup,
ShaderProperty,
SubFont,
TextHorizontalAlignment,
TextUtils,
TextVerticalAlignment,
Texture2D,
Vector3,
assignmentClone,
ignoreClone
} from "@galacean/engine";
import { CanvasRenderMode } from "../../enums/CanvasRenderMode";
import { RootCanvasModifyFlags } from "../UICanvas";
import { UIRenderer, UIRendererUpdateFlags } from "../UIRenderer";
import { UITransform, UITransformModifyFlags } from "../UITransform";
/**
* UI component used to render text.
*/
export class Text extends UIRenderer implements ITextRenderer {
private static _textTextureProperty = ShaderProperty.getByName("renderElement_TextTexture");
private static _worldPositions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()];
private static _charRenderInfos: CharRenderInfo[] = [];
@ignoreClone
private _textChunks = Array<TextChunk>();
@ignoreClone
private _subFont: SubFont = null;
@assignmentClone
private _text: string = "";
@ignoreClone
private _localBounds: BoundingBox = new BoundingBox();
@assignmentClone
private _font: Font = null;
@assignmentClone
private _fontSize: number = 24;
@assignmentClone
private _fontStyle: FontStyle = FontStyle.None;
@assignmentClone
private _lineSpacing: number = 0;
@assignmentClone
private _horizontalAlignment: TextHorizontalAlignment = TextHorizontalAlignment.Center;
@assignmentClone
private _verticalAlignment: TextVerticalAlignment = TextVerticalAlignment.Center;
@assignmentClone
private _enableWrapping: boolean = false;
@assignmentClone
private _overflowMode: OverflowMode = OverflowMode.Overflow;
/**
* Rendering string for the Text.
*/
get text(): string {
return this._text;
}
set text(value: string) {
value = value || "";
if (this._text !== value) {
this._text = value;
this._setDirtyFlagTrue(DirtyFlag.Position);
}
}
/**
* The font of the Text.
*/
get font(): Font {
return this._font;
}
set font(value: Font) {
const lastFont = this._font;
if (lastFont !== value) {
lastFont && this._addResourceReferCount(lastFont, -1);
value && this._addResourceReferCount(value, 1);
this._font = value;
this._setDirtyFlagTrue(DirtyFlag.Font);
}
}
/**
* The font size of the Text.
*/
get fontSize(): number {
return this._fontSize;
}
set fontSize(value: number) {
if (this._fontSize !== value) {
this._fontSize = value;
this._setDirtyFlagTrue(DirtyFlag.Font);
}
}
/**
* The style of the font.
*/
get fontStyle(): FontStyle {
return this._fontStyle;
}
set fontStyle(value: FontStyle) {
if (this.fontStyle !== value) {
this._fontStyle = value;
this._setDirtyFlagTrue(DirtyFlag.Font);
}
}
/**
* The space between two lines (in pixels).
*/
get lineSpacing(): number {
return this._lineSpacing;
}
set lineSpacing(value: number) {
if (this._lineSpacing !== value) {
this._lineSpacing = value;
this._setDirtyFlagTrue(DirtyFlag.Position);
}
}
/**
* The horizontal alignment.
*/
get horizontalAlignment(): TextHorizontalAlignment {
return this._horizontalAlignment;
}
set horizontalAlignment(value: TextHorizontalAlignment) {
if (this._horizontalAlignment !== value) {
this._horizontalAlignment = value;
this._setDirtyFlagTrue(DirtyFlag.Position);
}
}
/**
* The vertical alignment.
*/
get verticalAlignment(): TextVerticalAlignment {
return this._verticalAlignment;
}
set verticalAlignment(value: TextVerticalAlignment) {
if (this._verticalAlignment !== value) {
this._verticalAlignment = value;
this._setDirtyFlagTrue(DirtyFlag.Position);
}
}
/**
* Whether wrap text to next line when exceeds the width of the container.
*/
get enableWrapping(): boolean {
return this._enableWrapping;
}
set enableWrapping(value: boolean) {
if (this._enableWrapping !== value) {
this._enableWrapping = value;
this._setDirtyFlagTrue(DirtyFlag.Position);
}
}
/**
* The overflow mode.
*/
get overflowMode(): OverflowMode {
return this._overflowMode;
}
set overflowMode(value: OverflowMode) {
if (this._overflowMode !== value) {
this._overflowMode = value;
this._setDirtyFlagTrue(DirtyFlag.Position);
}
}
/**
* The mask layer the sprite renderer belongs to.
*/
get maskLayer(): number {
return this._maskLayer;
}
set maskLayer(value: number) {
this._maskLayer = value;
}
/**
* The bounding volume of the TextRenderer.
*/
override get bounds(): BoundingBox {
if (this._isTextNoVisible()) {
if (this._isContainDirtyFlag(RendererUpdateFlags.WorldVolume)) {
const localBounds = this._localBounds;
localBounds.min.set(0, 0, 0);
localBounds.max.set(0, 0, 0);
this._updateBounds(this._bounds);
this._setDirtyFlagFalse(RendererUpdateFlags.WorldVolume);
}
return this._bounds;
}
this._isContainDirtyFlag(DirtyFlag.SubFont) && this._resetSubFont();
this._isContainDirtyFlag(DirtyFlag.LocalPositionBounds) && this._updateLocalData();
this._isContainDirtyFlag(DirtyFlag.WorldPosition) && this._updatePosition();
this._isContainDirtyFlag(RendererUpdateFlags.WorldVolume) && this._updateBounds(this._bounds);
this._setDirtyFlagFalse(DirtyFlag.Font);
return this._bounds;
}
constructor(entity: Entity) {
super(entity);
const { engine } = this;
// @ts-ignore
this.font = engine._textDefaultFont;
this.raycastEnabled = false;
// @ts-ignore
this.setMaterial(engine._basicResources.textDefaultMaterial);
}
/**
* @internal
*/
protected override _onDestroy(): void {
if (this._font) {
this._addResourceReferCount(this._font, -1);
this._font = null;
}
super._onDestroy();
this._freeTextChunks();
this._textChunks = null;
this._subFont && (this._subFont = null);
}
// @ts-ignore
override _cloneTo(target: Text, srcRoot: Entity, targetRoot: Entity): void {
// @ts-ignore
super._cloneTo(target, srcRoot, targetRoot);
target.font = this._font;
target._subFont = this._subFont;
}
/**
* @internal
*/
_isContainDirtyFlag(type: number): boolean {
return (this._dirtyUpdateFlag & type) != 0;
}
/**
* @internal
*/
_setDirtyFlagTrue(type: number): void {
this._dirtyUpdateFlag |= type;
}
/**
* @internal
*/
_setDirtyFlagFalse(type: number): void {
this._dirtyUpdateFlag &= ~type;
}
/**
* @internal
*/
_getSubFont(): SubFont {
if (!this._subFont) {
this._resetSubFont();
}
return this._subFont;
}
/**
* @internal
*/
_onRootCanvasModify(flag: RootCanvasModifyFlags): void {
if (flag === RootCanvasModifyFlags.ReferenceResolutionPerUnit) {
this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds);
}
}
protected override _updateBounds(worldBounds: BoundingBox): void {
const transform = <UITransform>this._transformEntity.transform;
const { x: width, y: height } = transform.size;
const { x: pivotX, y: pivotY } = transform.pivot;
worldBounds.min.set(-width * pivotX, -height * pivotY, 0);
worldBounds.max.set(width * (1 - pivotX), height * (1 - pivotY), 0);
BoundingBox.transform(worldBounds, this._transformEntity.transform.worldMatrix, worldBounds);
}
protected override _render(context): void {
if (this._isTextNoVisible()) {
return;
}
if (this._isContainDirtyFlag(DirtyFlag.SubFont)) {
this._resetSubFont();
this._setDirtyFlagFalse(DirtyFlag.SubFont);
}
const canvas = this._getRootCanvas();
if (this._isContainDirtyFlag(DirtyFlag.LocalPositionBounds)) {
this._updateLocalData();
this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds);
}
if (this._isContainDirtyFlag(DirtyFlag.WorldPosition)) {
this._updatePosition();
this._setDirtyFlagFalse(DirtyFlag.WorldPosition);
}
if (this._isContainDirtyFlag(UIRendererUpdateFlags.Color)) {
this._updateColor();
this._setDirtyFlagFalse(UIRendererUpdateFlags.Color);
}
const engine = context.camera.engine;
const textSubRenderElementPool = engine._textSubRenderElementPool;
const material = this.getMaterial();
const renderElement = canvas._renderElement;
const textChunks = this._textChunks;
const isOverlay = canvas._realRenderMode === CanvasRenderMode.ScreenSpaceOverlay;
for (let i = 0, n = textChunks.length; i < n; ++i) {
const { subChunk, texture } = textChunks[i];
const subRenderElement = textSubRenderElementPool.get();
subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, texture, subChunk);
// @ts-ignore
subRenderElement.shaderData ||= new ShaderData(ShaderDataGroup.RenderElement);
subRenderElement.shaderData.setTexture(Text._textTextureProperty, texture);
if (isOverlay) {
subRenderElement.shaderPasses = material.shader.subShaders[0].passes;
subRenderElement.renderQueueFlags = RenderQueueFlags.All;
}
renderElement.addSubRenderElement(subRenderElement);
}
}
private _resetSubFont(): void {
const font = this._font;
// @ts-ignore
this._subFont = font._getSubFont(this.fontSize, this.fontStyle);
this._subFont.nativeFontString = TextUtils.getNativeFontString(font.name, this.fontSize, this.fontStyle);
}
private _updatePosition(): void {
const e = this._transformEntity.transform.worldMatrix.elements;
// prettier-ignore
const e0 = e[0], e1 = e[1], e2 = e[2],
e4 = e[4], e5 = e[5], e6 = e[6],
e12 = e[12], e13 = e[13], e14 = e[14];
const up = UIRenderer._tempVec31.set(e4, e5, e6);
const right = UIRenderer._tempVec30.set(e0, e1, e2);
const worldPositions = Text._worldPositions;
const [worldPosition0, worldPosition1, worldPosition2, worldPosition3] = worldPositions;
const textChunks = this._textChunks;
for (let i = 0, n = textChunks.length; i < n; ++i) {
const { subChunk, charRenderInfos } = textChunks[i];
for (let j = 0, m = charRenderInfos.length; j < m; ++j) {
const charRenderInfo = charRenderInfos[j];
const { localPositions } = charRenderInfo;
const { x: topLeftX, y: topLeftY } = localPositions;
// Top-Left
worldPosition0.set(
topLeftX * e0 + topLeftY * e4 + e12,
topLeftX * e1 + topLeftY * e5 + e13,
topLeftX * e2 + topLeftY * e6 + e14
);
// Right offset
Vector3.scale(right, localPositions.z - topLeftX, worldPosition1);
// Top-Right
Vector3.add(worldPosition0, worldPosition1, worldPosition1);
// Up offset
Vector3.scale(up, localPositions.w - topLeftY, worldPosition2);
// Bottom-Left
Vector3.add(worldPosition0, worldPosition2, worldPosition3);
// Bottom-Right
Vector3.add(worldPosition1, worldPosition2, worldPosition2);
const vertices = subChunk.chunk.vertices;
for (let k = 0, o = subChunk.vertexArea.start + charRenderInfo.indexInChunk * 36; k < 4; ++k, o += 9) {
worldPositions[k].copyToArray(vertices, o);
}
}
}
}
private _updateColor(): void {
const { r, g, b, a } = this._color;
const finalAlpha = a * this._getGlobalAlpha();
const textChunks = this._textChunks;
for (let i = 0, n = textChunks.length; i < n; ++i) {
const subChunk = textChunks[i].subChunk;
const vertexArea = subChunk.vertexArea;
const vertexCount = vertexArea.size / 9;
const vertices = subChunk.chunk.vertices;
for (let j = 0, o = vertexArea.start + 5; j < vertexCount; ++j, o += 9) {
vertices[o] = r;
vertices[o + 1] = g;
vertices[o + 2] = b;
vertices[o + 3] = finalAlpha;
}
}
}
private _updateLocalData(): void {
// @ts-ignore
const pixelsPerResolution = Engine._pixelsPerUnit / this._getRootCanvas().referenceResolutionPerUnit;
const { min, max } = this._localBounds;
const charRenderInfos = Text._charRenderInfos;
const charFont = this._getSubFont();
const { size, pivot } = <UITransform>this._transformEntity.transform;
let rendererWidth = size.x;
let rendererHeight = size.y;
const offsetWidth = rendererWidth * (0.5 - pivot.x);
const offsetHeight = rendererHeight * (0.5 - pivot.y);
const textMetrics = this.enableWrapping
? TextUtils.measureTextWithWrap(
this,
rendererWidth * pixelsPerResolution,
rendererHeight * pixelsPerResolution,
this._lineSpacing * pixelsPerResolution
)
: TextUtils.measureTextWithoutWrap(
this,
rendererHeight * pixelsPerResolution,
this._lineSpacing * pixelsPerResolution
);
const { height, lines, lineWidths, lineHeight, lineMaxSizes } = textMetrics;
// @ts-ignore
const charRenderInfoPool = this.engine._charRenderInfoPool;
const linesLen = lines.length;
let renderElementCount = 0;
if (linesLen > 0) {
const { horizontalAlignment } = this;
const pixelsPerUnitReciprocal = 1.0 / pixelsPerResolution;
rendererWidth *= pixelsPerResolution;
rendererHeight *= pixelsPerResolution;
const halfRendererWidth = rendererWidth * 0.5;
const halfLineHeight = lineHeight * 0.5;
let startY = 0;
const topDiff = lineHeight * 0.5 - lineMaxSizes[0].ascent;
const bottomDiff = lineHeight * 0.5 - lineMaxSizes[linesLen - 1].descent - 1;
switch (this.verticalAlignment) {
case TextVerticalAlignment.Top:
startY = rendererHeight * 0.5 - halfLineHeight + topDiff;
break;
case TextVerticalAlignment.Center:
startY = height * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5;
break;
case TextVerticalAlignment.Bottom:
startY = height - rendererHeight * 0.5 - halfLineHeight - bottomDiff;
break;
}
let firstLine = -1;
let minX = Number.MAX_SAFE_INTEGER;
let minY = Number.MAX_SAFE_INTEGER;
let maxX = Number.MIN_SAFE_INTEGER;
let maxY = Number.MIN_SAFE_INTEGER;
for (let i = 0; i < linesLen; ++i) {
const lineWidth = lineWidths[i];
if (lineWidth > 0) {
const line = lines[i];
let startX = 0;
let firstRow = -1;
if (firstLine < 0) {
firstLine = i;
}
switch (horizontalAlignment) {
case TextHorizontalAlignment.Left:
startX = -halfRendererWidth;
break;
case TextHorizontalAlignment.Center:
startX = -lineWidth * 0.5;
break;
case TextHorizontalAlignment.Right:
startX = halfRendererWidth - lineWidth;
break;
}
for (let j = 0, n = line.length; j < n; ++j) {
const char = line[j];
const charInfo = charFont._getCharInfo(char);
if (charInfo.h > 0) {
firstRow < 0 && (firstRow = j);
const charRenderInfo = (charRenderInfos[renderElementCount++] = charRenderInfoPool.get());
const { localPositions } = charRenderInfo;
charRenderInfo.texture = charFont._getTextureByIndex(charInfo.index);
charRenderInfo.uvs = charInfo.uvs;
const { w, ascent, descent } = charInfo;
const left = (startX + offsetWidth) * pixelsPerUnitReciprocal;
const right = (startX + w + offsetWidth) * pixelsPerUnitReciprocal;
const top = (startY + ascent + offsetHeight) * pixelsPerUnitReciprocal;
const bottom = (startY - descent + offsetHeight) * pixelsPerUnitReciprocal;
localPositions.set(left, top, right, bottom);
i === firstLine && (maxY = Math.max(maxY, top));
minY = Math.min(minY, bottom);
j === firstRow && (minX = Math.min(minX, left));
maxX = Math.max(maxX, right);
}
startX += charInfo.xAdvance + charInfo.offsetX;
}
}
startY -= lineHeight;
}
if (firstLine < 0) {
min.set(0, 0, 0);
max.set(0, 0, 0);
} else {
min.set(minX, minY, 0);
max.set(maxX, maxY, 0);
}
} else {
min.set(0, 0, 0);
max.set(0, 0, 0);
}
charFont._getLastIndex() > 0 &&
charRenderInfos.sort((a, b) => {
return a.texture.instanceId - b.texture.instanceId;
});
this._freeTextChunks();
if (renderElementCount === 0) {
return;
}
const textChunks = this._textChunks;
let curTextChunk = new TextChunk();
textChunks.push(curTextChunk);
const chunkMaxVertexCount = this._getChunkManager().maxVertexCount;
const curCharRenderInfo = charRenderInfos[0];
let curTexture = curCharRenderInfo.texture;
curTextChunk.texture = curTexture;
let curCharInfos = curTextChunk.charRenderInfos;
curCharInfos.push(curCharRenderInfo);
for (let i = 1; i < renderElementCount; ++i) {
const charRenderInfo = charRenderInfos[i];
const texture = charRenderInfo.texture;
if (curTexture !== texture || curCharInfos.length * 4 + 4 > chunkMaxVertexCount) {
this._buildChunk(curTextChunk, curCharInfos.length);
curTextChunk = new TextChunk();
textChunks.push(curTextChunk);
curTexture = texture;
curTextChunk.texture = texture;
curCharInfos = curTextChunk.charRenderInfos;
}
curCharInfos.push(charRenderInfo);
}
const charLength = curCharInfos.length;
if (charLength > 0) {
this._buildChunk(curTextChunk, charLength);
}
charRenderInfos.length = 0;
}
@ignoreClone
protected override _onTransformChanged(type: number): void {
if (type & UITransformModifyFlags.Size || type & UITransformModifyFlags.Pivot) {
this._dirtyUpdateFlag |= DirtyFlag.LocalPositionBounds;
}
super._onTransformChanged(type);
this._setDirtyFlagTrue(DirtyFlag.WorldPosition);
}
private _isTextNoVisible(): boolean {
const size = (<UITransform>this._transformEntity.transform).size;
return (
this._text === "" ||
this._fontSize === 0 ||
(this.enableWrapping && size.x <= 0) ||
(this.overflowMode === OverflowMode.Truncate && size.y <= 0) ||
!this._getRootCanvas()
);
}
private _buildChunk(textChunk: TextChunk, count: number) {
const { r, g, b, a } = this.color;
const finalAlpha = a * this._getGlobalAlpha();
const tempIndices = CharRenderInfo.triangles;
const tempIndicesLength = tempIndices.length;
const subChunk = (textChunk.subChunk = this._getChunkManager().allocateSubChunk(count * 4));
const vertices = subChunk.chunk.vertices;
const indices = (subChunk.indices = []);
const charRenderInfos = textChunk.charRenderInfos;
for (let i = 0, ii = 0, io = 0, vo = subChunk.vertexArea.start + 3; i < count; ++i, io += 4) {
const charRenderInfo = charRenderInfos[i];
charRenderInfo.indexInChunk = i;
// Set indices
for (let j = 0; j < tempIndicesLength; ++j) {
indices[ii++] = tempIndices[j] + io;
}
// Set uv and color for vertices
for (let j = 0; j < 4; ++j, vo += 9) {
const uv = charRenderInfo.uvs[j];
uv.copyToArray(vertices, vo);
vertices[vo + 2] = r;
vertices[vo + 3] = g;
vertices[vo + 4] = b;
vertices[vo + 5] = finalAlpha;
}
}
return subChunk;
}
private _freeTextChunks(): void {
const textChunks = this._textChunks;
// @ts-ignore
const charRenderInfoPool = this.engine._charRenderInfoPool;
const manager = this._getChunkManager();
for (let i = 0, n = textChunks.length; i < n; ++i) {
const textChunk = textChunks[i];
const { charRenderInfos } = textChunk;
for (let j = 0, m = charRenderInfos.length; j < m; ++j) {
charRenderInfoPool.return(charRenderInfos[j]);
}
charRenderInfos.length = 0;
manager.freeSubChunk(textChunk.subChunk);
textChunk.subChunk = null;
textChunk.texture = null;
}
textChunks.length = 0;
}
}
class TextChunk {
charRenderInfos = new Array<CharRenderInfo>();
texture: Texture2D;
subChunk;
}
/**
* @remarks Extends `UIRendererUpdateFlags`.
*/
enum DirtyFlag {
SubFont = 0x4,
LocalPositionBounds = 0x8,
WorldPosition = 0x10,
// LocalPositionBounds | WorldPosition | WorldVolume
Position = 0x19,
Font = SubFont | Position
}

View File

@@ -1,11 +0,0 @@
export { UICanvas } from "./UICanvas";
export { UIGroup } from "./UIGroup";
export { UIRenderer } from "./UIRenderer";
export { UITransform } from "./UITransform";
export { Button } from "./advanced/Button";
export { Image } from "./advanced/Image";
export { Text } from "./advanced/Text";
export { ColorTransition } from "./interactive/transition/ColorTransition";
export { ScaleTransition } from "./interactive/transition/ScaleTransition";
export { SpriteTransition } from "./interactive/transition/SpriteTransition";
export { Transition } from "./interactive/transition/Transition";

View File

@@ -1,302 +0,0 @@
import { Entity, EntityModifyFlags, Script, assignmentClone, ignoreClone } from "@galacean/engine";
import { UIGroup } from "../..";
import { Utils } from "../../Utils";
import { IGroupAble } from "../../interface/IGroupAble";
import { EntityUIModifyFlags, UICanvas } from "../UICanvas";
import { GroupModifyFlags } from "../UIGroup";
import { Transition } from "./transition/Transition";
/**
* Interactive component.
*/
export class UIInteractive extends Script implements IGroupAble {
private static _targetTempPath = new Array<number>();
/** @internal */
@ignoreClone
_rootCanvas: UICanvas;
/** @internal */
@ignoreClone
_indexInRootCanvas: number = -1;
/** @internal */
@ignoreClone
_isRootCanvasDirty: boolean = false;
/** @internal */
@ignoreClone
_rootCanvasListeningEntities: Entity[] = [];
/** @internal */
@ignoreClone
_group: UIGroup;
/** @internal */
@ignoreClone
_indexInGroup: number = -1;
/** @internal */
@ignoreClone
_isGroupDirty: boolean = false;
/** @internal */
@ignoreClone
_groupListeningEntities: Entity[] = [];
/** @internal */
@ignoreClone
_globalInteractive: boolean = false;
/** @internal */
@ignoreClone
_globalInteractiveDirty: boolean = false;
@ignoreClone
protected _transitions: Transition[] = [];
@assignmentClone
protected _interactive: boolean = true;
@ignoreClone
protected _state: InteractiveState = InteractiveState.Normal;
/** @todo Multi-touch points are not considered yet. */
@ignoreClone
private _isPointerInside: boolean = false;
@ignoreClone
private _isPointerDragging: boolean = false;
/**
* The transitions of this interactive.
*/
get transitions(): Readonly<Transition[]> {
return this._transitions;
}
/**
* Whether the interactive is enabled.
*/
get interactive() {
return this._interactive;
}
set interactive(value: boolean) {
if (this._interactive !== value) {
this._interactive = value;
this._globalInteractiveDirty = true;
}
}
/**
* @internal
*/
constructor(entity: Entity) {
super(entity);
this._groupListener = this._groupListener.bind(this);
this._rootCanvasListener = this._rootCanvasListener.bind(this);
}
/**
* Add transition on this interactive.
* @param transition - The transition
*/
addTransition(transition: Transition): void {
const interactive = transition._interactive;
if (interactive !== this) {
interactive?.removeTransition(transition);
this._transitions.push(transition);
transition._interactive = this;
transition._setState(this._state, true);
}
}
/**
* Remove a transition.
* @param shape - The transition.
*/
removeTransition(transition: Transition): void {
const transitions = this._transitions;
const lastOneIndex = transitions.length - 1;
for (let i = lastOneIndex; i >= 0; i--) {
if (transitions[i] === transition) {
i !== lastOneIndex && (transitions[i] = transitions[lastOneIndex]);
transitions.length = lastOneIndex;
transition._interactive = null;
break;
}
}
}
/**
* Remove all transitions.
*/
clearTransitions(): void {
const transitions = this._transitions;
for (let i = 0, n = transitions.length; i < n; i++) {
transitions[i]._interactive = null;
}
transitions.length = 0;
}
override onUpdate(deltaTime: number): void {
if (this._getGlobalInteractive()) {
const transitions = this._transitions;
for (let i = 0, n = transitions.length; i < n; i++) {
transitions[i]._onUpdate(deltaTime);
}
}
}
override onPointerBeginDrag(): void {
this._isPointerDragging = true;
this._updateState(false);
}
override onPointerEndDrag(): void {
this._isPointerDragging = false;
this._updateState(false);
}
override onPointerEnter(): void {
this._isPointerInside = true;
this._updateState(false);
}
override onPointerExit(): void {
this._isPointerInside = false;
this._updateState(false);
}
override onDestroy(): void {
super.onDestroy();
const transitions = this._transitions;
for (let i = transitions.length - 1; i >= 0; i--) {
transitions[i].destroy();
}
}
// @ts-ignore
override _cloneTo(target: UIInteractive, srcRoot: Entity, targetRoot: Entity): void {
const transitions = this._transitions;
for (let i = 0, n = transitions.length; i < n; i++) {
const srcTransition = transitions[i];
const dstTransition = new (transitions[i].constructor as new () => Transition)();
dstTransition.normal = srcTransition.normal;
dstTransition.pressed = srcTransition.pressed;
dstTransition.hover = srcTransition.hover;
dstTransition.disabled = srcTransition.disabled;
const transitionTarget = srcTransition.target;
if (transitionTarget) {
const paths = UIInteractive._targetTempPath;
// @ts-ignore
const success = Entity._getEntityHierarchyPath(srcRoot, transitionTarget.entity, paths);
dstTransition.target = success
? // @ts-ignore
Entity._getEntityByHierarchyPath(targetRoot, paths).getComponent(transitionTarget.constructor)
: transitionTarget;
}
target.addTransition(dstTransition);
}
}
/**
* @internal
*/
_getRootCanvas(): UICanvas {
this._isRootCanvasDirty && Utils.setRootCanvas(this, Utils.searchRootCanvasInParents(this));
return this._rootCanvas;
}
/**
* @internal
*/
_getGroup(): UIGroup {
this._isGroupDirty && Utils.setGroup(this, Utils.searchGroupInParents(this));
return this._group;
}
// @ts-ignore
override _onEnableInScene(): void {
// @ts-ignore
super._onEnableInScene();
Utils.setRootCanvasDirty(this);
Utils.setGroupDirty(this);
this._updateState(true);
}
// @ts-ignore
override _onDisableInScene(): void {
// @ts-ignore
super._onDisableInScene();
Utils.cleanRootCanvas(this);
Utils.cleanGroup(this);
this._isPointerInside = this._isPointerDragging = false;
}
/**
* @internal
*/
@ignoreClone
_groupListener(flag: number): void {
if (flag === EntityModifyFlags.Parent || flag === EntityUIModifyFlags.GroupEnableInScene) {
Utils.setGroupDirty(this);
}
}
/**
* @internal
*/
@ignoreClone
_rootCanvasListener(flag: number): void {
if (flag === EntityModifyFlags.Parent || flag === EntityUIModifyFlags.CanvasEnableInScene) {
Utils.setRootCanvasDirty(this);
Utils.setGroupDirty(this);
}
}
/**
* @internal
*/
@ignoreClone
_onGroupModify(flags: GroupModifyFlags): void {
if (flags & GroupModifyFlags.GlobalInteractive) {
this._globalInteractiveDirty = true;
}
}
/**
* @internal
*/
_getGlobalInteractive(): boolean {
if (this._globalInteractiveDirty) {
const group = this._getGroup();
const globalInteractive = this._interactive && (!group || group._getGlobalInteractive());
if (this._globalInteractive !== globalInteractive) {
this._globalInteractive = globalInteractive;
this._updateState(true);
}
this._globalInteractiveDirty = false;
}
return this._globalInteractive;
}
private _updateState(instant: boolean): void {
const state = this._getInteractiveState();
if (this._state !== state) {
this._state = state;
const transitions = this._transitions;
for (let i = 0, n = transitions.length; i < n; i++) {
transitions[i]._setState(state, instant);
}
}
}
private _getInteractiveState(): InteractiveState {
if (!this._getGlobalInteractive()) {
return InteractiveState.Disable;
}
if (this._isPointerDragging) {
return InteractiveState.Pressed;
} else {
return this._isPointerInside ? InteractiveState.Hover : InteractiveState.Normal;
}
}
}
export enum InteractiveState {
Normal,
Pressed,
Hover,
Disable
}

View File

@@ -1,80 +0,0 @@
import { Color } from "@galacean/engine";
import { UIRenderer } from "../../UIRenderer";
import { InteractiveState } from "../UIInteractive";
import { Transition } from "./Transition";
/**
* Color transition.
*/
export class ColorTransition extends Transition<Color, UIRenderer> {
private _color: Color = new Color();
constructor() {
super();
this._normal = new Color(1, 1, 1, 1);
this._hover = new Color(0.9130986517934192, 0.9130986517934192, 0.9130986517934192, 1);
this._pressed = new Color(0.5775804404296506, 0.5775804404296506, 0.5775804404296506, 1);
this._disabled = new Color(0.5775804404296506, 0.5775804404296506, 0.5775804404296506, 1);
this._duration = 0.1;
this._currentValue = new Color();
this._onNormalValueChanged = this._onNormalValueChanged.bind(this);
this._onHoverValueChanged = this._onHoverValueChanged.bind(this);
this._onPressedValueChanged = this._onPressedValueChanged.bind(this);
this._onDisabledValueChanged = this._onDisabledValueChanged.bind(this);
// @ts-ignore
this._normal._onValueChanged = this._onNormalValueChanged;
// @ts-ignore
this._hover._onValueChanged = this._onHoverValueChanged;
// @ts-ignore
this._pressed._onValueChanged = this._onPressedValueChanged;
// @ts-ignore
this._disabled._onValueChanged = this._onDisabledValueChanged;
}
private _onNormalValueChanged(): void {
if (this._finalState === InteractiveState.Normal) {
this._finalValue = this._normal;
this._updateValue();
}
}
private _onHoverValueChanged(): void {
if (this._finalState === InteractiveState.Hover) {
this._finalValue = this._hover;
this._updateValue();
}
}
private _onPressedValueChanged(): void {
if (this._finalState === InteractiveState.Pressed) {
this._finalValue = this._pressed;
this._updateValue();
}
}
private _onDisabledValueChanged(): void {
if (this._finalState === InteractiveState.Disable) {
this._finalValue = this._disabled;
this._updateValue();
}
}
protected _getTargetValueCopy(): Color {
const color = this._color;
color.copyFrom(this._target?.color || this._normal);
return color;
}
protected override _updateCurrentValue(srcValue: Color, destValue: Color, weight: number): void {
if (weight >= 1) {
this._currentValue.copyFrom(destValue);
} else {
Color.lerp(srcValue, destValue, weight, this._currentValue);
}
}
protected override _applyValue(value: Color): void {
this._target.color = value;
}
}

View File

@@ -1,28 +0,0 @@
import { UIRenderer } from "../../UIRenderer";
import { Transition } from "./Transition";
/**
* Scale transition.
*/
export class ScaleTransition extends Transition<number, UIRenderer> {
constructor() {
super();
this._normal = 1;
this._hover = 1;
this._pressed = 1.2;
this._disabled = 1;
this._duration = 0.1;
}
protected _getTargetValueCopy(): number {
return this._target?.entity.transform.scale.x || this._normal;
}
protected override _updateCurrentValue(srcValue: number, destValue: number, weight: number): void {
this._currentValue = weight >= 1 ? destValue : (destValue - srcValue) * weight + srcValue;
}
protected override _applyValue(value: number): void {
this._target.entity.transform.setScale(value, value, value);
}
}

View File

@@ -1,49 +0,0 @@
import { Sprite } from "@galacean/engine";
import { Image } from "../../advanced/Image";
import { Transition } from "./Transition";
/**
* Sprite transition.
*/
export class SpriteTransition extends Transition<Sprite, Image> {
/**
* @internal
*/
override destroy(): void {
super.destroy();
if (this._normal) {
// @ts-ignore
this._normal._addReferCount(-1);
this._normal = null;
}
if (this._hover) {
// @ts-ignore
this._hover._addReferCount(-1);
this._hover = null;
}
if (this._pressed) {
// @ts-ignore
this._pressed._addReferCount(-1);
this._pressed = null;
}
if (this._disabled) {
// @ts-ignore
this._disabled._addReferCount(-1);
this._disabled = null;
}
this._initialValue = this._currentValue = this._finalValue = null;
this._target = null;
}
protected _getTargetValueCopy(): Sprite {
return this._target?.sprite;
}
protected override _updateCurrentValue(srcValue: Sprite, destValue: Sprite, weight: number): void {
this._currentValue = weight >= 1 ? destValue : srcValue;
}
protected override _applyValue(value: Sprite): void {
this._target.sprite = value;
}
}

View File

@@ -1,187 +0,0 @@
import { Color, ReferResource, Sprite } from "@galacean/engine";
import { UIRenderer } from "../../UIRenderer";
import { InteractiveState, UIInteractive } from "../UIInteractive";
/**
* The transition behavior of UIInteractive.
*/
export abstract class Transition<
T extends TransitionValueType = TransitionValueType,
K extends UIRenderer = UIRenderer
> {
/** @internal */
_interactive: UIInteractive;
protected _target: K;
protected _normal: T;
protected _pressed: T;
protected _hover: T;
protected _disabled: T;
protected _duration: number = 0;
protected _countDown: number = 0;
protected _initialValue: T;
protected _finalValue: T;
protected _currentValue: T;
protected _finalState: InteractiveState = InteractiveState.Normal;
/**
* The normal state of the transition.
*/
get normal(): T {
return this._normal;
}
set normal(value: T) {
const preNormal = this._normal;
if (preNormal !== value) {
this._normal = value;
this._onStateValueDirty(InteractiveState.Normal, preNormal, value);
}
}
/**
* The pressed state of the transition.
*/
get pressed(): T {
return this._pressed;
}
set pressed(value: T) {
const prePressed = this._pressed;
if (prePressed !== value) {
this._pressed = value;
this._onStateValueDirty(InteractiveState.Pressed, prePressed, value);
}
}
/**
* The hover state of the transition.
*/
get hover(): T {
return this._hover;
}
set hover(value: T) {
const preHover = this._hover;
if (preHover !== value) {
this._hover = value;
this._onStateValueDirty(InteractiveState.Hover, preHover, value);
}
}
/**
* The disabled state of the transition.
*/
get disabled(): T {
return this._disabled;
}
set disabled(value: T) {
const preDisabled = this._disabled;
if (preDisabled !== value) {
this._disabled = value;
this._onStateValueDirty(InteractiveState.Disable, preDisabled, value);
}
}
/**
* The target of the transition.
*/
get target(): K {
return this._target;
}
set target(value: K) {
if (this._target !== value) {
this._target = value;
value?.enabled && this._applyValue(this._currentValue);
}
}
/**
* The duration of the transition.
*/
get duration(): number {
return this._duration;
}
set duration(value: number) {
if (value < 0) value = 0;
const preDuration = this._duration;
if (preDuration !== value) {
this._duration = value;
if (this._countDown > 0) {
this._countDown = value * (1 - this._countDown / preDuration);
this._updateValue();
}
}
}
destroy(): void {
this._interactive?.removeTransition(this);
this._target = null;
}
/**
* @internal
*/
_setState(state: InteractiveState, instant: boolean) {
this._finalState = state;
const value = this._getValueByState(state);
if (instant) {
this._countDown = 0;
this._initialValue = this._finalValue = value;
} else {
this._countDown = this._duration;
this._initialValue = this._getTargetValueCopy();
this._finalValue = value;
}
this._updateValue();
}
/**
* @internal
*/
_onUpdate(delta: number): void {
if (this._countDown > 0) {
this._countDown -= delta;
this._updateValue();
}
}
protected abstract _getTargetValueCopy(): T;
protected abstract _updateCurrentValue(srcValue: T, destValue: T, weight: number): void;
protected abstract _applyValue(value: T): void;
protected _onStateValueDirty(state: InteractiveState, preValue: T, curValue: T): void {
// @ts-ignore
preValue instanceof ReferResource && preValue._addReferCount(-1);
// @ts-ignore
curValue instanceof ReferResource && curValue._addReferCount(1);
if (this._finalState === state) {
this._finalValue = curValue;
this._updateValue();
}
}
protected _updateValue() {
const weight = this._duration ? 1 - this._countDown / this._duration : 1;
this._updateCurrentValue(this._initialValue, this._finalValue, weight);
this._target?.enabled && this._applyValue(this._currentValue);
}
private _getValueByState(state: InteractiveState): T {
switch (state) {
case InteractiveState.Normal:
return this.normal;
case InteractiveState.Pressed:
return this.pressed;
case InteractiveState.Hover:
return this.hover;
case InteractiveState.Disable:
return this.disabled;
}
}
}
export type TransitionValueType = number | Sprite | Color;

View File

@@ -1,22 +0,0 @@
/**
* Render mode for ui canvas.
*/
export enum CanvasRenderMode {
/**
* The UI canvas will be rendered directly onto the screen and adapted to screen space,
* overlaying other rendering elements in the same scene.
* @remarks if the `engine.canvas` size change, the UI canvas will automatically adapt.
*/
ScreenSpaceOverlay = 0,
/**
* The UI canvas is placed at a specified distance in front of the camera and adapted to screen space,
* with all objects rendered by the camera.
* @remarks if the camera's properties or the `engine.canvas` size change, the UI canvas will automatically adapt.
* @remarks if set `ScreenSpaceCamera` but no corresponding camera is assigned, the actual rendering mode defaults to `ScreenSpaceOverlay`.
*/
ScreenSpaceCamera = 1,
/**
* The UI canvas is placed in the 3D world space and rendered by every camera in the same scene.
*/
WorldSpace = 2
}

View File

@@ -1,13 +0,0 @@
/** Horizontal alignment mode. */
export enum HorizontalAlignmentMode {
/** No horizontal alignment. */
None = 0,
/** Left-aligned, `alignLeft` drives `position.x`. */
Left = 0x1,
/** Right-aligned, `alignRight` drives `position.x`. */
Right = 0x2,
/** Horizontal stretch, `alignLeft` and `alignRight` drive `position.x` and `size.x`. */
LeftAndRight = 0x3,
/** Center-aligned, `alignCenter` drives `position.x`. */
Center = 0x4
}

View File

@@ -1,16 +0,0 @@
/**
* Resolution adaptation mode.
* @remarks Only effective in screen space.
*/
export enum ResolutionAdaptationMode {
/** Adapt based on width.(`referenceResolution.x`) */
WidthAdaptation,
/** Adapt based on height.(`referenceResolution.y`) */
HeightAdaptation,
/** Adapt based on both width and height.(`referenceResolution`) */
BothAdaptation,
/** Adapt to the side with a larger ratio. */
ExpandAdaptation,
/** Adapt to the side with smaller ratio. */
ShrinkAdaptation
}

View File

@@ -1,13 +0,0 @@
/** Vertical alignment mode. */
export enum VerticalAlignmentMode {
/** No vertical alignment. */
None = 0,
/** Top-aligned, `alignTop` drives `position.y`. */
Top = 0x1,
/** Bottom-aligned, `alignBottom` drives `position.y`. */
Bottom = 0x2,
/** Vertical stretch, `alignTop` and `alignBottom` drive `position.y` and `size.y`. */
TopAndBottom = 0x3,
/** Middle-aligned, `alignMiddle` drives `position.y`. */
Middle = 0x4
}

View File

@@ -1,122 +0,0 @@
import {
BlendFactor,
BlendOperation,
CullMode,
Engine,
Entity,
Font,
IClass,
Loader,
Material,
PipelineStage,
ReflectionParser,
RenderQueueType,
Shader,
ShaderPass
} from "@galacean/engine";
import * as GUIComponent from "./component";
import uiDefaultFs from "./shader/uiDefault.fs.glsl";
import uiDefaultVs from "./shader/uiDefault.vs.glsl";
export * from "./component";
export { CanvasRenderMode } from "./enums/CanvasRenderMode";
export { ResolutionAdaptationMode } from "./enums/ResolutionAdaptationMode";
export { UIPointerEventEmitter } from "./input/UIPointerEventEmitter";
export { HorizontalAlignmentMode } from "./enums/HorizontalAlignmentMode";
export { VerticalAlignmentMode } from "./enums/VerticalAlignmentMode";
export class EngineExtension {
_uiDefaultMaterial: Material;
_getUIDefaultMaterial(): Material {
if (!this._uiDefaultMaterial) {
const shader = _getOrCreateUIShader();
// @ts-ignore
const material = new Material(this, shader);
const renderState = material.renderState;
const target = renderState.blendState.targetBlendState;
target.enabled = true;
target.sourceColorBlendFactor = BlendFactor.SourceAlpha;
target.destinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha;
target.sourceAlphaBlendFactor = BlendFactor.One;
target.destinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha;
target.colorBlendOperation = target.alphaBlendOperation = BlendOperation.Add;
renderState.depthState.writeEnabled = false;
renderState.rasterState.cullMode = CullMode.Off;
renderState.renderQueueType = RenderQueueType.Transparent;
material.isGCIgnored = true;
this._uiDefaultMaterial = material;
}
return this._uiDefaultMaterial;
}
}
export class EntityExtension {
_uiHierarchyVersion = 0;
_updateUIHierarchyVersion(version: number): void {
if (this._uiHierarchyVersion !== version) {
this._uiHierarchyVersion = version;
// @ts-ignore
this.parent?._updateUIHierarchyVersion(version);
}
}
}
declare module "@galacean/engine" {
interface Engine {
// @internal
_uiDefaultMaterial: Material;
// @internal
_getUIDefaultMaterial(): Material;
}
interface Entity {
// @internal
_uiHierarchyVersion: number;
// @internal
_updateUIHierarchyVersion(version: number): void;
}
}
function ApplyMixins(derivedCtor: any, baseCtors: any[]): void {
baseCtors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null)
);
});
});
}
ApplyMixins(Engine, [EngineExtension]);
ApplyMixins(Entity, [EntityExtension]);
ReflectionParser.registerCustomParseComponent("Text", async (instance: any, item: Omit<IClass, "class">) => {
const { props } = item;
if (!props.font) {
// @ts-ignore
instance.font = Font.createFromOS(instance.engine, props.fontFamily || "Arial");
}
return instance;
});
/**
* Register GUI components for the editor.
*/
export function registerGUI() {
for (let key in GUIComponent) {
Loader.registerClass(key, GUIComponent[key]);
}
_getOrCreateUIShader();
}
function _getOrCreateUIShader(): Shader {
let shader = Shader.find("ui");
if (!shader) {
shader = Shader.create("ui", [
new ShaderPass("Forward", uiDefaultVs, uiDefaultFs, {
pipelineStage: PipelineStage.Forward
})
]);
}
return shader;
}

View File

@@ -1,13 +0,0 @@
import { Entity, Vector3 } from "@galacean/engine";
import { UIRenderer } from "../component/UIRenderer";
/**
* @internal
*/
export class UIHitResult {
entity: Entity = null;
distance: number = 0;
point: Vector3 = new Vector3();
normal: Vector3 = new Vector3();
component: UIRenderer = null;
}

View File

@@ -1,266 +0,0 @@
import {
CameraClearFlags,
DisorderedArray,
Entity,
Pointer,
PointerEventData,
PointerEventEmitter,
Scene,
registerPointerEventEmitter
} from "@galacean/engine";
import { UICanvas } from "..";
import { UIRenderer } from "../component/UIRenderer";
import { UIHitResult } from "./UIHitResult";
/**
* @internal
*/
@registerPointerEventEmitter()
export class UIPointerEventEmitter extends PointerEventEmitter {
private static _MAX_PATH_DEPTH = 2048;
private static _tempSet: Set<number> = new Set();
private static _path: Entity[] = [];
private static _tempArray0: Entity[] = [];
private static _tempArray1: Entity[] = [];
private _enteredPath: Entity[] = [];
private _pressedPath: Entity[] = [];
private _draggedPath: Entity[] = [];
_init(): void {
this._hitResult = new UIHitResult();
}
override processRaycast(scenes: readonly Scene[], pointer: Pointer): void {
const { _tempRay: ray } = PointerEventEmitter;
const hitResult = this._hitResult;
const { position } = pointer;
const { x, y } = position;
for (let i = scenes.length - 1; i >= 0; i--) {
const scene = scenes[i];
if (!scene.isActive || scene.destroyed) continue;
// @ts-ignore
const componentsManager = scene._componentsManager;
// Overlay Canvas
let canvasElements: DisorderedArray<UICanvas> = componentsManager._overlayCanvases;
// Screen to world ( Assume that world units have a one-to-one relationship with pixel units )
ray.origin.set(position.x, scene.engine.canvas.height - position.y, 1);
ray.direction.set(0, 0, -1);
for (let j = canvasElements.length - 1; j >= 0; j--) {
if (canvasElements.get(j)._raycast(ray, hitResult)) {
this._updateRaycast((<UIHitResult>hitResult).component, pointer);
return;
}
}
const cameras = componentsManager._activeCameras;
for (let j = cameras.length - 1; j >= 0; j--) {
const camera = cameras.get(j);
if (camera.renderTarget) continue;
const { pixelViewport } = camera;
if (
x < pixelViewport.x ||
y < pixelViewport.y ||
x > pixelViewport.x + pixelViewport.width ||
y > pixelViewport.y + pixelViewport.height
) {
continue;
}
camera.screenPointToRay(pointer.position, ray);
// Other canvases
const isOrthographic = camera.isOrthographic;
const { worldPosition: cameraPosition, worldForward: cameraForward } = camera.entity.transform;
// Sort by rendering order
canvasElements = componentsManager._canvases;
for (let k = 0, n = canvasElements.length; k < n; k++) {
canvasElements.get(k)._updateSortDistance(isOrthographic, cameraPosition, cameraForward);
}
canvasElements.sort((a, b) => a.sortOrder - b.sortOrder || a._sortDistance - b._sortDistance);
for (let k = 0, n = canvasElements.length; k < n; k++) {
canvasElements.get(k)._canvasIndex = k;
}
const farClipPlane = camera.farClipPlane;
// Post-rendering first detection
for (let k = 0, n = canvasElements.length; k < n; k++) {
const canvas = canvasElements.get(k);
if (!canvas._canRender(camera)) continue;
if (canvas._raycast(ray, hitResult, farClipPlane)) {
this._updateRaycast((<UIHitResult>hitResult).component, pointer);
return;
}
}
if (camera.clearFlags & CameraClearFlags.Color) {
this._updateRaycast(null);
return;
}
}
this._updateRaycast(null);
}
}
override processDrag(pointer: Pointer): void {
const draggedPath = this._draggedPath;
if (draggedPath.length > 0) {
this._bubble(draggedPath, pointer, this._fireDrag);
}
}
override processDown(pointer: Pointer): void {
const enteredPath = this._enteredPath;
const pressedPath = this._pressedPath;
const draggedPath = this._draggedPath;
const length = (draggedPath.length = pressedPath.length = enteredPath.length);
if (length > 0) {
for (let i = 0; i < length; i++) {
pressedPath[i] = draggedPath[i] = enteredPath[i];
}
this._bubble(pressedPath, pointer, this._fireDown);
this._bubble(draggedPath, pointer, this._fireBeginDrag);
}
}
override processUp(pointer: Pointer): void {
const enteredPath = this._enteredPath;
const pressedPath = this._pressedPath;
if (enteredPath.length > 0) {
this._bubble(enteredPath, pointer, this._fireUp);
if (pressedPath.length > 0) {
const common = UIPointerEventEmitter._tempArray0;
if (this._findCommonInPath(enteredPath, pressedPath, common)) {
const eventData = this._createEventData(pointer);
for (let i = 0, n = common.length; i < n; i++) {
this._fireClick(common[i], eventData);
}
common.length = 0;
}
}
}
pressedPath.length = 0;
const draggedPath = this._draggedPath;
if (draggedPath.length > 0) {
this._bubble(draggedPath, pointer, this._fireEndDrag);
draggedPath.length = 0;
}
if (enteredPath.length > 0) {
this._bubble(enteredPath, pointer, this._fireDrop);
}
}
override processLeave(pointer: Pointer): void {
const enteredPath = this._enteredPath;
if (enteredPath.length > 0) {
this._bubble(enteredPath, pointer, this._fireExit);
enteredPath.length = 0;
}
const draggedPath = this._draggedPath;
if (draggedPath.length > 0) {
this._bubble(draggedPath, pointer, this._fireEndDrag);
draggedPath.length = 0;
}
this._pressedPath.length = 0;
}
override dispose(): void {
this._enteredPath.length = this._pressedPath.length = this._draggedPath.length = 0;
}
private _updateRaycast(element: UIRenderer, pointer: Pointer = null): void {
const enteredPath = this._enteredPath;
const curPath = this._composedPath(element, UIPointerEventEmitter._path);
const add = UIPointerEventEmitter._tempArray0;
const del = UIPointerEventEmitter._tempArray1;
if (this._findDiffInPath(enteredPath, curPath, add, del)) {
const eventData = this._createEventData(pointer);
for (let i = 0, n = add.length; i < n; i++) {
this._fireEnter(add[i], eventData);
}
for (let i = 0, n = del.length; i < n; i++) {
this._fireExit(del[i], eventData);
}
const length = (enteredPath.length = curPath.length);
for (let i = 0; i < length; i++) {
enteredPath[i] = curPath[i];
}
add.length = del.length = 0;
}
curPath.length = 0;
}
private _composedPath(element: UIRenderer, path: Entity[]): Entity[] {
if (!element) {
path.length = 0;
return path;
}
let entity = (path[0] = element.entity);
let i = 1;
const rootEntity = element._getRootCanvas().entity;
for (; i < UIPointerEventEmitter._MAX_PATH_DEPTH && !!entity && entity !== rootEntity; i++) {
entity = path[i] = entity.parent;
}
path.length = i;
return path;
}
private _findCommonInPath(prePath: Entity[], curPath: Entity[], common: Entity[]): boolean {
const idSet = UIPointerEventEmitter._tempSet;
idSet.clear();
for (let i = 0, n = prePath.length; i < n; i++) {
idSet.add(prePath[i].instanceId);
}
let hasCommon = false;
for (let i = 0, n = curPath.length; i < n; i++) {
const entity = curPath[i];
if (idSet.has(entity.instanceId)) {
common.push(entity);
hasCommon = true;
}
}
return hasCommon;
}
private _findDiffInPath(prePath: Entity[], curPath: Entity[], add: Entity[], del: Entity[]): boolean {
const idSet = UIPointerEventEmitter._tempSet;
idSet.clear();
let changed = false;
for (let i = 0, n = prePath.length; i < n; i++) {
idSet.add(prePath[i].instanceId);
}
for (let i = 0, n = curPath.length; i < n; i++) {
const entity = curPath[i];
if (!idSet.has(entity.instanceId)) {
add.push(entity);
changed = true;
}
}
idSet.clear();
for (let i = 0, n = curPath.length; i < n; i++) {
idSet.add(curPath[i].instanceId);
}
for (let i = 0, n = prePath.length; i < n; i++) {
const entity = prePath[i];
if (!idSet.has(entity.instanceId)) {
del.push(entity);
changed = true;
}
}
return changed;
}
private _bubble(path: Entity[], pointer: Pointer, fireEvent: FireEvent): void {
const length = path.length;
if (length <= 0) return;
const eventData = this._createEventData(pointer);
for (let i = 0; i < length; i++) {
fireEvent(path[i], eventData);
}
}
}
type FireEvent = (entity: Entity, eventData: PointerEventData) => void;

View File

@@ -1,15 +0,0 @@
import { Entity } from "@galacean/engine";
import { UICanvas } from "..";
import { RootCanvasModifyFlags } from "../component/UICanvas";
export interface IElement {
entity: Entity;
_rootCanvas: UICanvas;
_indexInRootCanvas: number;
_rootCanvasListeningEntities: Entity[];
_isRootCanvasDirty: boolean;
_getRootCanvas(): UICanvas;
_rootCanvasListener: (flag: number, param?: any) => void;
_onRootCanvasModify?(flag: RootCanvasModifyFlags): void;
}

View File

@@ -1,10 +0,0 @@
import { Ray, Vector4 } from "@galacean/engine";
import { UIHitResult } from "../input/UIHitResult";
import { IGroupAble } from "./IGroupAble";
export interface IGraphics extends IGroupAble {
raycastEnabled: boolean;
raycastPadding: Vector4;
_raycast(ray: Ray, out: UIHitResult, distance: number): boolean;
}

View File

@@ -1,17 +0,0 @@
import { Entity } from "@galacean/engine";
import { GroupModifyFlags, UIGroup } from "../component/UIGroup";
import { IElement } from "./IElement";
export interface IGroupAble extends IElement {
_group: UIGroup;
_indexInGroup: number;
_groupListeningEntities: Entity[];
_isGroupDirty: boolean;
_globalAlpha?: number;
_globalInteractive?: boolean;
_getGroup(): UIGroup;
_onGroupModify(flag: GroupModifyFlags, isPass?: boolean): void;
_groupListener(flag: number): void;
}

View File

@@ -1,9 +0,0 @@
declare module "*.glsl" {
const value: string;
export default value;
}
declare module "*.gs" {
const value: string;
export default value;
}

View File

@@ -1,14 +0,0 @@
#include <common>
uniform sampler2D renderer_UITexture;
varying vec2 v_uv;
varying vec4 v_color;
void main() {
vec4 baseColor = texture2DSRGB(renderer_UITexture, v_uv);
vec4 finalColor = baseColor * v_color;
#ifdef ENGINE_SHOULD_SRGB_CORRECT
finalColor = outputSRGBCorrection(finalColor);
#endif
gl_FragColor = finalColor;
}

View File

@@ -1,15 +0,0 @@
uniform mat4 renderer_MVPMat;
attribute vec3 POSITION;
attribute vec2 TEXCOORD_0;
attribute vec4 COLOR_0;
varying vec2 v_uv;
varying vec4 v_color;
void main() {
gl_Position = renderer_MVPMat * vec4(POSITION, 1.0);
v_uv = TEXCOORD_0;
v_color = COLOR_0;
}

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"declaration": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"declarationDir": "types",
"emitDeclarationOnly": true,
"noImplicitOverride": true,
"sourceMap": true,
"incremental": false,
"skipLibCheck": true,
"stripInternal": true
},
"include": ["src/**/*"]
}

View File

@@ -1,42 +0,0 @@
## Installation
`@galacean/engine-xr-webxr` is one of the implementation backends of xr. If your XR application interface is WebXR standard, please introduce this package.
To install, use:
```sh
npm install @galacean/engine-xr-webxr
```
This will allow you to import engine entirely using:
```javascript
import { WebXRDevice } from "@galacean/engine-xr-webxr";
import { XRHitTest, XRSessionMode } from "@galacean/engine-xr";
```
## Usage
```typescript
import { WebXRDevice } from "@galacean/engine-xr-webxr";
import { XRHitTest, XRSessionMode } from "@galacean/engine-xr";
// Create engine by passing in the HTMLCanvasElement
WebGLEngine.create({
canvas: "canvas",
xrDevice: new WebXRDevice(),
}).then((engine) => {
// Users need to actively click the button to enter XR
XRButton.onClick = function () {
this.engine.xrManager.enterXR(XRSessionMode.AR).then(
() => {
console.log("Enter AR");
},
(error) => {
console.log("Not supported AR", error);
}
);
};
});
......
```

View File

@@ -1,39 +0,0 @@
{
"name": "@galacean/engine-xr-webxr",
"version": "0.0.0-experimental-backup.4",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"repository": {
"url": "https://github.com/galacean/engine"
},
"license": "MIT",
"main": "dist/main.js",
"module": "dist/module.js",
"debug": "src/index.ts",
"browser": "dist/browser.js",
"types": "types/index.d.ts",
"scripts": {
"b:types": "tsc"
},
"umd": {
"name": "Galacean.WebXR",
"globals": {
"@galacean/engine": "Galacean"
}
},
"files": [
"dist/**/*",
"libs/**/*",
"types/**/*"
],
"dependencies": {
"@galacean/engine-xr": "workspace:*"
},
"devDependencies": {
"@galacean/engine-design": "workspace:*",
"@galacean/engine-math": "workspace:*"
},
"peerDependencies": {}
}

View File

@@ -1,53 +0,0 @@
import { XRTrackedInputDevice } from "@galacean/engine-xr";
export function parseXRMode(mode: number): XRSessionMode | null {
switch (mode) {
case 1:
return "immersive-ar";
case 2:
return "immersive-vr";
default:
return null;
}
}
export function getInputSource(inputSource: XRInputSource): XRTrackedInputDevice {
let type: number;
switch (inputSource.targetRayMode) {
case "gaze":
break;
case "screen":
return XRTrackedInputDevice.Controller;
case "tracked-pointer":
if (inputSource.hand) {
switch (inputSource.handedness) {
case "left":
return XRTrackedInputDevice.LeftHand;
case "right":
return XRTrackedInputDevice.RightHand;
}
} else {
switch (inputSource.handedness) {
case "left":
return XRTrackedInputDevice.LeftController;
case "right":
return XRTrackedInputDevice.RightController;
}
}
break;
default:
break;
}
return type;
}
export function viewToCamera(type: XREye): XRTrackedInputDevice {
switch (type) {
case "left":
return XRTrackedInputDevice.LeftCamera;
case "right":
return XRTrackedInputDevice.RightCamera;
default:
return XRTrackedInputDevice.Camera;
}
}

View File

@@ -1,97 +0,0 @@
import { IHardwareRenderer, IXRDevice } from "@galacean/engine-design";
import { XRFeatureType, XRSessionMode } from "@galacean/engine-xr";
import { parseXRMode } from "./Util";
import { WebXRSession } from "./WebXRSession";
import { WebXRFeature } from "./feature/WebXRFeature";
export class WebXRDevice implements IXRDevice {
/** @internal */
static _platformFeatureMap: PlatformFeatureConstructor[] = [];
isSupportedSessionMode(mode: XRSessionMode): Promise<void> {
return new Promise((resolve, reject: (reason: Error) => void) => {
if (!window.isSecureContext) {
reject(new Error("WebXR is available only in secure contexts (HTTPS)."));
return;
}
if (!navigator.xr) {
reject(new Error("WebXR isn't available"));
return;
}
navigator.xr.isSessionSupported(parseXRMode(mode)).then((isSupported: boolean) => {
isSupported ? resolve() : reject(new Error("The current context doesn't support WebXR."));
});
});
}
isSupportedFeature(type: XRFeatureType): boolean {
switch (type) {
case XRFeatureType.HitTest:
case XRFeatureType.PlaneTracking:
return typeof XRPlane !== "undefined";
case XRFeatureType.AnchorTracking:
return typeof XRAnchor !== "undefined";
case XRFeatureType.ImageTracking:
// @ts-ignore
return typeof XRImageTrackingResult !== "undefined";
}
}
createPlatformFeature(type: XRFeatureType, ...args: any[]): WebXRFeature {
const platformFeatureConstructor = WebXRDevice._platformFeatureMap[type];
return platformFeatureConstructor ? new platformFeatureConstructor(...args) : null;
}
requestSession(rhi: IHardwareRenderer, mode: XRSessionMode, platformFeatures: WebXRFeature[]): Promise<WebXRSession> {
return new Promise((resolve, reject) => {
const sessionMode = parseXRMode(mode);
const options: XRSessionInit = { requiredFeatures: ["local"], optionalFeatures: [] };
const promiseArr = [];
for (let i = 0, n = platformFeatures.length; i < n; i++) {
const promise = platformFeatures[i]._assembleOptions(options);
promise && promiseArr.push(promise);
}
Promise.all(promiseArr).then(() => {
navigator.xr.requestSession(sessionMode, options).then((session) => {
const { gl } = rhi;
const attributes = gl.getContextAttributes();
if (!attributes) {
reject(Error("GetContextAttributes Error!"));
}
gl.makeXRCompatible().then(() => {
const scaleFactor = XRWebGLLayer.getNativeFramebufferScaleFactor(session);
let layer: XRWebGLLayer;
if (session.renderState.layers === undefined || !!!rhi.isWebGL2) {
const layerInit = {
antialias: session.renderState.layers === undefined ? attributes.antialias : true,
alpha: true,
depth: attributes.depth,
stencil: attributes.stencil,
framebufferScaleFactor: scaleFactor
};
layer = new XRWebGLLayer(session, gl, layerInit);
session.updateRenderState({
baseLayer: layer
});
} else {
layer = new XRWebGLLayer(session, gl);
session.updateRenderState({
layers: [layer]
});
}
session.requestReferenceSpace("local").then((referenceSpace: XRReferenceSpace) => {
resolve(new WebXRSession(session, layer, referenceSpace));
}, reject);
}, reject);
}, reject);
}, reject);
});
}
}
type PlatformFeatureConstructor = new (...args: any[]) => WebXRFeature;
export function registerXRPlatformFeature(type: XRFeatureType) {
return (platformFeatureConstructor: PlatformFeatureConstructor) => {
WebXRDevice._platformFeatureMap[type] = platformFeatureConstructor;
};
}

View File

@@ -1,122 +0,0 @@
import { IXRCamera, IXRController, IXRFrame, IXRInput } from "@galacean/engine-design";
import { Vector3 } from "@galacean/engine-math";
import { XRTrackedInputDevice, XRTrackingState } from "@galacean/engine-xr";
import { getInputSource, viewToCamera } from "./Util";
import { WebXRSession } from "./WebXRSession";
export class WebXRFrame implements IXRFrame {
/** @internal */
_platformFrame: XRFrame;
private _session: WebXRSession;
updateInputs(inputs: IXRInput[]): void {
if (!this._platformFrame) return;
this._updateController(inputs);
this._updateCamera(inputs);
}
private _updateController(inputs: IXRInput[]) {
const { _platformFrame: frame } = this;
const { _platformSession: session, _platformReferenceSpace: referenceSpace } = this._session;
const { inputSources } = session;
for (let i = 0, n = inputSources.length; i < n; i++) {
const inputSource = inputSources[i];
const type = getInputSource(inputSource);
const input = <IXRController>inputs[type];
switch (inputSource.targetRayMode) {
case "screen":
case "tracked-pointer":
const { gripSpace, targetRaySpace } = inputSource;
if (gripSpace) {
const { transform, emulatedPosition } = frame.getPose(gripSpace, referenceSpace);
if (transform) {
const { gripPose } = input;
gripPose.matrix.copyFromArray(transform.matrix);
gripPose.position.copyFrom(transform.position);
gripPose.rotation.copyFrom(transform.orientation);
}
input.trackingState = emulatedPosition ? XRTrackingState.TrackingLost : XRTrackingState.Tracking;
}
if (targetRaySpace) {
const { transform, emulatedPosition } = frame.getPose(targetRaySpace, referenceSpace);
if (transform) {
const { targetRayPose } = input;
targetRayPose.matrix.copyFromArray(transform.matrix);
targetRayPose.position.copyFrom(transform.position);
targetRayPose.rotation.copyFrom(transform.orientation);
input.trackingState = emulatedPosition ? XRTrackingState.TrackingLost : XRTrackingState.Tracking;
}
}
break;
case "gaze":
break;
default:
break;
}
}
}
private _updateCamera(inputs: IXRInput[]) {
const { _platformFrame: frame } = this;
const {
_platformReferenceSpace: referenceSpace,
_platformLayer: layer,
framebufferWidth,
framebufferHeight
} = this._session;
const viewerPose = frame.getViewerPose(referenceSpace);
if (viewerPose) {
let hadUpdateCenterViewer = false;
const { views, emulatedPosition } = viewerPose;
for (let i = 0, n = views.length; i < n; i++) {
const view = views[i];
const type = viewToCamera(view.eye);
const { transform } = views[i];
if (type === XRTrackedInputDevice.Camera) {
hadUpdateCenterViewer ||= true;
}
const xrCamera = <IXRCamera>inputs[type];
const { pose } = xrCamera;
pose.matrix.copyFromArray(transform.matrix);
pose.position.copyFrom(transform.position);
pose.rotation.copyFrom(transform.orientation);
xrCamera.projectionMatrix.copyFromArray(view.projectionMatrix);
xrCamera.trackingState = emulatedPosition ? XRTrackingState.TrackingLost : XRTrackingState.Tracking;
const xrViewport = layer.getViewport(view);
const width = xrViewport.width / framebufferWidth;
const height = xrViewport.height / framebufferHeight;
const x = xrViewport.x / framebufferWidth;
const y = 1 - xrViewport.y / framebufferHeight - height;
xrCamera.viewport.set(x, y, width, height);
}
if (!hadUpdateCenterViewer) {
const leftCameraDevice = <IXRCamera>inputs[XRTrackedInputDevice.LeftCamera];
const rightCameraDevice = <IXRCamera>inputs[XRTrackedInputDevice.RightCamera];
const cameraDevice = <IXRCamera>inputs[XRTrackedInputDevice.Camera];
const { pose: leftCameraPose } = leftCameraDevice;
const { pose: rightCameraPose } = rightCameraDevice;
const { pose: cameraPose } = cameraDevice;
cameraPose.rotation.copyFrom(leftCameraPose.rotation);
const { position, matrix } = cameraPose;
Vector3.add(leftCameraPose.position, rightCameraPose.position, position);
position.scale(0.5);
matrix.copyFrom(leftCameraPose.matrix);
const { elements } = matrix;
elements[12] = position.x;
elements[13] = position.y;
elements[14] = position.z;
cameraDevice.projectionMatrix.copyFrom(leftCameraDevice.projectionMatrix);
cameraDevice.trackingState = emulatedPosition ? XRTrackingState.TrackingLost : XRTrackingState.Tracking;
cameraDevice.viewport =
leftCameraDevice.viewport.width && leftCameraDevice.viewport.height
? leftCameraDevice.viewport
: rightCameraDevice.viewport;
}
}
}
constructor(session: WebXRSession) {
this._session = session;
}
}

View File

@@ -1,208 +0,0 @@
import { IXRInputEvent, IXRSession } from "@galacean/engine-design";
import { XRInputEventType, XRTargetRayMode, XRTrackedInputDevice } from "@galacean/engine-xr";
import { getInputSource } from "./Util";
import { WebXRFrame } from "./WebXRFrame";
export class WebXRSession implements IXRSession {
requestAnimationFrame: (callback: FrameRequestCallback) => number;
cancelAnimationFrame: (id: number) => void;
/** @internal */
_onSessionExitCallBack: () => void;
/** @internal */
_platformSession: XRSession;
/** @internal */
_platformLayer: XRWebGLLayer;
/** @internal */
_platformReferenceSpace: XRReferenceSpace;
private _frame: WebXRFrame;
private _events: IXRInputEvent[] = [];
private _screenPointers: XRInputSource[] = [];
private _inputEventTypeMap: Record<string, number> = {
selectstart: XRInputEventType.SelectStart,
select: XRInputEventType.Select,
selectend: XRInputEventType.SelectEnd,
squeezestart: XRInputEventType.SqueezeStart,
squeeze: XRInputEventType.Squeeze,
squeezeend: XRInputEventType.SqueezeEnd
};
private _targetRayModeMap: Record<string, number> = {
gaze: XRTargetRayMode.Gaze,
"tracked-pointer": XRTargetRayMode.TrackedPointer,
screen: XRTargetRayMode.Screen
};
get frame(): WebXRFrame {
return this._frame;
}
get framebuffer(): WebGLFramebuffer {
return this._platformLayer.framebuffer;
}
get framebufferWidth(): number {
return this._platformLayer.framebufferWidth;
}
get framebufferHeight(): number {
return this._platformLayer.framebufferHeight;
}
get frameRate(): number {
return this._platformSession.frameRate;
}
get supportedFrameRates(): Float32Array {
return this._platformSession.supportedFrameRates;
}
get events(): IXRInputEvent[] {
const { _events: events } = this;
// Select event does not dispatch the move event, so we need to simulate dispatching the move here.
const { _screenPointers: screenPointers } = this;
for (let i = 0; i < screenPointers.length; i++) {
const inputSource = screenPointers[i];
if (!inputSource) continue;
const { axes } = inputSource.gamepad;
const event = {
type: XRInputEventType.Select,
targetRayMode: XRTargetRayMode.Screen,
input: XRTrackedInputDevice.Controller,
id: i,
x: axes[0],
y: axes[1]
};
events.push(event);
}
return events;
}
constructor(session: XRSession, layer: XRWebGLLayer, referenceSpace: XRReferenceSpace) {
this._frame = new WebXRFrame(this);
this._platformSession = session;
this._platformLayer = layer;
this._platformReferenceSpace = referenceSpace;
const xrRequestAnimationFrame = session.requestAnimationFrame.bind(session);
const onFrame = function (time: number, frame: XRFrame, callback: FrameRequestCallback) {
this._frame._platformFrame = frame;
callback(time);
}.bind(this);
this.requestAnimationFrame = (callback: FrameRequestCallback) => {
return xrRequestAnimationFrame((time: number, frame: XRFrame) => {
onFrame(time, frame, callback);
});
};
this.cancelAnimationFrame = session.cancelAnimationFrame.bind(session);
this._onSessionEvent = this._onSessionEvent.bind(this);
this._onSessionExit = this._onSessionExit.bind(this);
}
getFixedFoveation(): number {
return this._platformLayer.fixedFoveation;
}
setFixedFoveation(value: number) {
this._platformLayer.fixedFoveation = value;
}
start(): void {}
stop(): void {
this._frame._platformFrame = null;
}
end(): Promise<void> {
this._frame._platformFrame = null;
return this._platformSession.end();
}
setSessionExitCallBack(onSessionExitCallBack: () => void): void {
this._onSessionExitCallBack = onSessionExitCallBack;
}
addEventListener(): void {
const { _onSessionEvent: onSessionEvent, _platformSession: session } = this;
session.addEventListener("select", onSessionEvent);
session.addEventListener("selectstart", onSessionEvent);
session.addEventListener("selectend", onSessionEvent);
session.addEventListener("squeeze", onSessionEvent);
session.addEventListener("squeezestart", onSessionEvent);
session.addEventListener("squeezeend", onSessionEvent);
session.addEventListener("end", this._onSessionExit);
}
removeEventListener(): void {
const { _onSessionEvent: onSessionEvent, _platformSession: session } = this;
session.removeEventListener("select", onSessionEvent);
session.removeEventListener("selectstart", onSessionEvent);
session.removeEventListener("selectend", onSessionEvent);
session.removeEventListener("squeeze", onSessionEvent);
session.removeEventListener("squeezestart", onSessionEvent);
session.removeEventListener("squeezeend", onSessionEvent);
session.removeEventListener("end", this._onSessionExit);
this._events.length = 0;
}
resetEvents(): void {
this._events.length = 0;
}
private _onSessionExit(): void {
if (this._onSessionExitCallBack) {
this._onSessionExitCallBack();
this._onSessionExitCallBack = null;
}
}
private _onSessionEvent(inputSourceEvent: XRInputSourceEvent): void {
const { inputSource } = inputSourceEvent;
const event: IXRInputEvent = {
type: this._inputEventTypeMap[inputSourceEvent.type],
input: getInputSource(inputSource),
targetRayMode: this._targetRayModeMap[inputSource.targetRayMode]
};
if (event.targetRayMode === XRTargetRayMode.Screen) {
const { _screenPointers: screenPointers } = this;
const { axes } = inputSource.gamepad;
event.x = axes[0];
event.y = axes[1];
switch (event.type) {
case XRInputEventType.SelectStart:
let idx = -1;
let emptyIdx = -1;
for (let i = screenPointers.length - 1; i >= 0; i--) {
const pointer = screenPointers[i];
if (pointer === inputSource) {
idx = i;
break;
}
if (!pointer) {
emptyIdx = i;
}
}
if (idx === -1) {
if (emptyIdx === -1) {
idx = screenPointers.push(inputSource) - 1;
} else {
idx = emptyIdx;
screenPointers[emptyIdx] = inputSource;
}
}
event.id = idx;
break;
case XRInputEventType.SelectEnd:
for (let i = screenPointers.length - 1; i >= 0; i--) {
if (screenPointers[i] === inputSource) {
screenPointers[i] = null;
event.id = i;
}
}
break;
default:
break;
}
}
this._events.push(event);
}
}

View File

@@ -1,122 +0,0 @@
import { IXRRequestAnchor, IXRTracked } from "@galacean/engine-design";
import { XRFeatureType, XRRequestTrackingState, XRTrackingState } from "@galacean/engine-xr";
import { registerXRPlatformFeature } from "../WebXRDevice";
import { WebXRFrame } from "../WebXRFrame";
import { WebXRSession } from "../WebXRSession";
import { WebXRTrackableFeature } from "./WebXRTrackableFeature";
/**
* WebXR implementation of XRPlatformAnchorTracking.
*/
@registerXRPlatformFeature(XRFeatureType.AnchorTracking)
export class WebXRAnchorTracking extends WebXRTrackableFeature<IXRTracked, IWebXRRequestTrackingAnchor> {
checkAvailable(session: WebXRSession, frame: WebXRFrame, requestTrackings: IWebXRRequestTrackingAnchor[]): boolean {
if (!frame._platformFrame) return false;
for (let i = 0, n = requestTrackings.length; i < n; i++) {
const requestTracking = requestTrackings[i];
if (requestTracking.state === XRRequestTrackingState.None) {
this._addAnchor(session, frame, requestTracking);
}
}
return true;
}
getTrackedResult(session: WebXRSession, frame: WebXRFrame, requestTrackings: IWebXRRequestTrackingAnchor[]): void {
const { _platformReferenceSpace: platformReferenceSpace } = session;
const { _platformFrame: platformFrame } = frame;
const { trackedAnchors } = platformFrame;
for (let i = 0, n = requestTrackings.length; i < n; i++) {
const requestTracking = requestTrackings[i];
if (requestTracking.state !== XRRequestTrackingState.Resolved) continue;
const tracked = requestTracking.tracked[0];
if (trackedAnchors.has(requestTracking.xrAnchor)) {
const emulated = this._updateTrackedAnchor(platformFrame, platformReferenceSpace, requestTracking);
if (emulated) {
if (tracked.state === XRTrackingState.Tracking) {
tracked.state = XRTrackingState.TrackingLost;
}
} else {
tracked.state = XRTrackingState.Tracking;
}
} else {
tracked.state = XRTrackingState.NotTracking;
}
}
}
override get canModifyRequestTrackingAfterInit(): boolean {
return true;
}
override onDelRequestTracking(requestTracking: IWebXRRequestTrackingAnchor): void {
switch (requestTracking.state) {
case XRRequestTrackingState.Submitted:
requestTracking.state = XRRequestTrackingState.WaitingDestroy;
break;
case XRRequestTrackingState.Resolved:
requestTracking.xrAnchor.delete();
requestTracking.state = XRRequestTrackingState.Destroyed;
break;
default:
requestTracking.state = XRRequestTrackingState.Destroyed;
break;
}
}
/**
* @internal
*/
_assembleOptions(options: XRSessionInit): void {
options.optionalFeatures.push("anchors");
}
private _addAnchor(session: WebXRSession, frame: WebXRFrame, requestTracking: IWebXRRequestTrackingAnchor): void {
if (!session || !frame) {
return;
}
requestTracking.state = XRRequestTrackingState.Submitted;
const { position, rotation } = requestTracking;
const { _platformFrame: platformFrame } = frame;
const { _platformReferenceSpace: platformReferenceSpace } = session;
platformFrame
.createAnchor(
new XRRigidTransform(
{ x: position.x, y: position.y, z: position.z },
{ x: rotation.x, y: rotation.y, z: rotation.z, w: rotation.w }
),
platformReferenceSpace
)
.then(
(xrAnchor) => {
if (requestTracking.state === XRRequestTrackingState.WaitingDestroy) {
xrAnchor.delete();
requestTracking.state = XRRequestTrackingState.Destroyed;
} else {
requestTracking.xrAnchor = xrAnchor;
requestTracking.state = XRRequestTrackingState.Resolved;
}
},
() => {
if (requestTracking.state === XRRequestTrackingState.WaitingDestroy) {
requestTracking.state = XRRequestTrackingState.Destroyed;
} else {
requestTracking.state = XRRequestTrackingState.Rejected;
}
}
);
}
private _updateTrackedAnchor(frame: XRFrame, space: XRSpace, requestTracking: IWebXRRequestTrackingAnchor): boolean {
const { xrAnchor } = requestTracking;
const xrPose = frame.getPose(xrAnchor.anchorSpace, space);
const { transform } = xrPose;
const { pose } = requestTracking.tracked[0];
pose.matrix.copyFromArray(transform.matrix);
pose.rotation.copyFrom(transform.orientation);
pose.position.copyFrom(transform.position);
return xrPose.emulatedPosition;
}
}
interface IWebXRRequestTrackingAnchor extends IXRRequestAnchor {
xrAnchor: XRAnchor;
}

View File

@@ -1,9 +0,0 @@
import { IXRPlatformFeature } from "@galacean/engine-design";
/**
* @internal
*/
export abstract class WebXRFeature implements IXRPlatformFeature {
/** @internal */
abstract _assembleOptions(options: XRSessionInit): Promise<void> | void;
}

View File

@@ -1,153 +0,0 @@
import { IXRReferenceImage, IXRRequestImage, IXRTrackedImage } from "@galacean/engine-design";
import { XRFeatureType, XRRequestTrackingState, XRTrackingState } from "@galacean/engine-xr";
import { registerXRPlatformFeature } from "../WebXRDevice";
import { WebXRFrame } from "../WebXRFrame";
import { WebXRSession } from "../WebXRSession";
import { WebXRTrackableFeature } from "./WebXRTrackableFeature";
/**
* WebXR implementation of XRPlatformImageTracking.
* Note: each tracked image can appear at most once in the tracking results.
* If multiple copies of the same image exist in the users environment,
* the device can choose an arbitrary instance to report a pose,
* and this choice can change for future XRFrames.
*/
@registerXRPlatformFeature(XRFeatureType.ImageTracking)
export class WebXRImageTracking extends WebXRTrackableFeature<IXRTrackedImage, IXRRequestImage> {
private _images: IXRReferenceImage[];
private _trackingScoreStatus: ImageTrackingScoreStatus = ImageTrackingScoreStatus.NotReceived;
private _tempIdx: number = 0;
private _tempArr: number[] = [];
constructor(images: IXRReferenceImage[]) {
super();
this._images = images;
}
checkAvailable(session: WebXRSession, frame: WebXRFrame, requestTrackings: IXRRequestImage[]): boolean {
if (!frame._platformFrame) return false;
switch (this._trackingScoreStatus) {
case ImageTrackingScoreStatus.NotReceived:
this._requestTrackingScore(session, requestTrackings);
return false;
case ImageTrackingScoreStatus.Waiting:
return false;
}
return true;
}
getTrackedResult(
session: WebXRSession,
frame: WebXRFrame,
requestTrackings: IXRRequestImage[],
generateTracked: () => IXRTrackedImage
): void {
const { _platformReferenceSpace: platformReferenceSpace } = session;
const { _platformFrame: platformFrame } = frame;
const { _tempArr: tempArr } = this;
const idx = ++this._tempIdx;
// @ts-ignore
const trackingResults = platformFrame.getImageTrackingResults();
for (let i = 0, n = trackingResults.length; i < n; i++) {
const trackingResult = trackingResults[i];
const { index } = trackingResult;
const requestTrackingImage = requestTrackings[index];
if (requestTrackingImage) {
let tracked = requestTrackingImage.tracked[0];
if (!tracked) {
tracked = requestTrackingImage.tracked[0] = generateTracked();
tracked.referenceImage = requestTrackingImage.image;
}
if (trackingResult.trackingState === "tracked") {
this._updateTrackedImage(platformFrame, platformReferenceSpace, tracked, trackingResult);
tracked.state = XRTrackingState.Tracking;
} else {
tracked.state = XRTrackingState.TrackingLost;
}
tempArr[index] = idx;
} else {
console.warn("Images can not find " + index);
}
}
for (let i = 0, n = requestTrackings.length; i < n; i++) {
if (tempArr[i] < idx) requestTrackings[i].tracked[0].state = XRTrackingState.NotTracking;
}
}
override onAddRequestTracking(requestTracking: IXRRequestImage): void {
requestTracking.state = XRRequestTrackingState.Submitted;
}
/**
* @internal
*/
_assembleOptions(options: XRSessionInit): Promise<void> | void {
options.optionalFeatures.push("image-tracking");
const { _images: images } = this;
const promiseArr: Promise<ImageBitmap>[] = [];
if (images) {
for (let i = 0, n = images.length; i < n; i++) {
const referenceImage = images[i];
const { imageSource } = images[i];
if (!imageSource) {
return Promise.reject(new Error("referenceImage[" + referenceImage.name + "].src is null"));
} else {
promiseArr.push(createImageBitmap(imageSource));
}
}
return new Promise((resolve, reject) => {
// @ts-ignore
const trackedImages = (options.trackedImages = []);
Promise.all(promiseArr).then((bitmaps: ImageBitmap[]) => {
for (let i = 0, n = bitmaps.length; i < n; i++) {
trackedImages.push({
image: bitmaps[i],
widthInMeters: images[i].physicalWidth
});
}
resolve();
}, reject);
});
} else {
return Promise.reject(new Error("Images.length is 0"));
}
}
private _requestTrackingScore(session: WebXRSession, requestTrackings: IXRRequestImage[]): void {
this._trackingScoreStatus = ImageTrackingScoreStatus.Waiting;
session._platformSession
// @ts-ignore
.getTrackedImageScores()
.then((trackingScores: ("untrackable" | "trackable")[]) => {
if (trackingScores) {
for (let i = 0, n = trackingScores.length; i < n; i++) {
const trackingScore = trackingScores[i];
const requestTracking = requestTrackings[i];
if (trackingScore === "trackable") {
this._trackingScoreStatus = ImageTrackingScoreStatus.Received;
requestTracking.state = XRRequestTrackingState.Resolved;
} else {
requestTracking.state = XRRequestTrackingState.Rejected;
console.warn(requestTracking.image.name, " unTrackable");
}
}
}
});
}
private _updateTrackedImage(frame: XRFrame, space: XRSpace, trackedImage: IXRTrackedImage, trackingResult: any) {
const { pose } = trackedImage;
const { transform } = frame.getPose(trackingResult.imageSpace, space);
pose.matrix.copyFromArray(transform.matrix);
pose.rotation.copyFrom(transform.orientation);
pose.position.copyFrom(transform.position);
trackedImage.measuredPhysicalWidth = trackingResult.measuredWidthInMeters;
}
}
enum ImageTrackingScoreStatus {
NotReceived,
Waiting,
Received
}

View File

@@ -1,99 +0,0 @@
import { IXRRequestPlane, IXRTrackedPlane } from "@galacean/engine-design";
import { Vector3 } from "@galacean/engine-math";
import { XRFeatureType, XRPlaneMode, XRRequestTrackingState, XRTrackingState } from "@galacean/engine-xr";
import { registerXRPlatformFeature } from "../WebXRDevice";
import { WebXRFrame } from "../WebXRFrame";
import { WebXRSession } from "../WebXRSession";
import { WebXRTrackableFeature } from "./WebXRTrackableFeature";
/**
* WebXR implementation of XRPlatformPlaneTracking.
*/
@registerXRPlatformFeature(XRFeatureType.PlaneTracking)
export class WebXRPlaneTracking extends WebXRTrackableFeature<IWebXRTrackedPlane, IXRRequestPlane> {
private _lastDetectedPlanes: XRPlaneSet;
constructor(detectedMode: number) {
super();
if (detectedMode !== XRPlaneMode.EveryThing) {
console.warn("WebXR only support XRPlaneMode.EveryThing");
}
}
checkAvailable(session: WebXRSession, frame: WebXRFrame, requestTrackings: IXRRequestPlane[]): boolean {
return !!frame._platformFrame;
}
getTrackedResult(
session: WebXRSession,
frame: WebXRFrame,
requestTrackings: IXRRequestPlane[],
generateTracked: () => IWebXRTrackedPlane
): void {
const { _platformReferenceSpace: platformReferenceSpace } = session;
const { _platformFrame: platformFrame } = frame;
// @ts-ignore
const detectedPlanes: XRPlaneSet = platformFrame.detectedPlanes || platformFrame.worldInformation?.detectedPlanes;
const tracked = <IWebXRTrackedPlane[]>requestTrackings[0].tracked;
for (let i = 0, n = tracked.length; i < n; i++) {
const trackedPlane = tracked[i];
if (detectedPlanes.has(trackedPlane.xrPlane)) {
trackedPlane.state = XRTrackingState.Tracking;
this._updatePlane(platformFrame, platformReferenceSpace, trackedPlane);
} else {
trackedPlane.state = XRTrackingState.NotTracking;
}
}
const { _lastDetectedPlanes: lastDetectedPlanes } = this;
detectedPlanes.forEach((xrPlane) => {
if (!lastDetectedPlanes?.has(xrPlane)) {
const plane = generateTracked();
plane.xrPlane = xrPlane;
plane.lastChangedTime = -1;
this._updatePlane(platformFrame, platformReferenceSpace, plane);
tracked.push(plane);
}
});
this._lastDetectedPlanes = detectedPlanes;
}
override onAddRequestTracking(requestTracking: IXRRequestPlane): void {
requestTracking.state = XRRequestTrackingState.Resolved;
}
/**
* @internal
*/
_assembleOptions(options: XRSessionInit): void {
options.optionalFeatures.push("plane-detection");
}
private _updatePlane(frame: XRFrame, space: XRSpace, trackedPlane: IWebXRTrackedPlane): void {
const { pose, polygon, xrPlane } = trackedPlane;
const planePose = frame.getPose(xrPlane.planeSpace, space);
if (!planePose) return;
const { transform, emulatedPosition } = planePose;
trackedPlane.state = emulatedPosition ? XRTrackingState.TrackingLost : XRTrackingState.Tracking;
trackedPlane.planeMode = xrPlane.orientation === "horizontal" ? XRPlaneMode.Horizontal : XRPlaneMode.Vertical;
if (trackedPlane.lastChangedTime < xrPlane.lastChangedTime) {
trackedPlane.lastChangedTime = xrPlane.lastChangedTime;
trackedPlane.attributesDirty = true;
const { polygon: oriPolygon } = xrPlane;
for (let i = 0, n = (polygon.length = oriPolygon.length); i < n; i++) {
(polygon[i] ||= new Vector3()).copyFrom(oriPolygon[i]);
}
} else {
trackedPlane.attributesDirty = false;
}
pose.rotation.copyFrom(transform.orientation);
pose.position.copyFrom(transform.position);
pose.matrix.copyFromArray(transform.matrix);
pose.inverseMatrix.copyFromArray(transform.inverse.matrix);
}
}
interface IWebXRTrackedPlane extends IXRTrackedPlane {
xrPlane?: XRPlane;
lastChangedTime?: number;
}

View File

@@ -1,32 +0,0 @@
import { IXRRequestTracking, IXRTrackablePlatformFeature, IXRTracked } from "@galacean/engine-design";
import { XRRequestTrackingState } from "@galacean/engine-xr";
import { WebXRFrame } from "../WebXRFrame";
import { WebXRSession } from "../WebXRSession";
import { WebXRFeature } from "./WebXRFeature";
/**
* @internal
*/
export abstract class WebXRTrackableFeature<T extends IXRTracked, K extends IXRRequestTracking<T>>
extends WebXRFeature
implements IXRTrackablePlatformFeature<T, K>
{
get canModifyRequestTrackingAfterInit(): boolean {
return false;
}
abstract getTrackedResult(
session: WebXRSession,
frame: WebXRFrame,
requestTrackings: K[],
generateTracked: () => T
): void;
abstract checkAvailable(session: WebXRSession, frame: WebXRFrame, requestTrackings: K[]): boolean;
onAddRequestTracking(requestTracking: K): void {}
onDelRequestTracking(requestTracking: K): void {
requestTracking.state = XRRequestTrackingState.Destroyed;
}
}

View File

@@ -1,5 +0,0 @@
import "./feature/WebXRAnchorTracking";
import "./feature/WebXRImageTracking";
import "./feature/WebXRPlaneTracking";
export { WebXRDevice } from "./WebXRDevice";

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"declaration": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"declarationDir": "types",
"emitDeclarationOnly": true,
"noImplicitOverride": true,
"sourceMap": true,
"incremental": false,
"skipLibCheck": true,
"stripInternal": true
},
"include": ["src/**/*"]
}

View File

@@ -1,42 +0,0 @@
## Installation
`@galacean/engine-xr` is the implementation layer of XRManager and is a necessary package for developing XR applications.
To install, use:
```sh
npm install @galacean/engine-xr
```
This will allow you to import engine entirely using:
```javascript
import { WebXRDevice } from "@galacean/engine-xr-webxr";
import { XRHitTest, XRSessionMode } from "@galacean/engine-xr";
```
## Usage
```typescript
import { WebXRDevice } from "@galacean/engine-xr-webxr";
import { XRHitTest, XRSessionMode } from "@galacean/engine-xr";
// Create engine by passing in the HTMLCanvasElement
WebGLEngine.create({
canvas: "canvas",
xrDevice: new WebXRDevice(),
}).then((engine) => {
// Users need to actively click the button to enter XR
XRButton.onClick = function () {
this.engine.xrManager.enterXR(XRSessionMode.AR).then(
() => {
console.log("Enter AR");
},
(error) => {
console.log("Not supported AR", error);
}
);
};
});
......
```

View File

@@ -1,38 +0,0 @@
{
"name": "@galacean/engine-xr",
"version": "0.0.0-experimental-backup.4",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"repository": {
"url": "https://github.com/galacean/engine.git"
},
"license": "MIT",
"main": "dist/main.js",
"module": "dist/module.js",
"debug": "src/index.ts",
"browser": "dist/browser.js",
"types": "types/index.d.ts",
"scripts": {
"b:types": "tsc"
},
"umd": {
"name": "Galacean.XR",
"globals": {
"@galacean/engine": "Galacean"
}
},
"files": [
"dist/**/*",
"libs/**/*",
"types/**/*"
],
"devDependencies": {
"@galacean/engine-design": "workspace:*",
"@galacean/engine": "workspace:*"
},
"peerDependencies": {
"@galacean/engine": "workspace:*"
}
}

View File

@@ -1,320 +0,0 @@
import { CameraClearFlags, CameraType, Engine, Entity, XRManager } from "@galacean/engine";
import { IXRDevice } from "@galacean/engine-design";
import { XRFeature } from "./feature/XRFeature";
import { XRFeatureType } from "./feature/XRFeatureType";
import { XRCameraManager } from "./feature/camera/XRCameraManager";
import { XRInputManager } from "./input/XRInputManager";
import { XRSessionManager } from "./session/XRSessionManager";
import { XRSessionMode } from "./session/XRSessionMode";
import { XRSessionState } from "./session/XRSessionState";
/**
* @internal
*/
export class XRManagerExtended extends XRManager {
/** @internal */
static _featureMap: Map<TFeatureConstructor<XRFeature>, XRFeatureType> = new Map();
override inputManager: XRInputManager;
override sessionManager: XRSessionManager;
override cameraManager: XRCameraManager;
/** @internal */
_platformDevice: IXRDevice;
private _features: XRFeature[];
private _origin: Entity;
override get features(): XRFeature[] {
return this._features;
}
override get origin(): Entity {
return this._origin;
}
override set origin(value: Entity) {
if (this.sessionManager._platformSession) {
throw new Error("Cannot set origin when the session is initialized.");
}
this._origin = value;
}
override isSupportedFeature<T extends XRFeature>(feature: TFeatureConstructor<T>): boolean {
return this._platformDevice.isSupportedFeature(XRManagerExtended._featureMap.get(feature));
}
override addFeature<T extends new (xrManager: XRManagerExtended, ...args: any[]) => XRFeature>(
type: T,
...args: TFeatureConstructorArguments<T>
): InstanceType<T> | null {
if (this.sessionManager._platformSession) {
throw new Error("Cannot add feature when the session is initialized.");
}
if (!this._platformDevice.isSupportedFeature(XRManagerExtended._featureMap.get(type))) {
throw new Error("The feature is not supported");
}
const { features } = this;
for (let i = 0, n = features.length; i < n; i++) {
if (features[i] instanceof type) throw new Error("The feature has been added");
}
const feature = new type(this, ...args) as InstanceType<T>;
features.push(feature);
return feature;
}
override getFeature<T extends XRFeature>(type: TFeatureConstructor<T>): T | null {
const { features } = this;
for (let i = 0, n = features.length; i < n; i++) {
const feature = features[i];
if (feature instanceof type) {
return feature;
}
}
}
override enterXR(sessionMode: XRSessionMode, autoRun: boolean = true): Promise<void> {
const { sessionManager } = this;
if (sessionManager._platformSession) {
throw new Error("Please exit XR immersive mode first.");
}
if (!this._origin) {
throw new Error("Please set origin before enter XR.");
}
return new Promise((resolve, reject) => {
// 1. Check if this xr mode is supported
sessionManager.isSupportedMode(sessionMode).then(() => {
sessionManager._setState(XRSessionState.Initializing);
// 2. Initialize session
sessionManager._initialize(sessionMode, this.features).then(() => {
autoRun && sessionManager.run();
resolve();
}, reject);
}, reject);
});
}
override exitXR(): Promise<void> {
return new Promise((resolve, reject) => {
this.sessionManager._exit().then(() => {
resolve();
}, reject);
});
}
override _initialize(engine: Engine, xrDevice: IXRDevice): void {
this._features = [];
this._platformDevice = xrDevice;
this.sessionManager = new XRSessionManager(this, engine);
this.inputManager = new XRInputManager(this, engine);
this.cameraManager = new XRCameraManager(this);
}
override _update(): void {
const { sessionManager } = this;
if (sessionManager.state !== XRSessionState.Running) return;
sessionManager._onUpdate();
this.inputManager._onUpdate();
this.cameraManager._onUpdate();
const { features } = this;
for (let i = 0, n = features.length; i < n; i++) {
const feature = features[i];
feature.enabled && feature._onUpdate();
}
}
override _destroy(): void {
if (this.sessionManager._platformSession) {
this.exitXR().then(() => {
this.sessionManager._onDestroy();
this.inputManager._onDestroy();
this.cameraManager._onDestroy();
});
} else {
this.sessionManager._onDestroy();
this.inputManager._onDestroy();
this.cameraManager._onDestroy();
}
}
override _getRequestAnimationFrame(): (callback: FrameRequestCallback) => number {
return this.sessionManager._getRequestAnimationFrame();
}
override _getCancelAnimationFrame(): (id: number) => void {
return this.sessionManager._getCancelAnimationFrame();
}
override _getCameraIgnoreClearFlags(type: CameraType): CameraClearFlags {
return this.cameraManager._getIgnoreClearFlags(type);
}
/**
* @internal
*/
_onSessionStop(): void {
const { features } = this;
for (let i = 0, n = features.length; i < n; i++) {
const feature = features[i];
feature.enabled && feature._onSessionStop();
}
}
/**
* @internal
*/
_onSessionInit(): void {
const { features } = this;
for (let i = 0, n = features.length; i < n; i++) {
const feature = features[i];
feature.enabled && feature._onSessionInit();
}
}
/**
* @internal
*/
_onSessionStart(): void {
this.cameraManager._onSessionStart();
const { features } = this;
for (let i = 0, n = features.length; i < n; i++) {
const feature = features[i];
feature.enabled && feature._onSessionStart();
}
}
/**
* @internal
*/
_onSessionExit(): void {
this.cameraManager._onSessionExit();
const { features } = this;
for (let i = 0, n = features.length; i < n; i++) {
const feature = features[i];
feature.enabled && feature._onSessionExit();
feature._onDestroy();
}
features.length = 0;
}
}
/**
* @internal
*/
export function registerXRFeature<T extends XRFeature>(type: XRFeatureType): (feature: TFeatureConstructor<T>) => void {
return (feature: TFeatureConstructor<T>) => {
XRManagerExtended._featureMap.set(feature, type);
};
}
export interface IXRListener {
fn: (...args: any[]) => any;
destroyed?: boolean;
}
type TFeatureConstructor<T extends XRFeature> = new (xrManager: XRManagerExtended, ...args: any[]) => T;
type TFeatureConstructorArguments<T extends new (xrManager: XRManagerExtended, ...args: any[]) => XRFeature> =
T extends new (xrManager: XRManagerExtended, ...args: infer P) => XRFeature ? P : never;
declare module "@galacean/engine" {
interface XRManager {
/** Input manager for XR. */
inputManager: XRInputManager;
/** Session manager for XR. */
sessionManager: XRSessionManager;
/** Camera manager for XR. */
cameraManager: XRCameraManager;
/** Initialized features. */
get features(): XRFeature[];
/**
* The current origin of XR space.
* @remarks The connection point between the virtual world and the real world ( XR Space )
*/
get origin(): Entity;
set origin(value: Entity);
/**
* Check if the specified feature is supported.
* @param type - The type of the feature
* @returns If the feature is supported
*/
isSupportedFeature<T extends XRFeature>(feature: TFeatureConstructor<T>): boolean;
/**
* Add feature based on the xr feature type.
* @param type - The type of the feature
* @param args - The constructor params of the feature
* @returns The feature which has been added
*/
addFeature<T extends new (xrManager: XRManagerExtended, ...args: any[]) => XRFeature>(
type: T,
...args: TFeatureConstructorArguments<T>
): InstanceType<T> | null;
/**
* Get feature which match the type.
* @param type - The type of the feature
* @returns The feature which match type
*/
getFeature<T extends XRFeature>(type: TFeatureConstructor<T>): T | null;
/**
* Enter XR immersive mode, when you call this method, it will initialize and display the XR virtual world.
* @param sessionMode - The mode of the session
* @param autoRun - Whether to automatically run the session, when `autoRun` is set to true, xr will start working immediately after initialization. Otherwise, you need to call `sessionManager.run` later to work.
* @returns A promise that resolves if the XR virtual world is entered, otherwise rejects
*/
enterXR(sessionMode: XRSessionMode, autoRun?: boolean): Promise<void>;
/**
* Exit XR immersive mode, when you call this method, it will destroy the XR virtual world.
* @returns A promise that resolves if the XR virtual world is destroyed, otherwise rejects
*/
exitXR(): Promise<void>;
/**
* @internal
*/
_initialize(engine: Engine, xrDevice: IXRDevice): void;
/**
* @internal
*/
_getRequestAnimationFrame(): (callback: FrameRequestCallback) => number;
/**
* @internal
*/
_getCancelAnimationFrame(): (id: number) => void;
/**
* @internal
*/
_getCameraIgnoreClearFlags(type: CameraType): CameraClearFlags;
/**
* @internal
*/
_update(): void;
/**
* @internal
*/
_destroy(): void;
}
}
// 实现混入的函数
function ApplyMixins(derivedCtor: any, baseCtors: any[]): void {
baseCtors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null)
);
});
});
}
ApplyMixins(XRManager, [XRManagerExtended]);

View File

@@ -1,16 +0,0 @@
import { Matrix, Quaternion, Vector3 } from "@galacean/engine";
import { IXRPose } from "@galacean/engine-design";
/**
* Data for describing pose in the XR space.
*/
export class XRPose implements IXRPose {
/** The position of the pose in XR space. */
position: Vector3 = new Vector3();
/** The rotation of the pose in XR space. */
rotation: Quaternion = new Quaternion();
/** The matrix of the pose in XR space. */
matrix: Matrix = new Matrix();
/** The inverse matrix of the pose in XR space. */
inverseMatrix: Matrix = new Matrix();
}

View File

@@ -1,78 +0,0 @@
import { IXRPlatformFeature } from "@galacean/engine-design";
import { XRManagerExtended } from "../XRManagerExtended";
import { XRFeatureType } from "./XRFeatureType";
/**
* The base class of XR feature manager.
*/
export abstract class XRFeature<T extends IXRPlatformFeature = IXRPlatformFeature> {
/** @internal */
_platformFeature: T;
protected _enabled: boolean = true;
/**
* Returns whether the feature is enabled.
*/
get enabled(): boolean {
return this._enabled;
}
set enabled(value: boolean) {
if (this.enabled !== value) {
this._enabled = value;
value ? this._onEnable() : this._onDisable();
}
}
/**
* @internal
*/
constructor(
protected _xrManager: XRManagerExtended,
protected _type: XRFeatureType,
...args: any[]
) {
this._platformFeature = <T>_xrManager._platformDevice.createPlatformFeature(_type, ...args);
this._onEnable();
}
/**
* @internal
*/
_onEnable(): void {}
/**
* @internal
*/
_onDisable(): void {}
/**
* @internal
*/
_onUpdate(): void {}
/**
* @internal
*/
_onSessionInit(): void {}
/**
* @internal
*/
_onSessionStart(): void {}
/**
* @internal
*/
_onSessionStop(): void {}
/**
* @internal
*/
_onSessionExit(): void {}
/**
* @internal
*/
_onDestroy(): void {}
}

View File

@@ -1,6 +0,0 @@
export enum XRFeatureType {
AnchorTracking,
ImageTracking,
PlaneTracking,
HitTest
}

View File

@@ -1,156 +0,0 @@
import { Camera, CameraClearFlags, CameraType, Matrix } from "@galacean/engine";
import { XRManagerExtended } from "../../XRManagerExtended";
import { XRCamera } from "../../input/XRCamera";
import { XRTrackedInputDevice } from "../../input/XRTrackedInputDevice";
import { XRSessionState } from "../../session/XRSessionState";
/**
* The manager of XR camera.
*/
export class XRCameraManager {
/**
* The fixed foveation of the camera.
*/
get fixedFoveation(): number {
const { _platformSession: platformSession } = this._xrManager.sessionManager;
if (platformSession) {
return platformSession.getFixedFoveation();
} else {
throw new Error("XR session is not available.");
}
}
set fixedFoveation(value: number) {
const { _platformSession: platformSession } = this._xrManager.sessionManager;
if (platformSession) {
platformSession.setFixedFoveation(value);
} else {
throw new Error("XR session is not available.");
}
}
/**
* @internal
*/
constructor(private _xrManager: XRManagerExtended) {}
/**
* Attach the camera to the specified input type(Camera, LeftCamera or RightCamera).
* The camera entity need to be moved to the XROrigin entity.
* @param type - The input type
* @param camera - The camera to be attached
*/
attachCamera(
type: XRTrackedInputDevice.Camera | XRTrackedInputDevice.LeftCamera | XRTrackedInputDevice.RightCamera,
camera: Camera
): void {
const xrCamera = this._xrManager.inputManager.getTrackedDevice<XRCamera>(type);
const preCamera = xrCamera._camera;
if (preCamera !== camera) {
// @ts-ignore
preCamera && (preCamera._cameraType = CameraType.Normal);
switch (type) {
case XRTrackedInputDevice.Camera:
// @ts-ignore
camera._cameraType = CameraType.XRCenterCamera;
break;
case XRTrackedInputDevice.LeftCamera:
// @ts-ignore
camera._cameraType = CameraType.XRLeftCamera;
break;
case XRTrackedInputDevice.RightCamera:
// @ts-ignore
camera._cameraType = CameraType.XRRightCamera;
break;
default:
break;
}
xrCamera._camera = camera;
}
}
/**
* Detach the camera from the specified input type.
* @param type - The input type
* @returns The camera that was detached
*/
detachCamera(
type: XRTrackedInputDevice.Camera | XRTrackedInputDevice.LeftCamera | XRTrackedInputDevice.RightCamera
): Camera {
const xrCamera = this._xrManager.inputManager.getTrackedDevice<XRCamera>(type);
const preCamera = xrCamera._camera;
// @ts-ignore
preCamera && (preCamera._cameraType = CameraType.Normal);
xrCamera._camera = null;
return preCamera;
}
/**
* @internal
*/
_onSessionStart(): void {}
/**
* @internal
*/
_onUpdate(): void {
const cameras = this._xrManager.inputManager._cameras;
const platformSession = this._xrManager.sessionManager._platformSession;
for (let i = 0, n = cameras.length; i < n; i++) {
const cameraDevice = cameras[i];
const { _camera: camera } = cameraDevice;
if (!camera) continue;
// sync position and rotation
const { transform } = camera.entity;
const { pose } = cameraDevice;
transform.position = pose.position;
transform.rotationQuaternion = pose.rotation;
// sync viewport
const { viewport } = camera;
const { x, y, width, height } = cameraDevice.viewport;
if (!(x === viewport.x && y === viewport.y && width === viewport.z && height === viewport.w)) {
camera.viewport = viewport.set(x, y, width, height);
}
// sync project matrix
if (!Matrix.equals(camera.projectionMatrix, cameraDevice.projectionMatrix)) {
camera.projectionMatrix = cameraDevice.projectionMatrix;
}
// sync pixel viewport (only when rendering to XR main framebuffer)
if (!camera.renderTarget) {
// @ts-ignore
camera._adjustPixelViewport(platformSession.framebufferWidth, platformSession.framebufferHeight);
}
}
}
/**
* @internal
*/
_onSessionExit(): void {
const cameras = this._xrManager.inputManager._cameras;
for (let i = 0, n = cameras.length; i < n; i++) {
// @ts-ignore
cameras[i]._camera?._updatePixelViewport();
}
}
/**
* @internal
*/
_getIgnoreClearFlags(cameraType: CameraType): CameraClearFlags {
if (cameraType === CameraType.XRCenterCamera) {
if (this._xrManager.sessionManager.state === XRSessionState.Running) {
return CameraClearFlags.Color;
} else {
return CameraClearFlags.None;
}
} else {
return CameraClearFlags.None;
}
}
/**
* @internal
*/
_onDestroy(): void {}
}

View File

@@ -1,10 +0,0 @@
/**
* Enum for the types of hit test that can be performed.
* Note: currently only supports plane.
*/
export enum TrackableType {
/** Tracked plane. */
Plane = 0x1,
/** All tracked objects. */
All = 0x1
}

View File

@@ -1,20 +0,0 @@
import { IXRTracked } from "@galacean/engine-design";
import { TrackableType } from "./TrackableType";
import { Vector3 } from "@galacean/engine";
/**
* XR hit result.
* It is the detection result returned by using XR HitTest feature.
*/
export class XRHitResult {
/** The position of the hit point. */
point: Vector3 = new Vector3();
/** The normal of the hit point. */
normal: Vector3 = new Vector3();
/** The distance from the origin of the ray to the hit point. */
distance: number;
/** The hit tracked object, such as IXRTrackedPlane. */
trackedObject: IXRTracked;
/** The type of tracked object detected, such as TrackableType.Plane */
trackableType: TrackableType;
}

View File

@@ -1,126 +0,0 @@
import { Plane, Ray, Vector2, Vector3 } from "@galacean/engine";
import { IXRTrackedPlane } from "@galacean/engine-design";
import { XRManagerExtended, registerXRFeature } from "../../XRManagerExtended";
import { XRCamera } from "../../input/XRCamera";
import { XRTrackedInputDevice } from "../../input/XRTrackedInputDevice";
import { XRSessionMode } from "../../session/XRSessionMode";
import { XRFeature } from "../XRFeature";
import { XRFeatureType } from "../XRFeatureType";
import { XRPlaneTracking } from "../trackable/plane/XRPlaneTracking";
import { TrackableType } from "./TrackableType";
import { XRHitResult } from "./XRHitResult";
/**
* The manager of XR hit test.
*/
@registerXRFeature(XRFeatureType.HitTest)
export class XRHitTest extends XRFeature {
private _tempRay: Ray = new Ray();
private _tempPlane: Plane = new Plane();
private _tempVec2: Vector2 = new Vector2();
private _tempVec30: Vector3 = new Vector3();
private _tempVec31: Vector3 = new Vector3();
private _tempVec32: Vector3 = new Vector3();
private _tempVec33: Vector3 = new Vector3();
private _tempVec34: Vector3 = new Vector3();
private _tempVec35: Vector3 = new Vector3();
/**
* @param xrManager - The xr manager
*/
constructor(xrManager: XRManagerExtended) {
super(xrManager, XRFeatureType.HitTest);
}
/**
* Hit test.
* @param ray - The ray to test
* @param type - The type of hit test
* @returns The hit result
*/
hitTest(ray: Ray, type: TrackableType): XRHitResult[] {
const result = [];
if (type & TrackableType.Plane) {
this._hitTestPlane(ray, result);
}
return result;
}
/**
* Screen hit test.
* @param x - The x coordinate of the screen point
* @param y - The y coordinate of the screen point
* @param type - The type of hit test
* @returns The hit result
*/
screenHitTest(x: number, y: number, type: TrackableType): XRHitResult[] {
const { _xrManager: xrManager } = this;
if (xrManager.sessionManager.mode !== XRSessionMode.AR) {
throw new Error("Only AR mode supports using screen ray hit test.");
}
const { _camera: camera } = xrManager.inputManager.getTrackedDevice<XRCamera>(XRTrackedInputDevice.Camera);
if (!camera) {
throw new Error("No camera available.");
}
const ray = camera.screenPointToRay(this._tempVec2.set(x, y), this._tempRay);
return this.hitTest(ray, type);
}
private _hitTestPlane(ray: Ray, result: XRHitResult[]): void {
const planeManager = this._xrManager.getFeature(XRPlaneTracking);
if (!planeManager || !planeManager.enabled) {
throw new Error("The plane estimation function needs to be turned on for plane hit test.");
}
const { _tempPlane: plane, _tempVec30: normal, _tempVec31: hitPoint, _tempVec32: hitPointInPlane } = this;
const { trackedPlanes } = planeManager;
for (let i = 0, n = trackedPlanes.length; i < n; i++) {
const trackedPlane = trackedPlanes[i];
normal.set(0, 1, 0).transformNormal(trackedPlane.pose.matrix);
plane.normal.copyFrom(normal);
plane.distance = -Vector3.dot(normal, trackedPlane.pose.position);
const distance = ray.intersectPlane(plane);
if (distance >= 0) {
ray.getPoint(distance, hitPoint);
Vector3.transformToVec3(hitPoint, trackedPlane.pose.inverseMatrix, hitPointInPlane);
// Check if the hit position is within the plane boundary.
if (this._checkPointerWithinPlane(hitPointInPlane, trackedPlane)) {
const hitResult = new XRHitResult();
hitResult.point.copyFrom(hitPoint);
hitResult.normal.copyFrom(normal);
hitResult.distance = distance;
hitResult.trackedObject = trackedPlane;
hitResult.trackableType = TrackableType.Plane;
result.push(hitResult);
}
}
}
}
private _checkPointerWithinPlane(pointer: Vector3, plane: IXRTrackedPlane): boolean {
const { _tempVec33: preToCur, _tempVec34: preToPointer, _tempVec35: cross } = this;
const { polygon } = plane;
const length = polygon.length;
let prePoint = polygon[length - 1];
let side = 0;
for (let i = 0; i < length; i++) {
const curPoint = polygon[i];
Vector3.subtract(curPoint, prePoint, preToCur);
Vector3.subtract(pointer, prePoint, preToPointer);
Vector3.cross(preToCur, preToPointer, cross);
const y = cross.y;
if (side === 0) {
if (y > 0) {
side = 1;
} else if (y < 0) {
side = -1;
}
} else {
if ((y > 0 && side < 0) || (y < 0 && side > 0)) {
return false;
}
}
prePoint = curPoint;
}
return true;
}
}

View File

@@ -1,12 +0,0 @@
import { XRRequestTrackingState } from "./XRRequestTrackingState";
import { XRTracked } from "./XRTracked";
/**
* @internal
*/
export abstract class XRRequestTracking<T extends XRTracked> {
/** The status of the current request tracking. */
state: XRRequestTrackingState = XRRequestTrackingState.None;
/** Tracked instances, make up from the tracking data returned by Session. */
tracked: T[] = [];
}

View File

@@ -1,8 +0,0 @@
export enum XRRequestTrackingState {
None,
Submitted,
Resolved,
Rejected,
Destroyed,
WaitingDestroy
}

View File

@@ -1,156 +0,0 @@
import { SafeLoopArray } from "@galacean/engine";
import { IXRTrackablePlatformFeature } from "@galacean/engine-design";
import { IXRListener } from "../../XRManagerExtended";
import { XRTrackingState } from "../../input/XRTrackingState";
import { XRFeature } from "../XRFeature";
import { XRFeatureType } from "../XRFeatureType";
import { XRRequestTracking } from "./XRRequestTracking";
import { XRRequestTrackingState } from "./XRRequestTrackingState";
import { XRTracked } from "./XRTracked";
/**
* The base class of XR trackable manager.
*/
export abstract class XRTrackableFeature<
T extends XRTracked = XRTracked,
K extends XRRequestTracking<T> = XRRequestTracking<T>
> extends XRFeature<IXRTrackablePlatformFeature<T, K>> {
protected static _uuid = 0;
protected _requestTrackings: K[] = [];
protected _tracked: T[] = [];
protected _added: T[] = [];
protected _updated: T[] = [];
protected _removed: T[] = [];
protected _statusSnapshot: Record<number, XRTrackingState> = {};
private _listeners: SafeLoopArray<IXRListener> = new SafeLoopArray<IXRListener>();
/**
* Add a listening function for tracked object changes.
* @param listener - The listening function
*/
addChangedListener(listener: (added: readonly T[], updated: readonly T[], removed: readonly T[]) => void): void {
this._listeners.push({ fn: listener });
}
/**
* Remove a listening function of tracked object changes.
* @param listener - The listening function
*/
removeChangedListener(listener: (added: readonly T[], updated: readonly T[], removed: readonly T[]) => void): void {
this._listeners.findAndRemove((value) => (value.fn === listener ? (value.destroyed = true) : false));
}
override _onUpdate(): void {
const { _platformSession: platformSession } = this._xrManager.sessionManager;
const { frame: platformFrame } = platformSession;
const {
_platformFeature: platformFeature,
_requestTrackings: requestTrackings,
_statusSnapshot: statusSnapshot,
_tracked: allTracked,
_added: added,
_updated: updated,
_removed: removed
} = this;
if (!platformFrame || !requestTrackings.length) {
return;
}
if (!platformFeature.checkAvailable(platformSession, platformFrame, requestTrackings)) {
return;
}
added.length = updated.length = removed.length = 0;
platformFeature.getTrackedResult(platformSession, platformFrame, requestTrackings, this._generateTracked);
for (let i = 0, n = requestTrackings.length; i < n; i++) {
const requestTracking = requestTrackings[i];
switch (requestTracking.state) {
case XRRequestTrackingState.Destroyed:
const destroyedTracked = requestTracking.tracked;
for (let j = 0, n = destroyedTracked.length; j < n; j++) {
const tracked = destroyedTracked[j];
const trackId = tracked.id;
if (statusSnapshot[trackId] === XRTrackingState.Tracking) {
removed.push(tracked);
allTracked.splice(allTracked.indexOf(tracked), 1);
}
statusSnapshot[trackId] = XRTrackingState.NotTracking;
}
break;
case XRRequestTrackingState.Resolved:
const { tracked } = requestTracking;
for (let j = 0, n = tracked.length; j < n; j++) {
const trackedObject = tracked[j];
const trackId = trackedObject.id;
if (trackedObject.state === XRTrackingState.Tracking) {
if (statusSnapshot[trackId] === XRTrackingState.Tracking) {
updated.push(trackedObject);
} else {
added.push(trackedObject);
statusSnapshot[trackId] = XRTrackingState.Tracking;
allTracked.push(trackedObject);
}
} else {
if (statusSnapshot[trackId] === XRTrackingState.Tracking) {
removed.push(trackedObject);
allTracked.splice(allTracked.indexOf(trackedObject), 1);
}
statusSnapshot[trackId] = trackedObject.state;
}
}
break;
default:
break;
}
}
for (let i = requestTrackings.length - 1; i >= 0; i--) {
requestTrackings[i].state === XRRequestTrackingState.Destroyed && requestTrackings.splice(i, 1);
}
if (added.length > 0 || updated.length > 0 || removed.length > 0) {
const listeners = this._listeners.getLoopArray();
for (let i = 0, n = listeners.length; i < n; i++) {
const listener = listeners[i];
!listener.destroyed && listener.fn(added, updated, removed);
}
}
}
override _onSessionStop(): void {
this._added.length = this._updated.length = this._removed.length = 0;
}
override _onSessionExit(): void {
// prettier-ignore
this._requestTrackings.length = this._tracked.length = this._added.length = this._updated.length = this._removed.length = 0;
this._listeners.findAndRemove((value) => (value.destroyed = true));
}
protected _addRequestTracking(requestTracking: K): void {
const { _platformFeature: platformFeature } = this;
if (this._xrManager.sessionManager._platformSession && !platformFeature.canModifyRequestTrackingAfterInit) {
throw new Error(XRFeatureType[this._type] + " request tracking cannot be modified after initialization.");
}
this._requestTrackings.push(requestTracking);
platformFeature.onAddRequestTracking(requestTracking);
}
protected _removeRequestTracking(requestTracking: K): void {
const { _platformFeature: platformFeature } = this;
if (this._xrManager.sessionManager._platformSession && !platformFeature.canModifyRequestTrackingAfterInit) {
throw new Error(XRFeatureType[this._type] + " request tracking cannot be modified after initialization.");
}
platformFeature.onDelRequestTracking(requestTracking);
}
protected _removeAllRequestTrackings(): void {
const { _platformFeature: platformFeature } = this;
if (this._xrManager.sessionManager._platformSession && !platformFeature.canModifyRequestTrackingAfterInit) {
throw new Error(XRFeatureType[this._type] + " request tracking cannot be modified after initialization.");
}
const { _requestTrackings: requestTrackings } = this;
for (let i = 0, n = requestTrackings.length; i < n; i++) {
platformFeature.onDelRequestTracking(requestTrackings[i]);
}
}
protected abstract _generateTracked(): T;
}

View File

@@ -1,15 +0,0 @@
import { IXRTracked } from "@galacean/engine-design";
import { XRPose } from "../../XRPose";
import { XRTrackingState } from "../../input/XRTrackingState";
/**
* The base class of XR tracked object.
*/
export abstract class XRTracked implements IXRTracked {
/** The unique id of the trackable. */
id: number;
/** The pose of the trackable in XR space. */
pose: XRPose = new XRPose();
/** The tracking state of the trackable. */
state: XRTrackingState = XRTrackingState.NotTracking;
}

View File

@@ -1,6 +0,0 @@
import { XRTracked } from "../XRTracked";
/**
* The anchor in XR space.
*/
export class XRAnchor extends XRTracked {}

View File

@@ -1,87 +0,0 @@
import { Quaternion, Vector3 } from "@galacean/engine";
import { XRManagerExtended, registerXRFeature } from "../../../XRManagerExtended";
import { XRFeatureType } from "../../XRFeatureType";
import { XRTrackableFeature } from "../XRTrackableFeature";
import { XRAnchor } from "./XRAnchor";
import { XRRequestAnchor } from "./XRRequestAnchor";
/**
* The manager of XR anchor tracking.
*/
@registerXRFeature(XRFeatureType.AnchorTracking)
export class XRAnchorTracking extends XRTrackableFeature<XRAnchor, XRRequestAnchor> {
private _anchors: XRAnchor[] = [];
/**
* The anchors to tracking.
*/
get trackingAnchors(): readonly XRAnchor[] {
return this._anchors;
}
/**
* The tracked anchors.
*/
get trackedAnchors(): readonly XRAnchor[] {
return this._tracked;
}
/**
* @param xrManager - The xr manager
*/
constructor(xrManager: XRManagerExtended) {
super(xrManager, XRFeatureType.AnchorTracking);
}
/**
* Add a anchor in XR space.
* @param anchor - The anchor to be added
*/
addAnchor(position: Vector3, rotation: Quaternion): XRAnchor {
if (!this._enabled) {
throw new Error("Cannot add an anchor from a disabled anchor manager.");
}
const { _anchors: anchors } = this;
const requestAnchor = new XRRequestAnchor(position, rotation);
const xrAnchor = this._generateTracked();
requestAnchor.tracked[0] = xrAnchor;
this._addRequestTracking(requestAnchor);
anchors.push(xrAnchor);
return xrAnchor;
}
/**
* Remove a anchor in XR space.
* @param anchor - The anchor to be removed
*/
removeAnchor(anchor: XRAnchor): void {
if (!this._enabled) {
throw new Error("Cannot remove an anchor from a disabled anchor manager.");
}
const { _requestTrackings: requestTrackings, _anchors: anchors } = this;
for (let i = 0, n = requestTrackings.length; i < n; i++) {
const requestAnchor = requestTrackings[i];
if (requestAnchor.tracked[0] === anchor) {
this._removeRequestTracking(requestAnchor);
break;
}
}
anchors.splice(anchors.indexOf(anchor), 1);
}
/**
* Remove all tracking anchors.
*/
clearAnchors(): void {
if (!this._enabled) {
throw new Error("Cannot remove anchors from a disabled anchor manager.");
}
this._removeAllRequestTrackings();
}
protected override _generateTracked(): XRAnchor {
const anchor = new XRAnchor();
anchor.id = XRTrackableFeature._uuid++;
return anchor;
}
}

View File

@@ -1,20 +0,0 @@
import { Quaternion, Vector3 } from "@galacean/engine";
import { IXRRequestAnchor } from "@galacean/engine-design";
import { XRRequestTracking } from "../XRRequestTracking";
import { XRAnchor } from "./XRAnchor";
/**
* The request anchor in XR space.
*/
export class XRRequestAnchor extends XRRequestTracking<XRAnchor> implements IXRRequestAnchor {
/**
* @param position - Requests the position of the anchor to be added
* @param rotation - Requests the rotation of the anchor to be added
*/
constructor(
public position: Vector3,
public rotation: Quaternion
) {
super();
}
}

View File

@@ -1,50 +0,0 @@
import { XRManagerExtended, registerXRFeature } from "../../../XRManagerExtended";
import { XRFeatureType } from "../../XRFeatureType";
import { XRTrackableFeature } from "../XRTrackableFeature";
import { XRReferenceImage } from "./XRReferenceImage";
import { XRRequestImage } from "./XRRequestImage";
import { XRTrackedImage } from "./XRTrackedImage";
/**
* The manager of XR image tracking.
*/
@registerXRFeature(XRFeatureType.ImageTracking)
export class XRImageTracking extends XRTrackableFeature<XRTrackedImage, XRRequestImage> {
private _trackingImages: XRReferenceImage[];
/**
* The image to tracking.
*/
get trackingImages(): readonly XRReferenceImage[] {
return this._trackingImages;
}
/**
* The tracked images.
*/
get trackedImages(): readonly XRTrackedImage[] {
return this._tracked;
}
/**
* @param xrManager - The xr manager
* @param trackingImages - The images to be tracked
*/
constructor(xrManager: XRManagerExtended, trackingImages: XRReferenceImage[]) {
super(xrManager, XRFeatureType.ImageTracking, trackingImages);
this._trackingImages = trackingImages;
const imageLength = trackingImages ? trackingImages.length : 0;
if (imageLength > 0) {
for (let i = 0, n = trackingImages.length; i < n; i++) {
this._addRequestTracking(new XRRequestImage(trackingImages[i]));
}
} else {
console.warn("No image to be tracked.");
}
}
protected override _generateTracked(): XRTrackedImage {
const image = new XRTrackedImage();
image.id = XRTrackableFeature._uuid++;
return image;
}
}

View File

@@ -1,18 +0,0 @@
import { IXRReferenceImage } from "@galacean/engine-design";
/**
* A reference image is an image to look for in the physical environment.
*/
export class XRReferenceImage implements IXRReferenceImage {
/**
* Create a reference image.
* @param name - The name of the image to be tracked
* @param imageSource - The image to be tracked
* @param physicalWidth - The expected physical width measurement for the real-world image being tracked in meters
*/
constructor(
public name: string,
public imageSource: ImageBitmapSource,
public physicalWidth: number
) {}
}

View File

@@ -1,16 +0,0 @@
import { IXRRequestImage } from "@galacean/engine-design";
import { XRRequestTracking } from "../XRRequestTracking";
import { XRReferenceImage } from "./XRReferenceImage";
import { XRTrackedImage } from "./XRTrackedImage";
/**
* The request image in XR space.
*/
export class XRRequestImage extends XRRequestTracking<XRTrackedImage> implements IXRRequestImage {
/**
* @param image - The image to be tracked
*/
constructor(public image: XRReferenceImage) {
super();
}
}

View File

@@ -1,13 +0,0 @@
import { IXRTrackedImage } from "@galacean/engine-design";
import { XRTracked } from "../XRTracked";
import { XRReferenceImage } from "./XRReferenceImage";
/**
* A tracked image in XR space.
*/
export class XRTrackedImage extends XRTracked implements IXRTrackedImage {
/** The reference image which was used to detect this image in the environment. */
referenceImage: XRReferenceImage;
/** The width of the image in meters in the physical world. */
measuredPhysicalWidth: number;
}

View File

@@ -1,13 +0,0 @@
/**
* Enumerates modes of plane in XR.
*/
export enum XRPlaneMode {
/** None. */
None = 0,
/** Horizontal */
Horizontal = 0b1,
/** Vertical */
Vertical = 0b10,
/** Includes horizontal and vertical. */
EveryThing = 0b11
}

View File

@@ -1,41 +0,0 @@
import { XRManagerExtended, registerXRFeature } from "../../../XRManagerExtended";
import { XRFeatureType } from "../../XRFeatureType";
import { XRTrackableFeature } from "../XRTrackableFeature";
import { XRPlaneMode } from "./XRPlaneMode";
import { XRRequestPlane } from "./XRRequestPlane";
import { XRTrackedPlane } from "./XRTrackedPlane";
/**
* The manager of plane tracking feature.
*/
@registerXRFeature(XRFeatureType.PlaneTracking)
export class XRPlaneTracking extends XRTrackableFeature<XRTrackedPlane, XRRequestPlane> {
/**
* The plane detection mode.
*/
get detectionMode(): XRPlaneMode {
return this._requestTrackings[0].detectionMode;
}
/**
* The tracked planes.
*/
get trackedPlanes(): readonly XRTrackedPlane[] {
return this._tracked;
}
/**
* @param xrManager - The xr manager
* @param detectionMode - The plane detection mode
*/
constructor(xrManager: XRManagerExtended, detectionMode: XRPlaneMode = XRPlaneMode.EveryThing) {
super(xrManager, XRFeatureType.PlaneTracking, detectionMode);
this._addRequestTracking(new XRRequestPlane(detectionMode));
}
protected override _generateTracked(): XRTrackedPlane {
const plane = new XRTrackedPlane();
plane.id = XRTrackableFeature._uuid++;
return plane;
}
}

View File

@@ -1,15 +0,0 @@
import { IXRRequestPlane } from "@galacean/engine-design";
import { XRRequestTracking } from "../XRRequestTracking";
import { XRTrackedPlane } from "./XRTrackedPlane";
/**
* The request plane in XR space.
*/
export class XRRequestPlane extends XRRequestTracking<XRTrackedPlane> implements IXRRequestPlane {
/**
* @param detectionMode - The plane detection mode
*/
constructor(public detectionMode: number) {
super();
}
}

View File

@@ -1,22 +0,0 @@
import { Vector3 } from "@galacean/engine";
import { IXRTrackedPlane } from "@galacean/engine-design";
import { XRTracked } from "../XRTracked";
import { XRPlaneMode } from "./XRPlaneMode";
/**
* The tracked plane in XR space.
*/
export class XRTrackedPlane extends XRTracked implements IXRTrackedPlane {
/** Whether the detected plane is horizontal or vertical. */
planeMode: XRPlaneMode;
/** The points that make up this plane.
* Note: These points are in the plane coordinate system,
* and their Y coordinates are all zero.
*/
polygon: Vector3[] = [];
/**
* Whether this frame changes the attributes of the plane.
* Note: Includes `polygon` but no `pose`.
*/
attributesDirty: boolean = false;
}

View File

@@ -1,45 +0,0 @@
// xr manager
import "./XRManagerExtended";
// xr pose
export { XRPose } from "./XRPose";
// xr feature
export { XRFeature } from "./feature/XRFeature";
export { XRTrackableFeature } from "./feature/trackable/XRTrackableFeature";
export { XRTracked } from "./feature/trackable/XRTracked";
// camera
export { XRCameraManager } from "./feature/camera/XRCameraManager";
// hitTest
export { TrackableType } from "./feature/hitTest/TrackableType";
export { XRHitResult } from "./feature/hitTest/XRHitResult";
export { XRHitTest } from "./feature/hitTest/XRHitTest";
// anchor tracking
export { XRAnchor } from "./feature/trackable/anchor/XRAnchor";
export { XRAnchorTracking } from "./feature/trackable/anchor/XRAnchorTracking";
// image tracking
export { XRImageTracking } from "./feature/trackable/image/XRImageTracking";
export { XRReferenceImage } from "./feature/trackable/image/XRReferenceImage";
export { XRTrackedImage } from "./feature/trackable/image/XRTrackedImage";
// plane Tracking
export { XRPlaneMode } from "./feature/trackable/plane/XRPlaneMode";
export { XRPlaneTracking } from "./feature/trackable/plane/XRPlaneTracking";
export { XRTrackedPlane } from "./feature/trackable/plane/XRTrackedPlane";
// xr input
export { XRCamera } from "./input/XRCamera";
export { XRController } from "./input/XRController";
export { XRInputButton } from "./input/XRInputButton";
export { XRInputManager } from "./input/XRInputManager";
export { XRTrackedInputDevice } from "./input/XRTrackedInputDevice";
export { XRTrackingState } from "./input/XRTrackingState";
// xr session
export { XRSessionManager } from "./session/XRSessionManager";
export { XRSessionMode } from "./session/XRSessionMode";
export { XRSessionState } from "./session/XRSessionState";
// only use in xr backend
export { XRFeatureType } from "./feature/XRFeatureType";
export { XRRequestTrackingState } from "./feature/trackable/XRRequestTrackingState";
export { XRInputEventType } from "./input/XRInputEventType";
export { XRTargetRayMode } from "./input/XRTargetRayMode";
export * from "./loader/XRReferenceImageDecoder";
export * from "./loader/XRReferenceImageLoader";

View File

@@ -1,19 +0,0 @@
import { IXRCamera } from "@galacean/engine-design";
import { Matrix, Rect, Camera } from "@galacean/engine";
import { XRPose } from "../XRPose";
import { XRInput } from "./XRInput";
/**
* The XR camera.
*/
export class XRCamera extends XRInput implements IXRCamera {
/** The pose of the camera in XR space. */
pose: XRPose = new XRPose();
/** The viewport of the camera. */
viewport: Rect = new Rect();
/** The projection matrix of the camera. */
projectionMatrix: Matrix = new Matrix();
/** @internal */
_camera: Camera;
}

View File

@@ -1,48 +0,0 @@
import { IXRController } from "@galacean/engine-design";
import { XRPose } from "../XRPose";
import { XRInput } from "./XRInput";
import { XRInputButton } from "./XRInputButton";
/**
* The XR controller.
*/
export class XRController extends XRInput implements IXRController {
/** The grip space pose of the controller in XR space. */
gripPose: XRPose = new XRPose();
/** The target ray space pose of the controller in XR space. */
targetRayPose: XRPose = new XRPose();
/** The currently pressed buttons of this controller. */
pressedButtons: XRInputButton = XRInputButton.None;
/** Record button lifted. */
down: XRInputButton = XRInputButton.None;
/** Record button pressed. */
up: XRInputButton = XRInputButton.None;
/**
*
* Returns whether the button is pressed.
* @param button - The button to check
* @returns Whether the button is pressed
*/
isButtonDown(button: XRInputButton): boolean {
return (this.down & button) !== 0;
}
/**
* Returns whether the button is lifted.
* @param button - The button to check
* @returns Whether the button is lifted
*/
isButtonUp(button: XRInputButton): boolean {
return (this.up & button) !== 0;
}
/**
* Returns whether the button is held down.
* @param button - The button to check
* @returns Whether the button is held down
*/
isButtonHeldDown(button: XRInputButton): boolean {
return (this.pressedButtons & button) !== 0;
}
}

View File

@@ -1,13 +0,0 @@
import { IXRInput } from "@galacean/engine-design";
import { XRTrackedInputDevice } from "./XRTrackedInputDevice";
import { XRTrackingState } from "./XRTrackingState";
export class XRInput implements IXRInput {
/** The tracking state of xr input. */
trackingState: XRTrackingState = XRTrackingState.NotTracking;
/**
* @internal
*/
constructor(public type: XRTrackedInputDevice) {}
}

View File

@@ -1,19 +0,0 @@
/**
* Enum for XR input button.
*/
export enum XRInputButton {
/** None */
None = 0x0,
/** Select */
Select = 0x1,
/** Select */
Trigger = 0x1,
/** Squeeze */
Squeeze = 0x2,
/** TouchPad */
TouchPad = 0x4,
/** A */
AButton = 0x8,
/** B */
BButton = 0x10
}

View File

@@ -1,8 +0,0 @@
export enum XRInputEventType {
SelectStart,
Select,
SelectEnd,
SqueezeStart,
Squeeze,
SqueezeEnd
}

View File

@@ -1,215 +0,0 @@
import { Engine, SafeLoopArray } from "@galacean/engine";
import { IXRInputEvent } from "@galacean/engine-design";
import { IXRListener, XRManagerExtended } from "../XRManagerExtended";
import { XRCamera } from "./XRCamera";
import { XRController } from "./XRController";
import { XRInput } from "./XRInput";
import { XRInputButton } from "./XRInputButton";
import { XRInputEventType } from "./XRInputEventType";
import { XRTargetRayMode } from "./XRTargetRayMode";
import { XRTrackedInputDevice } from "./XRTrackedInputDevice";
import { XRTrackingState } from "./XRTrackingState";
/**
* The manager of XR input.
*/
export class XRInputManager {
/** @internal */
_cameras: XRCamera[] = [];
/** @internal */
_controllers: XRController[] = [];
private _added: XRInput[] = [];
private _removed: XRInput[] = [];
private _trackedDevices: XRInput[] = [];
private _statusSnapshot: XRTrackingState[] = [];
private _listeners: SafeLoopArray<IXRListener> = new SafeLoopArray<IXRListener>();
/**
* @internal
*/
constructor(
private _xrManager: XRManagerExtended,
private _engine: Engine
) {
const { _trackedDevices: trackedDevices, _controllers: controllers, _cameras: cameras } = this;
for (let i = 0; i < 6; i++) {
switch (i) {
case XRTrackedInputDevice.Camera:
case XRTrackedInputDevice.LeftCamera:
case XRTrackedInputDevice.RightCamera:
cameras.push((trackedDevices[i] = new XRCamera(i)));
break;
case XRTrackedInputDevice.Controller:
case XRTrackedInputDevice.LeftController:
case XRTrackedInputDevice.RightController:
controllers.push((trackedDevices[i] = new XRController(i)));
break;
default:
break;
}
}
this._statusSnapshot.fill(XRTrackingState.NotTracking, 0, trackedDevices.length);
}
/**
* Returns the tracked device instance.
* @param type - The tracked input device type
* @returns The input instance
*/
getTrackedDevice<T extends XRInput>(type: XRTrackedInputDevice): T {
return <T>this._trackedDevices[type];
}
/**
* Add a listener for tracked device changes.
* @param listener - The listener to add
*/
addTrackedDeviceChangedListener(listener: (added: readonly XRInput[], removed: readonly XRInput[]) => void): void {
this._listeners.push({ fn: listener });
}
/**
* Remove a listener of tracked device changes.
* @param listener - The listener to remove
*/
removeTrackedDeviceChangedListener(listener: (added: readonly XRInput[], removed: readonly XRInput[]) => void): void {
this._listeners.findAndRemove((value) => (value.fn === listener ? (value.destroyed = true) : false));
}
/**
* @internal
*/
_onUpdate(): void {
const { _added: added, _removed: removed, _statusSnapshot: statusSnapshot } = this;
const { _trackedDevices: trackedDevices, _controllers: controllers } = this;
// Reset data
added.length = removed.length = 0;
for (let i = 0, n = controllers.length; i < n; i++) {
const controller = controllers[i];
controller.down = controller.up = 0;
}
// Handle events and update tracked devices
const { _platformSession: platformSession } = this._xrManager.sessionManager;
const { events: platformEvents } = platformSession;
for (let i = 0, n = platformEvents.length; i < n; i++) {
this._handleEvent(platformEvents[i]);
}
platformSession.resetEvents();
platformSession.frame.updateInputs(trackedDevices);
for (let i = 0, n = trackedDevices.length; i < n; i++) {
const input = trackedDevices[i];
if (!input) continue;
const nowState = input.trackingState;
if (statusSnapshot[i] === XRTrackingState.Tracking) {
if (nowState !== XRTrackingState.Tracking) {
removed.push(input);
}
} else {
if (nowState === XRTrackingState.Tracking) {
added.push(input);
}
}
statusSnapshot[i] = nowState;
}
// Dispatch change event
if (added.length > 0 || removed.length > 0) {
const listeners = this._listeners.getLoopArray();
for (let i = 0, n = listeners.length; i < n; i++) {
const listener = listeners[i];
!listener.destroyed && listener.fn(added, removed);
}
}
}
/**
* @internal
*/
_onDestroy(): void {
this._listeners.findAndRemove((value) => (value.destroyed = true));
}
private _handleEvent(event: IXRInputEvent): void {
const input = <XRController>this._trackedDevices[event.input];
switch (event.targetRayMode) {
case XRTargetRayMode.TrackedPointer:
switch (event.type) {
case XRInputEventType.SelectStart:
input.down |= XRInputButton.Select;
input.pressedButtons |= XRInputButton.Select;
break;
case XRInputEventType.SelectEnd:
input.up |= XRInputButton.Select;
input.pressedButtons &= ~XRInputButton.Select;
break;
case XRInputEventType.SqueezeStart:
input.down |= XRInputButton.Squeeze;
input.pressedButtons |= XRInputButton.Squeeze;
break;
case XRInputEventType.SqueezeEnd:
input.up |= XRInputButton.Squeeze;
input.pressedButtons &= ~XRInputButton.Squeeze;
break;
default:
break;
}
break;
case XRTargetRayMode.Screen:
const { _engine: engine } = this;
// @ts-ignore
const target: EventTarget = engine.inputManager._pointerManager._target;
// @ts-ignore
const canvas = <HTMLCanvasElement>engine.canvas._webCanvas;
const { clientWidth, clientHeight } = canvas;
const clientX = clientWidth * (event.x + 1) * 0.5;
const clientY = clientHeight * (event.y + 1) * 0.5;
// @ts-ignore
switch (event.type) {
case XRInputEventType.SelectStart:
target.dispatchEvent(this._makeUpPointerEvent("pointerdown", event.id, clientX, clientY));
break;
case XRInputEventType.Select:
target.dispatchEvent(this._makeUpPointerEvent("pointermove", event.id, clientX, clientY));
break;
case XRInputEventType.SelectEnd:
target.dispatchEvent(this._makeUpPointerEvent("pointerup", event.id, clientX, clientY));
target.dispatchEvent(this._makeUpPointerEvent("pointerleave", event.id, clientX, clientY));
break;
default:
break;
}
break;
default:
break;
}
}
private _makeUpPointerEvent(type: string, pointerId: number, clientX: number, clientY: number): PointerEvent {
const eventInitDict: PointerEventInit = {
pointerId,
clientX,
clientY
};
switch (type) {
case "pointerdown":
eventInitDict.button = 0;
eventInitDict.buttons = 1;
break;
case "pointermove":
eventInitDict.button = -1;
eventInitDict.buttons = 1;
break;
case "pointerup":
eventInitDict.button = 0;
eventInitDict.buttons = 0;
break;
case "pointerleave":
eventInitDict.button = 0;
eventInitDict.buttons = 0;
break;
default:
break;
}
return new PointerEvent(type, eventInitDict);
}
}

View File

@@ -1,5 +0,0 @@
export enum XRTargetRayMode {
Gaze,
TrackedPointer,
Screen
}

View File

@@ -1,21 +0,0 @@
/**
* Enumerates some input devices that can be tracked.(including status, posture and other information)
*/
export enum XRTrackedInputDevice {
/** Controller */
Controller,
/** Left controller */
LeftController,
/** Right controller */
RightController,
/** Camera */
Camera,
/** Left camera */
LeftCamera,
/** Right camera */
RightCamera,
/** Head */
LeftHand,
/** Right hand */
RightHand
}

View File

@@ -1,11 +0,0 @@
/**
* Enum for XR tracking state.
*/
export enum XRTrackingState {
/** Not tracking */
NotTracking,
/** Tracking */
Tracking,
/** Lost track */
TrackingLost
}

View File

@@ -1,17 +0,0 @@
import { AssetPromise, BufferReader, Engine, decoder } from "@galacean/engine";
import { XRReferenceImage } from "../feature/trackable/image/XRReferenceImage";
@decoder("XRReferenceImage")
export class XRReferenceImageDecoder {
static decode(engine: Engine, bufferReader: BufferReader): AssetPromise<XRReferenceImage> {
return new AssetPromise((resolve, reject) => {
const physicalWidth = bufferReader.nextFloat32();
bufferReader.nextUint8();
const img = new Image();
img.onload = () => {
resolve(new XRReferenceImage("", img, physicalWidth));
};
img.src = URL.createObjectURL(new window.Blob([bufferReader.nextImagesData(1)[0]]));
});
}
}

View File

@@ -1,19 +0,0 @@
import { AssetPromise, decode, Loader, LoadItem, resourceLoader, ResourceManager } from "@galacean/engine";
import { XRReferenceImage } from "../feature/trackable/image/XRReferenceImage";
@resourceLoader("XRReferenceImage", [])
export class XRReferenceImageLoader extends Loader<XRReferenceImage> {
load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<XRReferenceImage> {
return new AssetPromise((resolve, reject) => {
resourceManager
// @ts-ignore
._request<ArrayBuffer>(item.url, { ...item, type: "arraybuffer" })
.then((data) => {
decode<XRReferenceImage>(data, resourceManager.engine).then((referenceImage) => {
resolve(referenceImage);
});
})
.catch(reject);
});
}
}

View File

@@ -1,230 +0,0 @@
import { Engine, SafeLoopArray } from "@galacean/engine";
import { IHardwareRenderer, IXRSession } from "@galacean/engine-design";
import { IXRListener, XRManagerExtended } from "../XRManagerExtended";
import { XRFeature } from "../feature/XRFeature";
import { XRSessionMode } from "./XRSessionMode";
import { XRSessionState } from "./XRSessionState";
/**
* XRSessionManager manages the life cycle of XR sessions.
*/
export class XRSessionManager {
/** @internal */
_platformSession: IXRSession;
private _mode: XRSessionMode = XRSessionMode.None;
private _state: XRSessionState = XRSessionState.None;
private _rhi: IHardwareRenderer;
private _raf: (callback: FrameRequestCallback) => number;
private _caf: (id: number) => void;
private _listeners: SafeLoopArray<IXRListener> = new SafeLoopArray<IXRListener>();
/**
* The current session mode( AR or VR ).
*/
get mode(): XRSessionMode {
return this._mode;
}
/**
* Return the current session state.
*/
get state(): XRSessionState {
return this._state;
}
/**
* Return a list of supported frame rates.(only available in-session)
*/
get supportedFrameRate(): Float32Array {
return this._platformSession.supportedFrameRates;
}
/**
* Return the current frame rate as reported by the device.
*/
get frameRate(): number {
return this._platformSession.frameRate;
}
/**
* @internal
*/
constructor(
private _xrManager: XRManagerExtended,
private _engine: Engine
) {
// @ts-ignore
this._rhi = _engine._hardwareRenderer;
this._raf = requestAnimationFrame.bind(window);
this._caf = cancelAnimationFrame.bind(window);
this._onSessionExit = this._onSessionExit.bind(this);
}
/**
* Check if the specified mode is supported.
* @param mode - The mode to check
* @returns A promise that resolves if the mode is supported, otherwise rejects
*/
isSupportedMode(mode: XRSessionMode): Promise<void> {
return this._xrManager._platformDevice.isSupportedSessionMode(mode);
}
/**
* Run the session.
*/
run(): void {
const { _platformSession: platformSession, _engine: engine } = this;
if (!platformSession) {
throw new Error("Without session to run.");
}
platformSession.start();
this._setState(XRSessionState.Running);
this._xrManager._onSessionStart();
if (!engine.isPaused) {
engine.pause();
engine.resume();
}
}
/**
* Stop the session.
*/
stop(): void {
const { _platformSession: platformSession, _engine: engine, _rhi: rhi } = this;
if (!platformSession) {
throw new Error("Without session to stop.");
}
if (this._state !== XRSessionState.Running) {
throw new Error("Session is not running.");
}
rhi._mainFrameBuffer = null;
rhi._mainFrameWidth = rhi._mainFrameHeight = 0;
platformSession.stop();
this._setState(XRSessionState.Paused);
this._xrManager._onSessionStop();
if (!engine.isPaused) {
engine.pause();
engine.resume();
}
}
/**
* Add a listening function for session state changes.
* @param listener - The listening function
*/
addStateChangedListener(listener: (state: XRSessionState) => void): void {
this._listeners.push({ fn: listener });
}
/**
* Remove a listening function of session state changes.
* @param listener - The listening function
*/
removeStateChangedListener(listener: (state: XRSessionState) => void): void {
this._listeners.findAndRemove((value) => (value.fn === listener ? (value.destroyed = true) : false));
}
/**
* @internal
*/
_setState(value: XRSessionState) {
this._state = value;
const listeners = this._listeners.getLoopArray();
for (let i = 0, n = listeners.length; i < n; i++) {
const listener = listeners[i];
!listener.destroyed && listener.fn(value);
}
}
/**
* @internal
*/
_initialize(mode: XRSessionMode, features: XRFeature[]): Promise<void> {
return new Promise((resolve, reject) => {
const { _xrManager: xrManager } = this;
// Initialize all features
const platformFeatures = [];
for (let i = 0, n = features.length; i < n; i++) {
const { _platformFeature: platformFeature } = features[i];
platformFeature && platformFeatures.push(platformFeature);
}
xrManager._platformDevice
.requestSession(this._rhi, mode, platformFeatures)
.then((platformSession: IXRSession) => {
this._mode = mode;
this._platformSession = platformSession;
this._setState(XRSessionState.Initialized);
platformSession.setSessionExitCallBack(this._onSessionExit);
platformSession.addEventListener();
xrManager._onSessionInit();
resolve();
}, reject);
});
}
/**
* @internal
*/
_onUpdate() {
const { _rhi: rhi, _platformSession: platformSession } = this;
rhi._mainFrameBuffer = platformSession.framebuffer;
rhi._mainFrameWidth = platformSession.framebufferWidth;
rhi._mainFrameHeight = platformSession.framebufferHeight;
}
/**
* @internal
*/
_getRequestAnimationFrame(): (callback: FrameRequestCallback) => number {
if (this._state === XRSessionState.Running) {
return this._platformSession.requestAnimationFrame;
} else {
return this._raf;
}
}
/**
* @internal
*/
_getCancelAnimationFrame(): (id: number) => void {
if (this._state === XRSessionState.Running) {
return this._platformSession.cancelAnimationFrame;
} else {
return this._caf;
}
}
/**
* @internal
*/
_exit(): Promise<void> {
const { _platformSession: platformSession } = this;
if (!platformSession) {
return Promise.reject("Without session to stop.");
}
return platformSession.end();
}
private _onSessionExit() {
const { _rhi: rhi, _platformSession: platformSession, _engine: engine } = this;
rhi._mainFrameBuffer = null;
rhi._mainFrameWidth = rhi._mainFrameHeight = 0;
platformSession.removeEventListener();
this._platformSession = null;
this._setState(XRSessionState.None);
this._xrManager._onSessionExit();
if (!engine.isPaused) {
engine.pause();
engine.resume();
}
}
/**
* @internal
*/
_onDestroy(): void {
this._listeners.findAndRemove((value) => (value.destroyed = true));
this._raf = this._caf = null;
}
}

View File

@@ -1,8 +0,0 @@
/**
* The type of XR session.
*/
export enum XRSessionMode {
None,
AR,
VR
}

Some files were not shown because too many files have changed in this diff Show More