mirror of
https://github.com/galacean/engine.git
synced 2026-06-20 09:32:14 +08:00
chore: cleanup
This commit is contained in:
@@ -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();
|
||||
```
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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!";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { IPhysicsManager } from "@galacean/engine-design";
|
||||
|
||||
export class LitePhysicsManager implements IPhysicsManager {}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export { LitePhysics } from "./LitePhysics";
|
||||
|
||||
//@ts-ignore
|
||||
export const version = `__buildVersion`;
|
||||
|
||||
console.log(`Galacean Engine Physics Lite Version: ${version}`);
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/**/*"]
|
||||
}
|
||||
@@ -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).
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
9
packages/ui/src/shader/global.d.ts
vendored
9
packages/ui/src/shader/global.d.ts
vendored
@@ -1,9 +0,0 @@
|
||||
declare module "*.glsl" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.gs" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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/**/*"]
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
};
|
||||
});
|
||||
......
|
||||
```
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 user’s 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import "./feature/WebXRAnchorTracking";
|
||||
import "./feature/WebXRImageTracking";
|
||||
import "./feature/WebXRPlaneTracking";
|
||||
|
||||
export { WebXRDevice } from "./WebXRDevice";
|
||||
@@ -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/**/*"]
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
};
|
||||
});
|
||||
......
|
||||
```
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export enum XRFeatureType {
|
||||
AnchorTracking,
|
||||
ImageTracking,
|
||||
PlaneTracking,
|
||||
HitTest
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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[] = [];
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export enum XRRequestTrackingState {
|
||||
None,
|
||||
Submitted,
|
||||
Resolved,
|
||||
Rejected,
|
||||
Destroyed,
|
||||
WaitingDestroy
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { XRTracked } from "../XRTracked";
|
||||
|
||||
/**
|
||||
* The anchor in XR space.
|
||||
*/
|
||||
export class XRAnchor extends XRTracked {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export enum XRInputEventType {
|
||||
SelectStart,
|
||||
Select,
|
||||
SelectEnd,
|
||||
SqueezeStart,
|
||||
Squeeze,
|
||||
SqueezeEnd
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum XRTargetRayMode {
|
||||
Gaze,
|
||||
TrackedPointer,
|
||||
Screen
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Enum for XR tracking state.
|
||||
*/
|
||||
export enum XRTrackingState {
|
||||
/** Not tracking */
|
||||
NotTracking,
|
||||
/** Tracking */
|
||||
Tracking,
|
||||
/** Lost track */
|
||||
TrackingLost
|
||||
}
|
||||
@@ -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]]));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user