From fbbf57aa3587b1781a8a06d0ffd211bc3351d5ab Mon Sep 17 00:00:00 2001 From: ChenMo Date: Mon, 11 Jul 2022 14:10:51 +0800 Subject: [PATCH] Merge latest dev/0.8 to dev/asset-file (#860) * Test: Use Floss and Chai to Replace Jest (#778) test: Use Floss and Chai to Replace Jest * refactor: remove toolkit features (#780) * v0.7.0-beta.4 (#795) * Fix:local translate (#706) * fix(transform):local translate * Add component denpendent decorator (#796) * feat: add `dependentComponents` decorator * ci: remove ci on node 12 (#801) * Update README.md * Fix Test Version Error (#802) * ci: remove ci on node 12 * test: fix version * feat: add `priority` for renderer to order(#803) * feat(renderer): add `priority` for renderer to order * Fix SkyBox render mirror problem (#816) * refactor: fix skybox mirror problem * fix: change `region` and `pivot` origin to left-bottom (#809) * fix: change `region` and `pivot` origin to left-bottom * Merge latest main (#820) * v0.7.0-beta.4 * Update README.md * Update README.md * Update README.md * Add component denpendent decorator (#796) (#807) * feat: add `dependentComponents` decorator * fix(Renderer): destroy crash when material is null (#808) * Improve BlendShape when use attribute mode (#804) * feat: improve BlendShape when use attribute mode * v0.7.0-beta.5 * refactor: fix skybox mirror problem * fix: fixed the bug of animator revert when blendWeight length exceeds 4 (#817) * fix: fixed the bug of animator revert when blendWeight length exceeds 4 * v0.7.0-beta.6 * refactor: fix shader * fix(text): fix horizontal and vertical alignment error (#772) * fix(text): fix horizontal alignment error Co-authored-by: luzhuang <364439895@qq.com> Co-authored-by: singlecoder * refactor: rename (#827) * v0.7.0-beta.7 * feat: physics character controller (#818) * feat: add `CharacterController` for physX backend * optimization camera code (#830) refactor: optimization camera code * Refactor `cloneTo` to `copyFrom` for math library and rename `setValue` to `set` (#844) * feat: refactor `cloneTo` to `copyFrom` for math library * feat: rename `setValue` to `set` for math library * Fix transform`translate` and `rotate` space bug (#847) * fix: transform`translate` and `rotate` space bug * refactor: opt comments * Feat(core): add more clear flag item for Camera.clearFlags (#843) * feat(core): add more clear flag item for Camera.clearFlags * v0.8.0-alpha.0 * v0.8.0-alpha.1 * Add SpriteRenderer drawMode property(support simple and sliced) (#828) * feat: add sprite draw mode(support simple and sliced) * fix: TextureCube is left-hand,so x need inverse (#855) * `InputManager` support pointer button and wheel (#831) * feat: `InputManager` support pointer button and wheel * Add basic physics joint component include `FixedJoint`, `SpringJoint`, `HingeJoint` (#853) * feat: basic physics joint * Use char cache mode for TextRenderer (#837) * feat(text): use char cache mode for TextRenderer * v0.8.0-alpha.2 * v0.8.0-alpha.3 * fix: resolve conflict * fix: resolve conflict Co-authored-by: Hu Song Co-authored-by: AZhan Co-authored-by: singlecoder Co-authored-by: luzhuang <364439895@qq.com> Co-authored-by: zhuxudong Co-authored-by: yangfengzzz --- lerna.json | 2 +- packages/core/package.json | 10 +- packages/core/src/2d/assembler/IAssembler.ts | 11 + .../src/2d/assembler/SimpleSpriteAssembler.ts | 77 +++ .../src/2d/assembler/SlicedSpriteAssembler.ts | 151 +++++ .../2d/assembler/StaticInterfaceImplement.ts | 10 + packages/core/src/2d/atlas/FontAtlas.ts | 93 +++ packages/core/src/2d/data/RenderData2D.ts | 14 + .../src/2d/dynamic-atlas/DynamicTextAtlas.ts | 95 --- .../dynamic-atlas/DynamicTextAtlasManager.ts | 126 ---- packages/core/src/2d/enums/SpriteDirtyFlag.ts | 13 + packages/core/src/2d/enums/SpriteDrawMode.ts | 9 + packages/core/src/2d/index.ts | 1 + packages/core/src/2d/sprite/Sprite.ts | 334 +++++----- packages/core/src/2d/sprite/SpriteMask.ts | 197 ++++-- packages/core/src/2d/sprite/SpriteRenderer.ts | 303 +++++---- packages/core/src/2d/text/CharInfo.ts | 19 + packages/core/src/2d/text/CharRenderData.ts | 21 + .../core/src/2d/text/CharRenderDataPool.ts | 26 + packages/core/src/2d/text/Font.ts | 94 ++- packages/core/src/2d/text/TextRenderer.ts | 392 +++++++----- packages/core/src/2d/text/TextUtils.ts | 577 ++++++++---------- packages/core/src/Camera.ts | 4 +- packages/core/src/Engine.ts | 9 +- .../core/src/RenderPipeline/Basic2DBatcher.ts | 24 +- .../src/RenderPipeline/BasicRenderPipeline.ts | 2 +- .../core/src/RenderPipeline/RenderQueue.ts | 8 +- .../core/src/RenderPipeline/SpriteBatcher.ts | 29 +- .../core/src/RenderPipeline/SpriteElement.ts | 28 +- .../src/RenderPipeline/SpriteMaskBatcher.ts | 21 +- .../src/RenderPipeline/SpriteMaskElement.ts | 12 +- .../src/RenderPipeline/SpriteMaskManager.ts | 6 +- packages/core/src/Renderer.ts | 10 +- packages/core/src/Transform.ts | 18 +- packages/core/src/enums/CameraClearFlags.ts | 22 +- packages/core/src/input/InputManager.ts | 161 ++++- .../core/src/input/enums/PointerButton.ts | 29 + packages/core/src/input/index.ts | 3 +- packages/core/src/input/interface/IInput.ts | 18 + .../src/input/keyboard/KeyboardManager.ts | 200 +++--- .../core/src/input/pointer/PointerManager.ts | 205 +++++-- packages/core/src/input/wheel/WheelManager.ts | 80 +++ packages/core/src/physics/PhysicsManager.ts | 2 +- .../core/src/physics/enums/HingeJointFlag.ts | 12 + packages/core/src/physics/enums/index.ts | 4 + packages/core/src/physics/index.ts | 7 +- packages/core/src/physics/joint/FixedJoint.ts | 18 + packages/core/src/physics/joint/HingeJoint.ts | 146 +++++ packages/core/src/physics/joint/Joint.ts | 143 +++++ .../core/src/physics/joint/JointLimits.ts | 16 + packages/core/src/physics/joint/JointMotor.ts | 13 + .../core/src/physics/joint/SpringJoint.ts | 103 ++++ packages/core/src/physics/joint/index.ts | 7 + .../src/shaderlib/pbr/ibl_frag_define.glsl | 1 + packages/design/package.json | 8 +- packages/design/src/physics/IPhysics.ts | 20 + packages/design/src/physics/index.ts | 1 + .../design/src/physics/joints/IFixedJoint.ts | 6 + .../design/src/physics/joints/IHingeJoint.ts | 69 +++ packages/design/src/physics/joints/IJoint.ts | 48 ++ .../design/src/physics/joints/ISpringJoint.ts | 42 ++ packages/design/src/physics/joints/index.ts | 4 + packages/draco/package.json | 8 +- packages/loader/package.json | 15 +- packages/loader/src/SpriteAtlasLoader.ts | 38 +- packages/math/package.json | 6 +- packages/math/src/CollisionUtil.ts | 2 +- packages/math/src/Color.ts | 11 + packages/math/src/Vector2.ts | 18 +- packages/math/src/Vector3.ts | 7 +- packages/math/src/Vector4.ts | 17 +- packages/oasis-engine/package.json | 14 +- packages/physics-lite/package.json | 13 +- packages/physics-lite/src/LitePhysics.ts | 27 +- .../physics-lite/src/LitePhysicsManager.ts | 2 +- packages/physics-physx/package.json | 13 +- packages/physics-physx/src/PhysXPhysics.ts | 30 +- .../physics-physx/src/PhysXPhysicsManager.ts | 2 +- .../src/joint/PhysXFixedJoint.ts | 22 + .../src/joint/PhysXHingeJoint.ts | 108 ++++ .../physics-physx/src/joint/PhysXJoint.ts | 86 +++ .../src/joint/PhysXSpringJoint.ts | 71 +++ packages/rhi-webgl/package.json | 12 +- packages/rhi-webgl/src/WebGLRenderer.ts | 58 +- tests/package.json | 11 +- 85 files changed, 3323 insertions(+), 1402 deletions(-) create mode 100644 packages/core/src/2d/assembler/IAssembler.ts create mode 100644 packages/core/src/2d/assembler/SimpleSpriteAssembler.ts create mode 100644 packages/core/src/2d/assembler/SlicedSpriteAssembler.ts create mode 100644 packages/core/src/2d/assembler/StaticInterfaceImplement.ts create mode 100644 packages/core/src/2d/atlas/FontAtlas.ts create mode 100644 packages/core/src/2d/data/RenderData2D.ts delete mode 100644 packages/core/src/2d/dynamic-atlas/DynamicTextAtlas.ts delete mode 100644 packages/core/src/2d/dynamic-atlas/DynamicTextAtlasManager.ts create mode 100644 packages/core/src/2d/enums/SpriteDirtyFlag.ts create mode 100644 packages/core/src/2d/enums/SpriteDrawMode.ts create mode 100644 packages/core/src/2d/text/CharInfo.ts create mode 100644 packages/core/src/2d/text/CharRenderData.ts create mode 100644 packages/core/src/2d/text/CharRenderDataPool.ts create mode 100644 packages/core/src/input/enums/PointerButton.ts create mode 100644 packages/core/src/input/interface/IInput.ts create mode 100644 packages/core/src/input/wheel/WheelManager.ts create mode 100644 packages/core/src/physics/enums/HingeJointFlag.ts create mode 100644 packages/core/src/physics/enums/index.ts create mode 100644 packages/core/src/physics/joint/FixedJoint.ts create mode 100644 packages/core/src/physics/joint/HingeJoint.ts create mode 100644 packages/core/src/physics/joint/Joint.ts create mode 100644 packages/core/src/physics/joint/JointLimits.ts create mode 100644 packages/core/src/physics/joint/JointMotor.ts create mode 100644 packages/core/src/physics/joint/SpringJoint.ts create mode 100644 packages/core/src/physics/joint/index.ts create mode 100644 packages/design/src/physics/joints/IFixedJoint.ts create mode 100644 packages/design/src/physics/joints/IHingeJoint.ts create mode 100644 packages/design/src/physics/joints/IJoint.ts create mode 100644 packages/design/src/physics/joints/ISpringJoint.ts create mode 100644 packages/design/src/physics/joints/index.ts create mode 100644 packages/physics-physx/src/joint/PhysXFixedJoint.ts create mode 100644 packages/physics-physx/src/joint/PhysXHingeJoint.ts create mode 100644 packages/physics-physx/src/joint/PhysXJoint.ts create mode 100644 packages/physics-physx/src/joint/PhysXSpringJoint.ts diff --git a/lerna.json b/lerna.json index 70794d55c2..e1654e6caa 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "npmClient": "npm", - "version": "0.7.0-beta.7", + "version": "0.8.0-alpha.3", "bootstrap": { "hoist": true }, diff --git a/packages/core/package.json b/packages/core/package.json index a2d923ddb6..c9bebac357 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,10 @@ { "name": "@oasis-engine/core", - "version": "0.7.0-beta.7", + "version": "0.8.0-alpha.3", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", @@ -15,9 +19,9 @@ "types/**/*" ], "dependencies": { - "@oasis-engine/math": "0.7.0-beta.7" + "@oasis-engine/math": "0.8.0-alpha.3" }, "devDependencies": { - "@oasis-engine/design": "0.7.0-beta.7" + "@oasis-engine/design": "0.8.0-alpha.3" } } diff --git a/packages/core/src/2d/assembler/IAssembler.ts b/packages/core/src/2d/assembler/IAssembler.ts new file mode 100644 index 0000000000..22a571fc1d --- /dev/null +++ b/packages/core/src/2d/assembler/IAssembler.ts @@ -0,0 +1,11 @@ +import { Renderer } from "../../Renderer"; + +/** + * @internal + */ +export interface IAssembler { + resetData(renderer: Renderer): void; + updateData(renderer: Renderer): void; + updatePositions?(renderer: Renderer): void; + updateUVs?(renderer: Renderer): void; +} diff --git a/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts b/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts new file mode 100644 index 0000000000..740e14cd1e --- /dev/null +++ b/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts @@ -0,0 +1,77 @@ +import { BoundingBox, Matrix, Vector2, Vector3 } from "@oasis-engine/math"; +import { SpriteMask } from "../sprite"; +import { SpriteRenderer } from "../sprite/SpriteRenderer"; +import { IAssembler } from "./IAssembler"; +import { StaticInterfaceImplement } from "./StaticInterfaceImplement"; + +/** + * @internal + */ +@StaticInterfaceImplement() +export class SimpleSpriteAssembler { + static _rectangleTriangles: number[] = [0, 1, 2, 2, 1, 3]; + static _worldMatrix: Matrix = new Matrix(); + + static resetData(renderer: SpriteRenderer | SpriteMask): void { + const { _renderData: renderData } = renderer; + const vertexCount = (renderData.vertexCount = 4); + const { positions, uvs } = renderData; + if (positions.length < vertexCount) { + for (let i = positions.length; i < vertexCount; i++) { + positions.push(new Vector3()); + uvs.push(new Vector2()); + } + } + renderData.triangles = SimpleSpriteAssembler._rectangleTriangles; + } + + static updateData(renderer: SpriteRenderer | SpriteMask): void {} + + static updatePositions(renderer: SpriteRenderer | SpriteMask): void { + const { width, height } = renderer; + if (width === 0 || height === 0) { + return; + } + const { sprite } = renderer; + const { x: pivotX, y: pivotY } = sprite.pivot; + // Renderer's worldMatrix; + const { _worldMatrix: worldMatrix } = SimpleSpriteAssembler; + const { elements: wE } = worldMatrix; + // Parent's worldMatrix. + const { elements: pWE } = renderer.entity.transform.worldMatrix; + const sx = renderer.flipX ? -width : width; + const sy = renderer.flipY ? -height : height; + (wE[0] = pWE[0] * sx), (wE[1] = pWE[1] * sx), (wE[2] = pWE[2] * sx); + (wE[4] = pWE[4] * sy), (wE[5] = pWE[5] * sy), (wE[6] = pWE[6] * sy); + (wE[8] = pWE[8]), (wE[9] = pWE[9]), (wE[10] = pWE[10]); + wE[12] = pWE[12] - pivotX * wE[0] - pivotY * wE[4]; + wE[13] = pWE[13] - pivotX * wE[1] - pivotY * wE[5]; + wE[14] = pWE[14]; + + // --------------- + // 2 - 3 + // | | + // 0 - 1 + // --------------- + // Update positions. + const spritePositions = sprite._getPositions(); + const { positions } = renderer._renderData; + for (let i = 0; i < 4; i++) { + const { x, y } = spritePositions[i]; + positions[i].set(wE[0] * x + wE[4] * y + wE[12], wE[1] * x + wE[5] * y + wE[13], wE[2] * x + wE[6] * y + wE[14]); + } + + BoundingBox.transform(sprite._getBounds(), worldMatrix, renderer._bounds); + } + + static updateUVs(renderer: SpriteRenderer | SpriteMask): void { + const spriteUVs = renderer.sprite._getUVs(); + const renderUVs = renderer._renderData.uvs; + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[3]; + renderUVs[0].set(left, bottom); + renderUVs[1].set(right, bottom); + renderUVs[2].set(left, top); + renderUVs[3].set(right, top); + } +} diff --git a/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts b/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts new file mode 100644 index 0000000000..2b8e45ac9a --- /dev/null +++ b/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts @@ -0,0 +1,151 @@ +import { Matrix, Vector2, Vector3 } from "@oasis-engine/math"; +import { SpriteMask } from "../sprite"; +import { SpriteRenderer } from "../sprite/SpriteRenderer"; +import { IAssembler } from "./IAssembler"; +import { StaticInterfaceImplement } from "./StaticInterfaceImplement"; + +/** + * @internal + */ +@StaticInterfaceImplement() +export class SlicedSpriteAssembler { + static _worldMatrix: Matrix = new Matrix(); + static resetData(renderer: SpriteRenderer | SpriteMask): void { + const { _renderData: renderData } = renderer; + const { positions, uvs } = renderData; + if (positions.length < 16) { + for (let i = positions.length; i < 16; i++) { + positions.push(new Vector3()); + uvs.push(new Vector2()); + } + } + renderData.triangles = []; + } + + static updateData(renderer: SpriteRenderer | SpriteMask): void {} + + static updatePositions(renderer: SpriteRenderer | SpriteMask): void { + const { width, height } = renderer; + if (width === 0 || height === 0) { + return; + } + const { sprite } = renderer; + const { positions, uvs, triangles } = renderer._renderData; + const { border } = sprite; + const spriteUVs = sprite._getUVs(); + // Update local positions. + const spritePositions = sprite._getPositions(); + const { x: left, y: bottom } = spritePositions[0]; + const { x: right, y: top } = spritePositions[3]; + const { width: expectWidth, height: expectHeight } = sprite; + const fixedLeft = expectWidth * border.x; + const fixedBottom = expectHeight * border.y; + const fixedRight = expectHeight * border.z; + const fixedTop = expectWidth * border.w; + + // ------------------------ + // [3] + // | + // [2] + // | + // [1] + // | + // row [0] - [1] - [2] - [3] + // column + // ------------------------ + // Calculate row and column. + let row: number[], column: number[]; + if (fixedLeft + fixedRight > width) { + const widthScale = width / (fixedLeft + fixedRight); + row = [ + expectWidth * left * widthScale, + fixedLeft * widthScale, + fixedLeft * widthScale, + width - expectWidth * (1 - right) * widthScale + ]; + } else { + row = [expectWidth * left, fixedLeft, width - fixedRight, width - expectWidth * (1 - right)]; + } + + if (fixedTop + fixedBottom > height) { + const heightScale = height / (fixedTop + fixedBottom); + column = [ + expectHeight * bottom * heightScale, + fixedBottom * heightScale, + fixedBottom * heightScale, + height - expectHeight * (1 - top) * heightScale + ]; + } else { + column = [expectHeight * bottom, fixedBottom, height - fixedTop, height - expectHeight * (1 - top)]; + } + + // Update renderer's worldMatrix. + const { x: pivotX, y: pivotY } = renderer.sprite.pivot; + const localTransX = renderer.width * pivotX; + const localTransY = renderer.height * pivotY; + // Renderer's worldMatrix. + const { _worldMatrix: worldMatrix } = SlicedSpriteAssembler; + const { elements: wE } = worldMatrix; + // Parent's worldMatrix. + const { elements: pWE } = renderer.entity.transform.worldMatrix; + const sx = renderer.flipX ? -1 : 1; + const sy = renderer.flipY ? -1 : 1; + (wE[0] = pWE[0] * sx), (wE[1] = pWE[1] * sx), (wE[2] = pWE[2] * sx); + (wE[4] = pWE[4] * sy), (wE[5] = pWE[5] * sy), (wE[6] = pWE[6] * sy); + (wE[8] = pWE[8]), (wE[9] = pWE[9]), (wE[10] = pWE[10]); + wE[12] = pWE[12] - localTransX * wE[0] - localTransY * wE[4]; + wE[13] = pWE[13] - localTransX * wE[1] - localTransY * wE[5]; + wE[14] = pWE[14]; + + // ------------------------ + // 3 - 7 - 11 - 15 + // | | | | + // 2 - 6 - 10 - 14 + // | | | | + // 1 - 5 - 9 - 13 + // | | | | + // 0 - 4 - 8 - 12 + // ------------------------ + // Assemble position and uv. + let vertexCount = 0; + let realICount = 0; + for (let i = 0; i < 4; i++) { + const rowValue = row[i]; + const rowU = spriteUVs[i].x; + for (let j = 0; j < 4; j++) { + const columnValue = column[j]; + positions[vertexCount].set( + wE[0] * rowValue + wE[4] * columnValue + wE[12], + wE[1] * rowValue + wE[5] * columnValue + wE[13], + wE[2] * rowValue + wE[6] * columnValue + wE[14] + ); + uvs[vertexCount].set(rowU, spriteUVs[j].y); + ++vertexCount; + } + ++realICount; + } + + const realJCount = vertexCount / realICount; + let indexOffset = 0; + for (let i = 0; i < realICount - 1; ++i) { + for (let j = 0; j < realJCount - 1; ++j) { + const start = i * realJCount + j; + triangles[indexOffset++] = start; + triangles[indexOffset++] = start + 1; + triangles[indexOffset++] = start + realJCount; + triangles[indexOffset++] = start + 1; + triangles[indexOffset++] = start + realJCount + 1; + triangles[indexOffset++] = start + realJCount; + } + } + renderer._renderData.vertexCount = realICount * realJCount; + triangles.length = (realICount - 1) * (realJCount - 1) * 6; + + const { min, max } = renderer._bounds; + min.set(row[0], column[0], 0); + max.set(row[3], column[3], 0); + renderer._bounds.transform(worldMatrix); + } + + static updateUVs(renderer: SpriteRenderer | SpriteMask): void {} +} diff --git a/packages/core/src/2d/assembler/StaticInterfaceImplement.ts b/packages/core/src/2d/assembler/StaticInterfaceImplement.ts new file mode 100644 index 0000000000..0741e6897b --- /dev/null +++ b/packages/core/src/2d/assembler/StaticInterfaceImplement.ts @@ -0,0 +1,10 @@ +/** + * Static interface implement decorator. + * https://stackoverflow.com/questions/13955157/how-to-define-static-property-in-typescript-interface + */ +export function StaticInterfaceImplement() { + return (constructor: U) => { + constructor; + }; +} + diff --git a/packages/core/src/2d/atlas/FontAtlas.ts b/packages/core/src/2d/atlas/FontAtlas.ts new file mode 100644 index 0000000000..5b5e37241c --- /dev/null +++ b/packages/core/src/2d/atlas/FontAtlas.ts @@ -0,0 +1,93 @@ +import { RefObject } from "../../asset/RefObject"; +import { Engine } from "../../Engine"; +import { Texture2D } from "../../texture/Texture2D"; +import { CharInfo } from "../text/CharInfo"; + +/** + * @internal + * Font Atlas. + */ +export class FontAtlas extends RefObject { + private _charInfoMap: Record = {}; + private _texture: Texture2D; + private _space: number = 1; + private _curX: number = 1; + private _curY: number = 1; + private _nextY: number = 1; + + get texture(): Texture2D { + return this._texture; + } + + set texture(value: Texture2D) { + this._texture = value; + } + + /** + * Constructor a FontAtlas. + * @param engine - Engine to which the FontAtlas belongs + */ + constructor(engine: Engine) { + super(engine); + } + + /** + * @override + */ + _onDestroy(): void { + this._texture.destroy(); + this._texture = null; + this._charInfoMap = {}; + } + + uploadCharTexture(charInfo: CharInfo, imageSource: TexImageSource | OffscreenCanvas): boolean { + const { w: width, h: height } = charInfo; + const { _space: space, texture } = this; + const textureSize = texture.width; + const offsetWidth = width + space; + const offsetHeight = height + space; + if ((1 + offsetWidth) >= textureSize || (1 + offsetHeight) >= textureSize) { + throw Error("The char fontSize is too large."); + } + + const endX = this._curX + offsetWidth; + if (endX >= textureSize) { + this._curX = space; + this._curY = this._nextY + space; + } + const endY = this._curY + offsetHeight; + if (endY > this._nextY) { + this._nextY = endY; + } + if (endY >= textureSize) { + return false; + } + + if (width > 0 && height > 0) { + texture.setImageSource(imageSource, 0, false, false, this._curX, this._curY); + texture.generateMipmaps(); + } + + const textureSizeReciprocal = 1.0 / textureSize; + const x = this._curX; + const y = this._curY; + const w = width; + const h = height; + charInfo.x = x; + charInfo.y = y; + charInfo.u0 = x * textureSizeReciprocal; + charInfo.u1 = (x + w) * textureSizeReciprocal; + charInfo.v0 = y * textureSizeReciprocal; + charInfo.v1 = (y + h) * textureSizeReciprocal; + this._curX += offsetWidth + space; + return true; + } + + addCharInfo(char: string, charInfo: CharInfo) { + this._charInfoMap[char.charCodeAt(0)] = charInfo; + } + + getCharInfo(char: string): CharInfo { + return this._charInfoMap[char.charCodeAt(0)]; + } +} diff --git a/packages/core/src/2d/data/RenderData2D.ts b/packages/core/src/2d/data/RenderData2D.ts new file mode 100644 index 0000000000..13e3aeb684 --- /dev/null +++ b/packages/core/src/2d/data/RenderData2D.ts @@ -0,0 +1,14 @@ +import { Color, Vector2, Vector3 } from "@oasis-engine/math"; + +/** + * @internal + */ +export class RenderData2D { + constructor( + public vertexCount: number, + public positions: Vector3[], + public uvs: Vector2[], + public triangles: number[] = null, + public color: Color = null + ) {} +} diff --git a/packages/core/src/2d/dynamic-atlas/DynamicTextAtlas.ts b/packages/core/src/2d/dynamic-atlas/DynamicTextAtlas.ts deleted file mode 100644 index 89d91ee39b..0000000000 --- a/packages/core/src/2d/dynamic-atlas/DynamicTextAtlas.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Rect } from "@oasis-engine/math"; -import { Engine } from "../../Engine"; -import { Texture2D } from "../../texture/Texture2D"; -import { Sprite } from "../sprite/Sprite"; - -/** - * Dynamic atlas for text. - */ -export class DynamicTextAtlas { - private static _region: Rect = new Rect(); - - private _texture: Texture2D; - private _width: number; - private _height: number; - - private _space: number = 1; - private _curX: number = 1; - private _curY: number = 1; - private _nextY: number = 1; - - private _sprites: Record = {}; - - constructor(engine: Engine, width: number, height: number) { - this._width = width; - this._height = height; - this._texture = new Texture2D(engine, width, height); - this._texture._addRefCount(1); - } - - /** - * Destroy atlas, it will release the texture. - */ - public destroy() { - this._sprites = {}; - this._texture.destroy(true); - } - - /** - * Add a sprite. - * @param sprite - the sprite to add - * @param imageSource - The source of texture - * @returns true if add sprite success, otherwise false - */ - public addSprite(sprite: Sprite, imageSource: TexImageSource | OffscreenCanvas): boolean { - const { _space: space, _texture: texture } = this; - const { width, height } = imageSource; - - const endX = this._curX + width + space; - if (endX >= this._width) { - this._curX = space; - this._curY = this._nextY + space; - } - - const endY = this._curY + height + space; - if (endY > this._nextY) { - this._nextY = endY; - } - - if (this._nextY >= this._height) { - return false; - } - - texture.setImageSource(imageSource, 0, false, false, this._curX, this._curY); - texture.generateMipmaps(); - - const { _width, _height } = this; - const region = DynamicTextAtlas._region; - region.set(this._curX / _width, this._curY / _height, width / _width, height / _height); - - // destroy origin texture. - sprite.texture && sprite.texture.destroy(); - // Update atlas texture. - sprite.atlasRegion = region; - sprite.texture = texture; - this._curX = endX + space; - - return true; - } - - /** - * Remove a sprite. - * @param sprite - the sprite to remove - * @returns true if remove sprite success, otherwise false - */ - public removeSprite(sprite: Sprite): boolean { - const id = sprite.instanceId; - const { _sprites } = this; - if (_sprites[id]) { - delete _sprites[id]; - return true; - } - return false; - } -} - diff --git a/packages/core/src/2d/dynamic-atlas/DynamicTextAtlasManager.ts b/packages/core/src/2d/dynamic-atlas/DynamicTextAtlasManager.ts deleted file mode 100644 index 0032ac87c1..0000000000 --- a/packages/core/src/2d/dynamic-atlas/DynamicTextAtlasManager.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Sprite } from "../sprite/Sprite"; -import { Engine } from "../../Engine"; -import { DynamicTextAtlas } from "./DynamicTextAtlas"; - -/** - * Dynamic atlas manager for text. - */ -export class DynamicTextAtlasManager { - private _maxAtlasCount: number = 2; - private _textureSize: number = 1024; - private _atlases: Array = []; - private _atlasIndex: number = -1; - private _spritesInAtlasIndex: Record = {}; - - /** - * Indicates how many atlases should be created. - */ - get maxAtlasCount(): number { - return this._maxAtlasCount; - } - - set maxAtlasCount(val: number) { - this._maxAtlasCount = val; - } - - /** - * Indicates the size of the texture. - */ - get textureSize(): number { - return this._textureSize; - } - - set textureSize(val: number) { - this._textureSize = Math.min(val, 2048); - } - - /** - * @internal - */ - constructor(public readonly engine: Engine) {} - - /** - * Add a sprite to atlas. - * @param sprite - the sprite to add - * @param imageSource - The source of texture - * @returns true if add sprite success, otherwise false - */ - public addSprite(sprite: Sprite, imageSource: TexImageSource | OffscreenCanvas): boolean { - // Remove sprite if the sprite has been add. - const { _spritesInAtlasIndex, _atlases } = this; - const id = sprite.instanceId; - const atlasIndex = _spritesInAtlasIndex[id]; - if (atlasIndex) { - _atlases[atlasIndex].removeSprite(sprite); - delete _spritesInAtlasIndex[id]; - } - - if (this._atlasIndex >= this._maxAtlasCount) { - return false; - } - - let atlas = _atlases[this._atlasIndex]; - if (!atlas) { - atlas = this._createAtlas(); - } - - if (atlas.addSprite(sprite, imageSource)) { - _spritesInAtlasIndex[id] = this._atlasIndex; - return true; - } - - if (this._atlasIndex + 1 >= this._maxAtlasCount) { - this._atlasIndex = this._maxAtlasCount; - return false; - } - - atlas = this._createAtlas(); - if (atlas.addSprite(sprite, imageSource)) { - _spritesInAtlasIndex[id] = this._atlasIndex; - return true; - } - return false; - } - - /** - * Remove a sprite from atlas. - * @param sprite - the sprite to remove - * @returns true if remove sprite success, otherwise false - */ - public removeSprite(sprite: Sprite): boolean { - if (!sprite) return false; - - const { _atlases } = this; - for (let i = _atlases.length - 1; i >= 0; --i) { - const atlas = _atlases[i]; - if(atlas.removeSprite(sprite)) { - delete this._spritesInAtlasIndex[i]; - return true; - } - } - - return false; - } - - /** - * Reset all atlases. - */ - public reset() { - const { _atlases } = this; - for (let i = 0, l = _atlases.length; i < l; ++i) { - _atlases[i].destroy(); - } - - _atlases.length = 0; - this._atlasIndex = -1; - this._spritesInAtlasIndex = {}; - } - - private _createAtlas(): DynamicTextAtlas { - this._atlasIndex++; - const { _textureSize } = this; - const atlas = new DynamicTextAtlas(this.engine, _textureSize, _textureSize); - this._atlases.push(atlas); - return atlas; - } -} diff --git a/packages/core/src/2d/enums/SpriteDirtyFlag.ts b/packages/core/src/2d/enums/SpriteDirtyFlag.ts new file mode 100644 index 0000000000..3070862dcf --- /dev/null +++ b/packages/core/src/2d/enums/SpriteDirtyFlag.ts @@ -0,0 +1,13 @@ +/** + * Sprite Property Dirty Flag. + */ +export enum SpritePropertyDirtyFlag { + texture = 0x1, + size = 0x2, + atlasRotate = 0x4, + atlasRegion = 0x8, + atlasRegionOffset = 0x10, + region = 0x20, + pivot = 0x40, + border = 0x80 +} diff --git a/packages/core/src/2d/enums/SpriteDrawMode.ts b/packages/core/src/2d/enums/SpriteDrawMode.ts new file mode 100644 index 0000000000..40b7979e95 --- /dev/null +++ b/packages/core/src/2d/enums/SpriteDrawMode.ts @@ -0,0 +1,9 @@ +/** + * Sprite's drawing mode enumeration. + */ +export enum SpriteDrawMode { + /** Overall scaling when modifying size. */ + Simple, + /** When modifying the size, it is transformed according to the 9-slice settings (border). */ + Sliced +} diff --git a/packages/core/src/2d/index.ts b/packages/core/src/2d/index.ts index f58f66ada7..c7bb91fa4e 100644 --- a/packages/core/src/2d/index.ts +++ b/packages/core/src/2d/index.ts @@ -4,5 +4,6 @@ export { TextHorizontalAlignment, TextVerticalAlignment } from "./enums/TextAlig export { OverflowMode } from "./enums/TextOverflow"; export { FontStyle } from "./enums/FontStyle"; export { SpriteAtlas } from "./atlas/SpriteAtlas"; +export { SpriteDrawMode } from "./enums/SpriteDrawMode"; export * from "./sprite/index"; export * from "./text/index"; diff --git a/packages/core/src/2d/sprite/Sprite.ts b/packages/core/src/2d/sprite/Sprite.ts index d9075392e3..ccd3486613 100644 --- a/packages/core/src/2d/sprite/Sprite.ts +++ b/packages/core/src/2d/sprite/Sprite.ts @@ -1,37 +1,37 @@ import { BoundingBox, MathUtil, Rect, Vector2, Vector4 } from "@oasis-engine/math"; import { RefObject } from "../../asset/RefObject"; -import { BoolUpdateFlag } from "../../BoolUpdateFlag"; import { Engine } from "../../Engine"; +import { ListenerUpdateFlag } from "../../ListenerUpdateFlag"; import { Texture2D } from "../../texture/Texture2D"; import { UpdateFlagManager } from "../../UpdateFlagManager"; +import { SpritePropertyDirtyFlag } from "../enums/SpriteDirtyFlag"; /** * 2D sprite. */ export class Sprite extends RefObject { - private static _rectangleTriangles: number[] = [0, 2, 1, 2, 0, 3]; - /** The name of sprite. */ name: string; - /** @internal */ - _uv: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; - /** @internal */ - _positions: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; - /** @internal */ - _bounds: BoundingBox = new BoundingBox(); - /** @internal */ - _triangles: number[]; /** @internal temp solution. */ _assetID: number; - private _pixelsPerUnit: number; + private _width: number = undefined; + private _height: number = undefined; + + private _positions: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; + private _uvs: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; + private _bounds: BoundingBox = new BoundingBox(); + private _texture: Texture2D = null; private _atlasRotated: boolean = false; - private _region: Rect = new Rect(0, 0, 1, 1); - private _pivot: Vector2 = new Vector2(0.5, 0.5); private _atlasRegion: Rect = new Rect(0, 0, 1, 1); private _atlasRegionOffset: Vector4 = new Vector4(0, 0, 0, 0); + + private _region: Rect = new Rect(0, 0, 1, 1); + private _pivot: Vector2 = new Vector2(0.5, 0.5); + private _border: Vector4 = new Vector4(0, 0, 0, 0); + private _dirtyFlag: DirtyFlag = DirtyFlag.all; private _updateFlagManager: UpdateFlagManager = new UpdateFlagManager(); @@ -45,20 +45,38 @@ export class Sprite extends RefObject { set texture(value: Texture2D) { if (this._texture !== value) { this._texture = value; - this._setDirtyFlagTrue(DirtyFlag.positions); + this._dispatchSpriteChange(SpritePropertyDirtyFlag.texture); } } /** - * Bounding volume of the sprite. - * @remarks The returned bounds should be considered deep-read-only. + * The width of the sprite (in world coordinates). */ - get bounds(): Readonly { - if (this._isContainDirtyFlag(DirtyFlag.positions) && this._texture) { - this._updatePositionsAndBounds(); - this._setDirtyFlagFalse(DirtyFlag.positions); + get width(): number { + this._width === undefined && this._calDefaultSize(); + return this._width; + } + + set width(value: number) { + if (this._width !== value) { + this._width = value; + this._dispatchSpriteChange(SpritePropertyDirtyFlag.size); + } + } + + /** + * The height of the sprite (in world coordinates). + */ + get height(): number { + this._height === undefined && this._calDefaultSize(); + return this._height; + } + + set height(value: number) { + if (this._height !== value) { + this._height = value; + this._dispatchSpriteChange(SpritePropertyDirtyFlag.size); } - return this._bounds; } /** @@ -71,7 +89,6 @@ export class Sprite extends RefObject { set atlasRotated(value: boolean) { if (this._atlasRotated != value) { this._atlasRotated = value; - this._setDirtyFlagTrue(DirtyFlag.positions | DirtyFlag.uv); } } @@ -86,7 +103,7 @@ export class Sprite extends RefObject { const x = MathUtil.clamp(value.x, 0, 1); const y = MathUtil.clamp(value.y, 0, 1); this._atlasRegion.set(x, y, MathUtil.clamp(value.width, 0, 1 - x), MathUtil.clamp(value.height, 0, 1 - y)); - this._setDirtyFlagTrue(DirtyFlag.positions | DirtyFlag.uv); + this._dispatchSpriteChange(SpritePropertyDirtyFlag.atlasRegion); } /** @@ -100,23 +117,7 @@ export class Sprite extends RefObject { const x = MathUtil.clamp(value.x, 0, 1); const y = MathUtil.clamp(value.y, 0, 1); this._atlasRegionOffset.set(x, y, MathUtil.clamp(value.z, 0, 1 - x), MathUtil.clamp(value.w, 0, 1 - y)); - this._setDirtyFlagTrue(DirtyFlag.positions | DirtyFlag.uv); - } - - /** - * Location of the sprite's center point in the rectangle region, specified in normalized. - */ - get pivot(): Vector2 { - return this._pivot; - } - - set pivot(value: Vector2) { - const pivot = this._pivot; - const { x, y } = value; - if (pivot === value || pivot.x !== x || pivot.y !== y) { - pivot.set(x, y); - this._setDirtyFlagTrue(DirtyFlag.positions); - } + this._dispatchSpriteChange(SpritePropertyDirtyFlag.atlasRegionOffset); } /** @@ -131,30 +132,56 @@ export class Sprite extends RefObject { const x = MathUtil.clamp(value.x, 0, 1); const y = MathUtil.clamp(value.y, 0, 1); region.set(x, y, MathUtil.clamp(value.width, 0, 1 - x), MathUtil.clamp(value.height, 0, 1 - y)); - this._setDirtyFlagTrue(DirtyFlag.positions | DirtyFlag.uv); + this._dispatchSpriteChange(SpritePropertyDirtyFlag.region); } /** - * The number of pixels in the sprite that correspond to one unit in world space. + * Location of the sprite's center point in the rectangle region, specified in normalized. + * The origin is at the bottom left and the default value is (0.5, 0.5). */ - get pixelsPerUnit(): number { - return this._pixelsPerUnit; + get pivot(): Vector2 { + return this._pivot; } - set pixelsPerUnit(value: number) { - if (this._pixelsPerUnit !== value) { - this._pixelsPerUnit = value; - this._setDirtyFlagTrue(DirtyFlag.positions); + set pivot(value: Vector2) { + const pivot = this._pivot; + if (pivot === value) { + this._dispatchSpriteChange(SpritePropertyDirtyFlag.pivot); + } else { + const { x, y } = value; + if (pivot.x !== x || pivot.y !== y) { + pivot.set(x, y); + this._dispatchSpriteChange(SpritePropertyDirtyFlag.pivot); + } } } + /** + * Get the border of the sprite. + * x y z w + * | | | | + * Left, bottom, right, top. + * @remark only use in sliced mode. + */ + get border(): Vector4 { + return this._border; + } + + set border(value: Vector4) { + const border = this._border; + const x = MathUtil.clamp(value.x, 0, 1); + const y = MathUtil.clamp(value.y, 0, 1); + border.set(x, y, MathUtil.clamp(value.z, 0, 1 - x), MathUtil.clamp(value.w, 0, 1 - y)); + this._dispatchSpriteChange(SpritePropertyDirtyFlag.border); + } + /** * Constructor a Sprite. * @param engine - Engine to which the sprite belongs * @param texture - Texture from which to obtain the Sprite * @param region - Rectangle region of the texture to use for the Sprite, specified in normalized * @param pivot - Sprite's pivot point relative to its graphic rectangle, specified in normalized - * @param pixelsPerUnit - The number of pixels in the Sprite that correspond to one unit in world space + * @param border - Boundaries when using Slice DrawMode, specified in normalized * @param name - The name of Sprite */ constructor( @@ -162,19 +189,15 @@ export class Sprite extends RefObject { texture: Texture2D = null, region: Rect = null, pivot: Vector2 = null, - pixelsPerUnit: number = 128, + border: Vector4 = null, name: string = null ) { super(engine); - - this.name = name; this._texture = texture; - this._pixelsPerUnit = pixelsPerUnit; - - region && this._region.copyFrom(region); - pivot && this._pivot.copyFrom(pivot); - - this._triangles = Sprite._rectangleTriangles; + region && region.copyFrom(this._region); + pivot && pivot.copyFrom(this._pivot); + border && border.copyFrom(this._border); + this.name = name; } /** @@ -182,14 +205,7 @@ export class Sprite extends RefObject { * @returns Cloned sprite */ clone(): Sprite { - const cloneSprite = new Sprite( - this._engine, - this._texture, - this._region, - this._pivot, - this._pixelsPerUnit, - this.name - ); + const cloneSprite = new Sprite(this._engine, this._texture, this._region, this._pivot, this._border, this.name); cloneSprite._assetID = this._assetID; cloneSprite._atlasRotated = this._atlasRotated; cloneSprite._atlasRegion.copyFrom(this._atlasRegion); @@ -200,111 +216,137 @@ export class Sprite extends RefObject { /** * @internal */ - _registerUpdateFlag(): BoolUpdateFlag { - return this._updateFlagManager.createFlag(BoolUpdateFlag); + _registerUpdateFlag(): ListenerUpdateFlag { + return this._updateFlagManager.createFlag(ListenerUpdateFlag); } /** - * @override + * @internal */ - _onDestroy(): void { - if (this._texture) { - this._texture = null; - } + _getPositions(): Vector2[] { + this._dirtyFlag & DirtyFlag.positions && this._updatePositions(); + return this._positions; } /** - * Update positions and bounds. + * @internal */ - private _updatePositionsAndBounds(): void { - const { _texture: texture, _bounds: bounds } = this; - if (texture) { - const { _atlasRegion: atlasRegion, _pivot: pivot } = this; - const { x: blankLeft, y: blankTop, z: blankRight, w: blankBottom } = this._atlasRegionOffset; - const { x: regionX, y: regionY, width: regionW, height: regionH } = this._region; - const regionRight = 1 - regionX - regionW; - const regionBottom = 1 - regionY - regionH; - // Real rendering size. - const realRenderW = - (texture.width * atlasRegion.width * regionW) / ((1 - blankLeft - blankRight) * this._pixelsPerUnit); - const realRenderH = - (texture.height * atlasRegion.height * regionH) / ((1 - blankTop - blankBottom) * this._pixelsPerUnit); - // Coordinates of the four boundaries. - const left = (Math.max(blankLeft - regionX, 0) / regionW - pivot.x) * realRenderW; - const right = (1 - Math.max(blankRight - regionRight, 0) / regionW - pivot.x) * realRenderW; - const top = (1 - Math.max(blankTop - regionBottom, 0) / regionH - pivot.y) * realRenderH; - const bottom = (Math.max(blankBottom - regionY, 0) / regionH - pivot.y) * realRenderH; - // Assign values ​​to _positions - const positions = this._positions; - // Top-left. - positions[0].set(left, top); - // Top-right. - positions[1].set(right, top); - // Bottom-right. - positions[2].set(right, bottom); - // Bottom-left. - positions[3].set(left, bottom); - - // Update bounds. - bounds.min.set(left, bottom, 0); - bounds.max.set(right, top, 0); - } else { - // Update bounds. - bounds.min.set(0, 0, 0); - bounds.max.set(0, 0, 0); - } + _getUVs(): Vector2[] { + this._dirtyFlag & DirtyFlag.uvs && this._updateUVs(); + return this._uvs; } /** * @internal */ - _updateMesh(): void { - if (this._isContainDirtyFlag(DirtyFlag.positions)) { - this._updatePositionsAndBounds(); + _getBounds(): BoundingBox { + this._dirtyFlag & DirtyFlag.positions && this._updatePositions(); + return this._bounds; + } + + /** + * @override + */ + _onDestroy(): void { + if (this._texture) { + this._texture = null; } + } - if (this._isContainDirtyFlag(DirtyFlag.uv)) { - const { _uv: uv, _atlasRegionOffset: atlasRegionOffset } = this; - const { x: regionX, y: regionY, width: regionW, height: regionH } = this._region; - const regionRight = 1 - regionX - regionW; - const regionBottom = 1 - regionY - regionH; - const { x: atlasRegionX, y: atlasRegionY, width: atlasRegionW, height: atlasRegionH } = this._atlasRegion; - const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = atlasRegionOffset; - const realWidth = atlasRegionW / (1 - offsetLeft - offsetRight); - const realHeight = atlasRegionH / (1 - offsetTop - offsetBottom); - // Coordinates of the four boundaries. - const left = Math.max(regionX - offsetLeft, 0) * realWidth + atlasRegionX; - const top = Math.max(regionBottom - offsetTop, 0) * realHeight + atlasRegionY; - const right = atlasRegionW + atlasRegionX - Math.max(regionRight - offsetRight, 0) * realWidth; - const bottom = atlasRegionH + atlasRegionY - Math.max(regionY - offsetBottom, 0) * realHeight; - // Top-left. - uv[0].set(left, top); - // Top-right. - uv[1].set(right, top); - // Bottom-right. - uv[2].set(right, bottom); - // Bottom-left. - uv[3].set(left, bottom); + private _calDefaultSize(): void { + if (this._texture) { + const { _texture, _atlasRegion, _atlasRegionOffset, _region } = this; + const pixelsPerUnitReciprocal = 1.0 / Engine._pixelsPerUnit; + this.width = + ((_texture.width * _atlasRegion.width) / (1 - _atlasRegionOffset.x - _atlasRegionOffset.z)) * + _region.width * + pixelsPerUnitReciprocal; + this.height = + ((_texture.height * _atlasRegion.height) / (1 - _atlasRegionOffset.y - _atlasRegionOffset.w)) * + _region.height * + pixelsPerUnitReciprocal; } - this._setDirtyFlagFalse(DirtyFlag.all); } - private _isContainDirtyFlag(type: number): boolean { - return (this._dirtyFlag & type) != 0; + private _updatePositions(): void { + const blank = this._atlasRegionOffset; + const { x: regionX, y: regionY, width: regionW, height: regionH } = this._region; + const regionRight = 1 - regionX - regionW; + const regionBottom = 1 - regionY - regionH; + const left = Math.max(blank.x - regionX, 0) / regionW; + const bottom = Math.max(blank.w - regionY, 0) / regionH; + const right = 1 - Math.max(blank.z - regionRight, 0) / regionW; + const top = 1 - Math.max(blank.y - regionBottom, 0) / regionH; + + // Update positions. + // --------------- + // 2 - 3 + // | | + // 0 - 1 + // --------------- + const positions = this._positions; + positions[0].set(left, bottom); + positions[1].set(right, bottom); + positions[2].set(left, top); + positions[3].set(right, top); + + const { min, max } = this._bounds; + min.set(left, bottom, 0); + max.set(right, top, 0); + this._dirtyFlag &= ~DirtyFlag.positions; } - private _setDirtyFlagTrue(type: number): void { - this._dirtyFlag |= type; - this._updateFlagManager.dispatch(); + private _updateUVs(): void { + const { _uvs: uv, _atlasRegionOffset: atlasRegionOffset } = this; + const { x: regionX, y: regionY, width: regionW, height: regionH } = this._region; + const regionRight = 1 - regionX - regionW; + const regionBottom = 1 - regionY - regionH; + const { x: atlasRegionX, y: atlasRegionY, width: atlasRegionW, height: atlasRegionH } = this._atlasRegion; + const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = atlasRegionOffset; + const realWidth = atlasRegionW / (1 - offsetLeft - offsetRight); + const realHeight = atlasRegionH / (1 - offsetTop - offsetBottom); + // Coordinates of the four boundaries. + const left = Math.max(regionX - offsetLeft, 0) * realWidth + atlasRegionX; + const top = Math.max(regionBottom - offsetTop, 0) * realHeight + atlasRegionY; + const right = atlasRegionW + atlasRegionX - Math.max(regionRight - offsetRight, 0) * realWidth; + const bottom = atlasRegionH + atlasRegionY - Math.max(regionY - offsetBottom, 0) * realHeight; + const { x: borderLeft, y: borderBottom, z: borderRight, w: borderTop } = this._border; + // Left-Bottom + uv[0].set(left, bottom); + // Border ( Left-Bottom ) + uv[1].set( + (regionX - offsetLeft + borderLeft * regionW) * realWidth + atlasRegionX, + atlasRegionH + atlasRegionY - (regionY - offsetBottom + borderBottom * regionH) * realHeight + ); + // Border ( Right-Top ) + uv[2].set( + atlasRegionW + atlasRegionX - (regionRight - offsetRight + borderRight * regionW) * realWidth, + (regionBottom - offsetTop + borderTop * regionH) * realHeight + atlasRegionY + ); + // Right-Top + uv[3].set(right, top); + this._dirtyFlag &= ~DirtyFlag.uvs; } - private _setDirtyFlagFalse(type: number): void { - this._dirtyFlag &= ~type; + private _dispatchSpriteChange(type: SpritePropertyDirtyFlag): void { + switch (type) { + case SpritePropertyDirtyFlag.atlasRegionOffset: + case SpritePropertyDirtyFlag.region: + this._dirtyFlag |= DirtyFlag.all; + break; + case SpritePropertyDirtyFlag.atlasRegion: + case SpritePropertyDirtyFlag.border: + this._dirtyFlag |= DirtyFlag.uvs; + break; + default: + break; + } + this._updateFlagManager.dispatch(type); } } enum DirtyFlag { positions = 0x1, - uv = 0x2, + uvs = 0x2, all = 0x3 } diff --git a/packages/core/src/2d/sprite/SpriteMask.ts b/packages/core/src/2d/sprite/SpriteMask.ts index 4ca887862b..49833f2827 100644 --- a/packages/core/src/2d/sprite/SpriteMask.ts +++ b/packages/core/src/2d/sprite/SpriteMask.ts @@ -1,13 +1,16 @@ -import { Vector3 } from "@oasis-engine/math"; -import { BoolUpdateFlag } from "../../BoolUpdateFlag"; +import { BoundingBox } from "@oasis-engine/math"; import { Camera } from "../../Camera"; -import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; +import { assignmentClone, ignoreClone } from "../../clone/CloneManager"; import { ICustomClone } from "../../clone/ComponentCloner"; import { Entity } from "../../Entity"; +import { ListenerUpdateFlag } from "../../ListenerUpdateFlag"; import { Renderer } from "../../Renderer"; import { SpriteMaskElement } from "../../RenderPipeline/SpriteMaskElement"; import { Shader } from "../../shader/Shader"; import { ShaderProperty } from "../../shader/ShaderProperty"; +import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; +import { RenderData2D } from "../data/RenderData2D"; +import { SpritePropertyDirtyFlag } from "../enums/SpriteDirtyFlag"; import { SpriteMaskLayer } from "../enums/SpriteMaskLayer"; import { Sprite } from "./Sprite"; @@ -20,40 +23,114 @@ export class SpriteMask extends Renderer implements ICustomClone { /** @internal */ static _alphaCutoffProperty: ShaderProperty = Shader.getPropertyByName("u_maskAlphaCutoff"); - private static _tempVec3: Vector3 = new Vector3(); - + /** The mask layers the sprite mask influence to. */ + @assignmentClone + influenceLayers: number = SpriteMaskLayer.Everything; /** @internal */ _maskElement: SpriteMaskElement; - @deepClone - private _positions: Vector3[] = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; - @ignoreClone - private _worldMatrixDirtyFlag: BoolUpdateFlag; + /** @internal */ + _renderData: RenderData2D; + @ignoreClone private _sprite: Sprite = null; + + @ignoreClone + private _width: number = undefined; + @ignoreClone + private _height: number = undefined; + @assignmentClone + private _flipX: boolean = false; + @assignmentClone + private _flipY: boolean = false; + @assignmentClone private _alphaCutoff: number = 0.5; + @ignoreClone - private _spriteDirty: BoolUpdateFlag; + private _dirtyFlag: number = 0; + @ignoreClone + private _spriteChangeFlag: ListenerUpdateFlag = null; - /** The mask layers the sprite mask influence to. */ - @assignmentClone - influenceLayers: number = SpriteMaskLayer.Everything; + /** + * Render width. + */ + get width(): number { + if (this._width === undefined && this._sprite) { + this.width = this._sprite.width; + } + return this._width; + } + + set width(value: number) { + if (this._width !== value) { + this._width = value; + this._dirtyFlag |= DirtyFlag.Position; + } + } /** - * The Sprite used to define the mask. + * Render height. + */ + get height(): number { + if (this._height === undefined && this._sprite) { + this.height = this._sprite.height; + } + return this._height; + } + + set height(value: number) { + if (this._height !== value) { + this._height = value; + this._dirtyFlag |= DirtyFlag.Position; + } + } + + /** + * Flips the sprite on the X axis. + */ + get flipX(): boolean { + return this._flipX; + } + + set flipX(value: boolean) { + if (this._flipX !== value) { + this._flipX = value; + this._dirtyFlag |= DirtyFlag.Position; + } + } + + /** + * Flips the sprite on the Y axis. + */ + get flipY(): boolean { + return this._flipY; + } + + set flipY(value: boolean) { + if (this._flipY !== value) { + this._flipY = value; + this._dirtyFlag |= DirtyFlag.Position; + } + } + + /** + * The Sprite to render. */ get sprite(): Sprite { return this._sprite; } - set sprite(value: Sprite) { + set sprite(value: Sprite | null) { if (this._sprite !== value) { - this._spriteDirty && this._spriteDirty.destroy(); this._sprite = value; + this._spriteChangeFlag && this._spriteChangeFlag.destroy(); if (value) { - this._spriteDirty = value._registerUpdateFlag(); + this._spriteChangeFlag = value._registerUpdateFlag(); + this._spriteChangeFlag.listener = this._onSpriteChange; + this._dirtyFlag |= DirtyFlag.All; } + this.shaderData.setTexture(SpriteMask._textureProperty, value.texture); } } @@ -76,9 +153,11 @@ export class SpriteMask extends Renderer implements ICustomClone { */ constructor(entity: Entity) { super(entity); - this._worldMatrixDirtyFlag = entity.transform.registerWorldChangeFlag(); + this._renderData = new RenderData2D(4, [], []); + SimpleSpriteAssembler.resetData(this); this.setMaterial(this._engine._spriteMaskDefaultMaterial); this.shaderData.setFloat(SpriteMask._alphaCutoffProperty, this._alphaCutoff); + this._onSpriteChange = this._onSpriteChange.bind(this); } /** @@ -86,8 +165,9 @@ export class SpriteMask extends Renderer implements ICustomClone { * @inheritdoc */ _onDestroy(): void { - this._worldMatrixDirtyFlag.destroy(); - this._spriteDirty && this._spriteDirty.destroy(); + this._sprite = null; + this._renderData = null; + this._spriteChangeFlag && this._spriteChangeFlag.destroy(); super._onDestroy(); } @@ -96,50 +176,69 @@ export class SpriteMask extends Renderer implements ICustomClone { * @inheritdoc */ _render(camera: Camera): void { - const sprite = this.sprite; - if (!sprite) { - return null; + const { sprite } = this; + if (!sprite || !sprite.texture) { + return; } - const texture = sprite.texture; - if (!texture) { - return null; + // Update position. + if (this._transformChangeFlag.flag || this._dirtyFlag & DirtyFlag.Position) { + SimpleSpriteAssembler.updatePositions(this); + this._dirtyFlag &= ~DirtyFlag.Position; + this._transformChangeFlag.flag = false; } - const positions = this._positions; - const transform = this.entity.transform; - - // Update sprite data. - sprite._updateMesh(); - - if (this._worldMatrixDirtyFlag.flag || this._spriteDirty.flag) { - const localPositions = sprite._positions; - const localVertexPos = SpriteMask._tempVec3; - const worldMatrix = transform.worldMatrix; - - for (let i = 0, n = positions.length; i < n; i++) { - const curVertexPos = localPositions[i]; - localVertexPos.set(curVertexPos.x, curVertexPos.y, 0); - Vector3.transformToVec3(localVertexPos, worldMatrix, positions[i]); - } - - this._spriteDirty.flag = false; - this._worldMatrixDirtyFlag.flag = false; + // Update uv. + if (this._dirtyFlag & DirtyFlag.UV) { + SimpleSpriteAssembler.updateUVs(this); + this._dirtyFlag &= ~DirtyFlag.UV; } - this.shaderData.setTexture(SpriteMask._textureProperty, texture); const spriteMaskElementPool = this._engine._spriteMaskElementPool; const maskElement = spriteMaskElementPool.getFromPool(); - maskElement.setValue(this, positions, sprite._uv, sprite._triangles, this.getMaterial()); - maskElement.camera = camera; - + maskElement.setValue(this, this._renderData, this.getMaterial()); camera._renderPipeline._allSpriteMasks.add(this); this._maskElement = maskElement; } + /** + * The bounding volume of the spriteRenderer. + */ + get bounds(): BoundingBox { + if (this._transformChangeFlag.flag || this._dirtyFlag & DirtyFlag.Position) { + SimpleSpriteAssembler.updatePositions(this); + this._dirtyFlag &= ~DirtyFlag.Position; + this._transformChangeFlag.flag = false; + } + return this._bounds; + } + /** * @internal */ _cloneTo(target: SpriteMask): void { target.sprite = this._sprite; } + + private _onSpriteChange(dirtyFlag: SpritePropertyDirtyFlag): void { + switch (dirtyFlag) { + case SpritePropertyDirtyFlag.texture: + this.shaderData.setTexture(SpriteMask._textureProperty, this.sprite.texture); + break; + case SpritePropertyDirtyFlag.region: + case SpritePropertyDirtyFlag.atlasRegionOffset: + this._dirtyFlag |= DirtyFlag.All; + break; + case SpritePropertyDirtyFlag.atlasRegion: + this._dirtyFlag |= DirtyFlag.UV; + break; + default: + break; + } + } +} + +enum DirtyFlag { + Position = 0x1, + UV = 0x2, + All = 0x3 } diff --git a/packages/core/src/2d/sprite/SpriteRenderer.ts b/packages/core/src/2d/sprite/SpriteRenderer.ts index d3d4aff53a..9f7901f3b4 100644 --- a/packages/core/src/2d/sprite/SpriteRenderer.ts +++ b/packages/core/src/2d/sprite/SpriteRenderer.ts @@ -1,5 +1,4 @@ -import { BoundingBox, Color, Vector3 } from "@oasis-engine/math"; -import { BoolUpdateFlag } from "../../BoolUpdateFlag"; +import { BoundingBox, Color } from "@oasis-engine/math"; import { Camera } from "../../Camera"; import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; import { ICustomClone } from "../../clone/ComponentCloner"; @@ -8,9 +7,16 @@ import { Renderer } from "../../Renderer"; import { CompareFunction } from "../../shader/enums/CompareFunction"; import { Shader } from "../../shader/Shader"; import { ShaderProperty } from "../../shader/ShaderProperty"; +import { RenderData2D } from "../data/RenderData2D"; import { SpriteMaskInteraction } from "../enums/SpriteMaskInteraction"; import { SpriteMaskLayer } from "../enums/SpriteMaskLayer"; import { Sprite } from "./Sprite"; +import { IAssembler } from "../assembler/IAssembler"; +import { SpritePropertyDirtyFlag } from "../enums/SpriteDirtyFlag"; +import { SpriteDrawMode } from "../enums/SpriteDrawMode"; +import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; +import { ListenerUpdateFlag } from "../../ListenerUpdateFlag"; +import { SlicedSpriteAssembler } from "../assembler/SlicedSpriteAssembler"; /** * Renders a Sprite for 2D graphics. @@ -19,39 +25,63 @@ export class SpriteRenderer extends Renderer implements ICustomClone { /** @internal */ static _textureProperty: ShaderProperty = Shader.getPropertyByName("u_spriteTexture"); - private static _tempVec3: Vector3 = new Vector3(); + /** @internal */ + @ignoreClone + _renderData: RenderData2D; - /** @internal temp solution. */ @ignoreClone - _customLocalBounds: BoundingBox = null; - /** @internal temp solution. */ + private _drawMode: SpriteDrawMode; @ignoreClone - _customRootEntity: Entity = null; + private _assembler: IAssembler; @deepClone - private _positions: Vector3[] = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; + private _color: Color = new Color(1, 1, 1, 1); @ignoreClone private _sprite: Sprite = null; - @deepClone - private _color: Color = new Color(1, 1, 1, 1); + + @ignoreClone + private _width: number = undefined; + @ignoreClone + private _height: number = undefined; @assignmentClone private _flipX: boolean = false; @assignmentClone private _flipY: boolean = false; + @assignmentClone - private _cacheFlipX: boolean = false; + private _maskLayer: number = SpriteMaskLayer.Layer0; @assignmentClone - private _cacheFlipY: boolean = false; + private _maskInteraction: SpriteMaskInteraction = SpriteMaskInteraction.None; + @ignoreClone private _dirtyFlag: number = 0; @ignoreClone - private _isWorldMatrixDirty: BoolUpdateFlag; - @ignoreClone - private _spriteDirty: BoolUpdateFlag; - @assignmentClone - private _maskInteraction: SpriteMaskInteraction = SpriteMaskInteraction.None; - @assignmentClone - private _maskLayer: number = SpriteMaskLayer.Layer0; + private _spriteChangeFlag: ListenerUpdateFlag = null; + + /** + * The draw mode of the sprite renderer. + */ + get drawMode(): SpriteDrawMode { + return this._drawMode; + } + + set drawMode(drawMode: SpriteDrawMode) { + if (this._drawMode !== drawMode) { + this._drawMode = drawMode; + switch (drawMode) { + case SpriteDrawMode.Simple: + this._assembler = SimpleSpriteAssembler; + break; + case SpriteDrawMode.Sliced: + this._assembler = SlicedSpriteAssembler; + break; + default: + break; + } + this._assembler.resetData(this); + this._dirtyFlag |= DirtyFlag.All; + } + } /** * The Sprite to render. @@ -62,11 +92,14 @@ export class SpriteRenderer extends Renderer implements ICustomClone { set sprite(value: Sprite | null) { if (this._sprite !== value) { - this._spriteDirty && this._spriteDirty.destroy(); this._sprite = value; + this._spriteChangeFlag && this._spriteChangeFlag.destroy(); if (value) { - this._spriteDirty = value._registerUpdateFlag(); + this._spriteChangeFlag = value._registerUpdateFlag(); + this._spriteChangeFlag.listener = this._onSpriteChange; + this._dirtyFlag |= DirtyFlag.All; } + this.shaderData.setTexture(SpriteRenderer._textureProperty, value.texture); } } @@ -83,6 +116,40 @@ export class SpriteRenderer extends Renderer implements ICustomClone { } } + /** + * Render width. + */ + get width(): number { + if (this._width === undefined && this._sprite) { + this.width = this._sprite.width; + } + return this._width; + } + + set width(value: number) { + if (this._width !== value) { + this._width = value; + this._dirtyFlag |= DirtyFlag.Position; + } + } + + /** + * Render height. + */ + get height(): number { + if (this._height === undefined && this._sprite) { + this.height = this._sprite.height; + } + return this._height; + } + + set height(value: number) { + if (this._height !== value) { + this._height = value; + this._dirtyFlag |= DirtyFlag.Position; + } + } + /** * Flips the sprite on the X axis. */ @@ -93,7 +160,7 @@ export class SpriteRenderer extends Renderer implements ICustomClone { set flipX(value: boolean) { if (this._flipX !== value) { this._flipX = value; - this._setDirtyFlagTrue(DirtyFlag.Flip); + this._dirtyFlag |= DirtyFlag.Position; } } @@ -107,22 +174,20 @@ export class SpriteRenderer extends Renderer implements ICustomClone { set flipY(value: boolean) { if (this._flipY !== value) { this._flipY = value; - this._setDirtyFlagTrue(DirtyFlag.Flip); + this._dirtyFlag |= DirtyFlag.Position; } } /** - * Interacts with the masks. + * The bounding volume of the spriteRenderer. */ - get maskInteraction(): SpriteMaskInteraction { - return this._maskInteraction; - } - - set maskInteraction(value: SpriteMaskInteraction) { - if (this._maskInteraction !== value) { - this._maskInteraction = value; - this._setDirtyFlagTrue(DirtyFlag.MaskInteraction); + get bounds(): BoundingBox { + if (this._transformChangeFlag.flag || this._dirtyFlag & DirtyFlag.Position) { + this._assembler.updatePositions(this); + this._dirtyFlag &= ~DirtyFlag.Position; + this._transformChangeFlag.flag = false; } + return this._bounds; } /** @@ -136,111 +201,58 @@ export class SpriteRenderer extends Renderer implements ICustomClone { this._maskLayer = value; } + /** + * Interacts with the masks. + */ + get maskInteraction(): SpriteMaskInteraction { + return this._maskInteraction; + } + + set maskInteraction(value: SpriteMaskInteraction) { + if (this._maskInteraction !== value) { + this._maskInteraction = value; + this._updateStencilState(); + } + } + /** * @internal */ constructor(entity: Entity) { super(entity); - this._isWorldMatrixDirty = entity.transform.registerWorldChangeFlag(); + this._renderData = new RenderData2D(4, [], [], null, this._color); + this.drawMode = SpriteDrawMode.Simple; this.setMaterial(this._engine._spriteDefaultMaterial); + this._onSpriteChange = this._onSpriteChange.bind(this); } /** * @internal */ _render(camera: Camera): void { - const { sprite } = this; - if (!sprite) { + if (!this.sprite?.texture) { return; } - const { texture } = sprite; - if (!texture) { - return; - } - - const { _positions } = this; - const { transform } = this.entity; - - // Update sprite data. - sprite._updateMesh(); - - if (this._isWorldMatrixDirty.flag || this._spriteDirty.flag) { - const localPositions = sprite._positions; - const localVertexPos = SpriteRenderer._tempVec3; - const worldMatrix = transform.worldMatrix; - const { flipX, flipY } = this; - - for (let i = 0, n = _positions.length; i < n; i++) { - const curVertexPos = localPositions[i]; - localVertexPos.set(flipX ? -curVertexPos.x : curVertexPos.x, flipY ? -curVertexPos.y : curVertexPos.y, 0); - Vector3.transformToVec3(localVertexPos, worldMatrix, _positions[i]); - } - - this._setDirtyFlagFalse(DirtyFlag.Flip); - this._isWorldMatrixDirty.flag = false; - this._spriteDirty.flag = false; - this._cacheFlipX = flipX; - this._cacheFlipY = flipY; - } else if (this._isContainDirtyFlag(DirtyFlag.Flip)) { - const { flipX, flipY } = this; - const flipXChange = this._cacheFlipX !== flipX; - const flipYChange = this._cacheFlipY !== flipY; - - if (flipXChange || flipYChange) { - const { x, y } = transform.worldPosition; - - for (let i = 0, n = _positions.length; i < n; i++) { - const curPos = _positions[i]; - - if (flipXChange) { - curPos.x = x * 2 - curPos.x; - } - if (flipYChange) { - curPos.y = y * 2 - curPos.y; - } - } - } - this._setDirtyFlagFalse(DirtyFlag.Flip); - this._cacheFlipX = flipX; - this._cacheFlipY = flipY; + // Update position. + if (this._transformChangeFlag.flag || this._dirtyFlag & DirtyFlag.Position) { + this._assembler.updatePositions(this); + this._dirtyFlag &= ~DirtyFlag.Position; + this._transformChangeFlag.flag = false; } - if (this._isContainDirtyFlag(DirtyFlag.MaskInteraction)) { - this._updateStencilState(); - this._setDirtyFlagFalse(DirtyFlag.MaskInteraction); + // Update uv. + if (this._dirtyFlag & DirtyFlag.UV) { + this._assembler.updateUVs(this); + this._dirtyFlag &= ~DirtyFlag.UV; } - this.shaderData.setTexture(SpriteRenderer._textureProperty, texture); - const material = this.getMaterial(); - - const spriteElementPool = this._engine._spriteElementPool; - const spriteElement = spriteElementPool.getFromPool(); - spriteElement.setValue(this, _positions, sprite._uv, sprite._triangles, this.color, material, camera); + // Push primitive. + const spriteElement = this._engine._spriteElementPool.getFromPool(); + spriteElement.setValue(this, this._renderData, this.getMaterial()); camera._renderPipeline.pushPrimitive(spriteElement); } - /** - * @internal - */ - _onDestroy(): void { - this._isWorldMatrixDirty.destroy(); - this._spriteDirty && this._spriteDirty.destroy(); - super._onDestroy(); - } - - private _isContainDirtyFlag(type: number): boolean { - return (this._dirtyFlag & type) != 0; - } - - private _setDirtyFlagTrue(type: number): void { - this._dirtyFlag |= type; - } - - private _setDirtyFlagFalse(type: number): void { - this._dirtyFlag &= ~type; - } - /** * @internal */ @@ -249,23 +261,15 @@ export class SpriteRenderer extends Renderer implements ICustomClone { } /** - * @override + * @internal */ - protected _updateBounds(worldBounds: BoundingBox): void { - const sprite = this._sprite; - if (sprite) { - if (this._customLocalBounds && this._customRootEntity) { - const worldMatrix = this._customRootEntity.transform.worldMatrix; - BoundingBox.transform(this._customLocalBounds, worldMatrix, worldBounds); - } else { - const localBounds = sprite.bounds; - const worldMatrix = this._entity.transform.worldMatrix; - BoundingBox.transform(localBounds, worldMatrix, worldBounds); - } - } else { - worldBounds.min.set(0, 0, 0); - worldBounds.max.set(0, 0, 0); - } + _onDestroy(): void { + this._color = null; + this._sprite = null; + this._assembler = null; + this._renderData = null; + this._spriteChangeFlag && this._spriteChangeFlag.destroy(); + super._onDestroy(); } private _updateStencilState(): void { @@ -273,7 +277,6 @@ export class SpriteRenderer extends Renderer implements ICustomClone { const material = this.getInstanceMaterial(); const stencilState = material.renderState.stencilState; const maskInteraction = this._maskInteraction; - if (maskInteraction === SpriteMaskInteraction.None) { stencilState.enabled = false; stencilState.writeMask = 0xff; @@ -291,9 +294,41 @@ export class SpriteRenderer extends Renderer implements ICustomClone { stencilState.compareFunctionBack = compare; } } + + private _onSpriteChange(dirtyFlag: SpritePropertyDirtyFlag): void { + switch (dirtyFlag) { + case SpritePropertyDirtyFlag.texture: + const { _sprite: sprite } = this; + if (this._width === undefined && this._height === undefined) { + this.width = sprite.width; + this.height = sprite.height; + } + this.shaderData.setTexture(SpriteRenderer._textureProperty, sprite.texture); + break; + case SpritePropertyDirtyFlag.size: + this._drawMode === SpriteDrawMode.Sliced && (this._dirtyFlag |= DirtyFlag.Position); + break; + case SpritePropertyDirtyFlag.border: + this._drawMode === SpriteDrawMode.Sliced && (this._dirtyFlag |= DirtyFlag.All); + break; + case SpritePropertyDirtyFlag.region: + case SpritePropertyDirtyFlag.atlasRegionOffset: + this._dirtyFlag |= DirtyFlag.All; + break; + case SpritePropertyDirtyFlag.atlasRegion: + this._dirtyFlag |= DirtyFlag.UV; + break; + case SpritePropertyDirtyFlag.pivot: + this._dirtyFlag |= DirtyFlag.Position; + break; + default: + break; + } + } } enum DirtyFlag { - Flip = 0x1, - MaskInteraction = 0x2 + Position = 0x1, + UV = 0x2, + All = 0x3 } diff --git a/packages/core/src/2d/text/CharInfo.ts b/packages/core/src/2d/text/CharInfo.ts new file mode 100644 index 0000000000..a4696fd6c2 --- /dev/null +++ b/packages/core/src/2d/text/CharInfo.ts @@ -0,0 +1,19 @@ +/** + * @internal + */ +export interface CharInfo { + x: number; + y: number; + w: number; + h: number; + offsetX: number; + offsetY: number; + xAdvance: number; + u0: number; + v0: number; + u1: number; + v1: number; + ascent: number; + descent: number; + index: number; +} diff --git a/packages/core/src/2d/text/CharRenderData.ts b/packages/core/src/2d/text/CharRenderData.ts new file mode 100644 index 0000000000..e02d562e36 --- /dev/null +++ b/packages/core/src/2d/text/CharRenderData.ts @@ -0,0 +1,21 @@ +import { Vector2, Vector3 } from "@oasis-engine/math"; +import { Texture2D } from "../../texture"; +import { RenderData2D } from "../data/RenderData2D"; + +/** + * @internal + */ +export class CharRenderData { + static triangles: number[] = [0, 2, 1, 2, 0, 3]; + + texture: Texture2D; + localPositions: Vector3[]; + renderData: RenderData2D; + + constructor() { + const positions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; + const uvs = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; + this.localPositions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; + this.renderData = new RenderData2D(4, positions, uvs, CharRenderData.triangles, null); + } +} diff --git a/packages/core/src/2d/text/CharRenderDataPool.ts b/packages/core/src/2d/text/CharRenderDataPool.ts new file mode 100644 index 0000000000..ac4e89a9d5 --- /dev/null +++ b/packages/core/src/2d/text/CharRenderDataPool.ts @@ -0,0 +1,26 @@ +/** + * @internal + */ +export class CharRenderDataPool { + private _elements: T[] = []; + private _type: new () => T; + + constructor(type: new () => T, length: number) { + this._type = type; + const elements = this._elements; + for (let i = 0; i < length; ++i) { + elements[i] = new type(); + } + } + + get(): T { + if (this._elements.length > 0) { + return this._elements.pop(); + } + return new this._type(); + } + + put(data: T): void { + this._elements.push(data); + } +} diff --git a/packages/core/src/2d/text/Font.ts b/packages/core/src/2d/text/Font.ts index f56e36b27a..fc973a465d 100644 --- a/packages/core/src/2d/text/Font.ts +++ b/packages/core/src/2d/text/Font.ts @@ -1,5 +1,8 @@ import { RefObject } from "../../asset/RefObject"; import { Engine } from "../../Engine"; +import { Texture2D } from "../../texture"; +import { CharInfo } from "./CharInfo"; +import { FontAtlas } from "../atlas/FontAtlas"; /** * Font. @@ -10,21 +13,23 @@ export class Font extends RefObject { /** * Create a font from OS. * @param engine - Engine to which the font belongs - * @param fontName - The name of font + * @param name - The name of font * @returns The font object has been create */ - static createFromOS(engine: Engine, fontName: string = ""): Font { + static createFromOS(engine: Engine, name: string = ""): Font { const fontMap = Font._fontMap; - let font = fontMap[fontName]; + let font = fontMap[name]; if (font) { return font; } - font = new Font(engine, fontName); - fontMap[fontName] = font; + font = new Font(engine, name); + fontMap[name] = font; return font; } private _name: string = ""; + private _fontAtlases: Array = []; + private _lastIndex: number = -1; /** * The name of the font object. @@ -38,8 +43,85 @@ export class Font extends RefObject { this._name = name; } + /** + * @internal + */ + _uploadCharTexture(charInfo: CharInfo, imageSource: TexImageSource | OffscreenCanvas): void { + const fontAtlases = this._fontAtlases; + let lastIndex = this._lastIndex; + if (lastIndex === -1) { + this._createFontAtlas(); + lastIndex++ + } + let fontAtlas = fontAtlases[lastIndex]; + if (!fontAtlas.uploadCharTexture(charInfo, imageSource)) { + fontAtlas = this._createFontAtlas(); + fontAtlas.uploadCharTexture(charInfo, imageSource); + lastIndex++; + } + this._lastIndex = lastIndex; + } + + /** + * @internal + */ + _addCharInfo(char: string, charInfo: CharInfo) { + const lastIndex = this._lastIndex; + charInfo.index = lastIndex; + this._fontAtlases[lastIndex].addCharInfo(char, charInfo); + } + + /** + * @internal + */ + _getCharInfo(char: string): CharInfo { + const fontAtlases = this._fontAtlases; + for (let i = 0, n = fontAtlases.length; i < n; ++i) { + const fontAtlas = fontAtlases[i]; + const charInfo = fontAtlas.getCharInfo(char); + if (charInfo) { + return charInfo; + } + } + return null; + } + + /** + * @internal + */ + _getTextureByIndex(index: number): Texture2D { + const fontAtlas = this._fontAtlases[index]; + if (fontAtlas) { + return fontAtlas.texture; + } + return null; + } + + /** + * @internal + */ + _getLastIndex(): number { + return this._lastIndex; + } + /** * @override */ - protected _onDestroy(): void {} + _onDestroy(): void { + const fontAtlases = this._fontAtlases; + for (let i = 0, n = fontAtlases.length; i < n; ++i) { + fontAtlases[i].destroy(true); + } + fontAtlases.length = 0; + delete Font._fontMap[this._name]; + } + + private _createFontAtlas(): FontAtlas { + const { engine } = this; + const fontAtlas = new FontAtlas(engine); + const texture = new Texture2D(engine, 512, 512); + fontAtlas.texture = texture; + this._fontAtlases.push(fontAtlas); + return fontAtlas; + } } diff --git a/packages/core/src/2d/text/TextRenderer.ts b/packages/core/src/2d/text/TextRenderer.ts index c292ac3ae3..0b24807043 100644 --- a/packages/core/src/2d/text/TextRenderer.ts +++ b/packages/core/src/2d/text/TextRenderer.ts @@ -1,38 +1,41 @@ import { BoundingBox, Color, Vector3 } from "@oasis-engine/math"; -import { BoolUpdateFlag } from "../../BoolUpdateFlag"; import { Camera } from "../../Camera"; import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; import { Entity } from "../../Entity"; -import { Renderer } from "../../Renderer"; -import { CompareFunction } from "../../shader/enums/CompareFunction"; -import { Texture2D } from "../../texture"; +import { CharRenderData } from "./CharRenderData"; import { FontStyle } from "../enums/FontStyle"; -import { SpriteMaskInteraction } from "../enums/SpriteMaskInteraction"; -import { SpriteMaskLayer } from "../enums/SpriteMaskLayer"; import { TextHorizontalAlignment, TextVerticalAlignment } from "../enums/TextAlignment"; import { OverflowMode } from "../enums/TextOverflow"; -import { Sprite } from "../sprite/Sprite"; -import { SpriteRenderer } from "../sprite/SpriteRenderer"; import { Font } from "./Font"; +import { Renderer } from "../../Renderer"; +import { SpriteMaskInteraction } from "../enums/SpriteMaskInteraction"; +import { SpriteMaskLayer } from "../enums/SpriteMaskLayer"; +import { CompareFunction } from "../../shader/enums/CompareFunction"; +import { ICustomClone } from "../../clone/ComponentCloner"; import { TextUtils } from "./TextUtils"; +import { CharRenderDataPool } from "./CharRenderDataPool"; +import { Engine } from "../../Engine"; +import { ListenerUpdateFlag } from "../../ListenerUpdateFlag"; /** * Renders a text for 2D graphics. */ -export class TextRenderer extends Renderer { - private static _tempVec3: Vector3 = new Vector3(); +export class TextRenderer extends Renderer implements ICustomClone { + private static _charRenderDataPool: CharRenderDataPool = new CharRenderDataPool(CharRenderData, 50); - /** @internal temp solution. */ - @ignoreClone - _customLocalBounds: BoundingBox = null; - /** @internal temp solution. */ + /** @internal */ + @assignmentClone + _charFont: Font = null; + /** @internal */ @ignoreClone - _customRootEntity: Entity = null; + _charRenderDatas: Array = []; @ignoreClone - private _sprite: Sprite = null; - @deepClone - private _positions: Vector3[] = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; + _dirtyFlag: number = DirtyFlag.Font | DirtyFlag.LocalPositionBounds | DirtyFlag.WorldPosition | DirtyFlag.WorldBounds; + /** @internal */ + @ignoreClone + _isWorldMatrixDirty: ListenerUpdateFlag; + @deepClone private _color: Color = new Color(1, 1, 1, 1); @assignmentClone @@ -41,6 +44,8 @@ export class TextRenderer extends Renderer { private _width: number = 0; @assignmentClone private _height: number = 0; + @ignoreClone + private _localBounds: BoundingBox = new BoundingBox(); @assignmentClone private _font: Font = null; @assignmentClone @@ -57,10 +62,6 @@ export class TextRenderer extends Renderer { private _enableWrapping: boolean = false; @assignmentClone private _overflowMode: OverflowMode = OverflowMode.Overflow; - @ignoreClone - private _dirtyFlag: number = DirtyFlag.Property; - @ignoreClone - private _isWorldMatrixDirty: BoolUpdateFlag; @assignmentClone private _maskInteraction: SpriteMaskInteraction = SpriteMaskInteraction.None; @assignmentClone @@ -90,7 +91,7 @@ export class TextRenderer extends Renderer { value = value || ""; if (this._text !== value) { this._text = value; - this._setDirtyFlagTrue(DirtyFlag.Property); + this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds); } } @@ -104,7 +105,7 @@ export class TextRenderer extends Renderer { set width(value: number) { if (this._width !== value) { this._width = value; - this._setDirtyFlagTrue(DirtyFlag.Property); + this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds); } } @@ -118,7 +119,7 @@ export class TextRenderer extends Renderer { set height(value: number) { if (this._height !== value) { this._height = value; - this._setDirtyFlagTrue(DirtyFlag.Property); + this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds); } } @@ -132,7 +133,7 @@ export class TextRenderer extends Renderer { set font(value: Font) { if (this._font !== value) { this._font = value; - this._setDirtyFlagTrue(DirtyFlag.Property); + this._setDirtyFlagTrue(DirtyFlag.Font); } } @@ -146,7 +147,7 @@ export class TextRenderer extends Renderer { set fontSize(value: number) { if (this._fontSize !== value) { this._fontSize = value; - this._setDirtyFlagTrue(DirtyFlag.Property); + this._setDirtyFlagTrue(DirtyFlag.Font); } } @@ -160,7 +161,7 @@ export class TextRenderer extends Renderer { set fontStyle(value: FontStyle) { if (this.fontStyle !== value) { this._fontStyle = value; - this._setDirtyFlagTrue(DirtyFlag.Property); + this._setDirtyFlagTrue(DirtyFlag.Font); } } @@ -174,7 +175,7 @@ export class TextRenderer extends Renderer { set lineSpacing(value: number) { if (this._lineSpacing !== value) { this._lineSpacing = value; - this._setDirtyFlagTrue(DirtyFlag.Property); + this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds); } } @@ -188,7 +189,7 @@ export class TextRenderer extends Renderer { set horizontalAlignment(value: TextHorizontalAlignment) { if (this._horizontalAlignment !== value) { this._horizontalAlignment = value; - this._setDirtyFlagTrue(DirtyFlag.Property); + this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds); } } @@ -202,7 +203,7 @@ export class TextRenderer extends Renderer { set verticalAlignment(value: TextVerticalAlignment) { if (this._verticalAlignment !== value) { this._verticalAlignment = value; - this._setDirtyFlagTrue(DirtyFlag.Property); + this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds); } } @@ -216,7 +217,7 @@ export class TextRenderer extends Renderer { set enableWrapping(value: boolean) { if (this._enableWrapping !== value) { this._enableWrapping = value; - this._setDirtyFlagTrue(DirtyFlag.Property); + this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds); } } @@ -230,7 +231,7 @@ export class TextRenderer extends Renderer { set overflowMode(value: OverflowMode) { if (this._overflowMode !== value) { this._overflowMode = value; - this._setDirtyFlagTrue(DirtyFlag.Property); + this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds); } } @@ -259,11 +260,29 @@ export class TextRenderer extends Renderer { this._maskLayer = value; } + /** + * The bounding volume of the TextRenderer. + */ + get bounds(): BoundingBox { + const isFontDirty = this._isContainDirtyFlag(DirtyFlag.Font); + const isLocalPositionBoundsDirty = this._isContainDirtyFlag(DirtyFlag.LocalPositionBounds); + const isWorldBoundsDirty = this._isContainDirtyFlag(DirtyFlag.WorldBounds); + if (isFontDirty || isLocalPositionBoundsDirty || isWorldBoundsDirty) { + isFontDirty && this._resetCharFont(); + isLocalPositionBoundsDirty && this._updateLocalData(); + isWorldBoundsDirty && this._updateBounds(this._bounds); + this._setDirtyFlagFalse(DirtyFlag.Font | DirtyFlag.LocalPositionBounds | DirtyFlag.WorldBounds); + } + return this._bounds; + } + constructor(entity: Entity) { super(entity); const { engine } = this; - this._isWorldMatrixDirty = entity.transform.registerWorldChangeFlag(); - this._sprite = new Sprite(engine); + this._isWorldMatrixDirty = entity.transform._registerWorldChangeListenser(); + this._isWorldMatrixDirty.listener = () => { + this._setDirtyFlagTrue(DirtyFlag.WorldPosition | DirtyFlag.WorldBounds); + }; this.font = Font.createFromOS(engine); this.setMaterial(engine._spriteDefaultMaterial); } @@ -277,47 +296,50 @@ export class TextRenderer extends Renderer { (this.enableWrapping && this.width <= 0) || (this.overflowMode === OverflowMode.Truncate && this.height <= 0) ) { - this._clearTexture(); return; } - const { _sprite: sprite } = this; - const isTextureDirty = this._isContainDirtyFlag(DirtyFlag.Property); - if (isTextureDirty) { - this._updateText(); - this._setDirtyFlagFalse(DirtyFlag.Property); + if (this._isContainDirtyFlag(DirtyFlag.MaskInteraction)) { + this._updateStencilState(); + this._setDirtyFlagFalse(DirtyFlag.MaskInteraction); } - if (this._isWorldMatrixDirty.flag || isTextureDirty) { - this._updatePosition(); - this._isWorldMatrixDirty.flag = false; + const isFontDirty = this._isContainDirtyFlag(DirtyFlag.Font); + if (isFontDirty) { + this._resetCharFont(); + this._setDirtyFlagFalse(DirtyFlag.Font); } - if (this._isContainDirtyFlag(DirtyFlag.MaskInteraction)) { - this._updateStencilState(); - this._setDirtyFlagFalse(DirtyFlag.MaskInteraction); + if (this._isContainDirtyFlag(DirtyFlag.LocalPositionBounds) || isFontDirty) { + this._updateLocalData(); + this._setDirtyFlagFalse(DirtyFlag.LocalPositionBounds); } - this.shaderData.setTexture(SpriteRenderer._textureProperty, sprite.texture); - const spriteElementPool = this._engine._spriteElementPool; - const spriteElement = spriteElementPool.getFromPool(); - spriteElement.setValue( - this, - this._positions, - sprite._uv, - sprite._triangles, - this.color, - this.getMaterial(), - camera - ); - camera._renderPipeline.pushPrimitive(spriteElement); + if (this._isContainDirtyFlag(DirtyFlag.WorldPosition) || isFontDirty) { + this._updatePosition(); + this._setDirtyFlagFalse(DirtyFlag.WorldPosition); + } + + const charRenderDatas = this._charRenderDatas; + for (let i = 0, n = charRenderDatas.length; i < n; ++i) { + const charRenderData = charRenderDatas[i]; + const spriteElement = this._engine._spriteElementPool.getFromPool(); + spriteElement.setValue(this, charRenderData.renderData, this.getMaterial(), charRenderData.texture); + camera._renderPipeline.pushPrimitive(spriteElement); + } } /** * @internal */ _onDestroy(): void { - this.engine._dynamicTextAtlasManager.removeSprite(this._sprite); + // Clear render data. + const charRenderDatas = this._charRenderDatas; + for (let i = 0, n = charRenderDatas.length; i < n; ++i) { + TextRenderer._charRenderDataPool.put(charRenderDatas[i]); + } + charRenderDatas.length = 0; + this._isWorldMatrixDirty.destroy(); super._onDestroy(); } @@ -330,88 +352,31 @@ export class TextRenderer extends Renderer { } /** - * @override + * @internal */ - protected _updateBounds(worldBounds: BoundingBox): void { - if (this._customLocalBounds && this._customRootEntity) { - const worldMatrix = this._customRootEntity.transform.worldMatrix; - BoundingBox.transform(this._customLocalBounds, worldMatrix, worldBounds); - } else { - const worldMatrix = this._entity.transform.worldMatrix; - BoundingBox.transform(this._sprite.bounds, worldMatrix, worldBounds); - } - } - - private _isContainDirtyFlag(type: number): boolean { + _isContainDirtyFlag(type: number): boolean { return (this._dirtyFlag & type) != 0; } - private _setDirtyFlagTrue(type: number): void { + /** + * @internal + */ + _setDirtyFlagTrue(type: number): void { this._dirtyFlag |= type; } - private _setDirtyFlagFalse(type: number): void { + /** + * @internal + */ + _setDirtyFlagFalse(type: number): void { this._dirtyFlag &= ~type; } - private _updateText(): void { - const { width: originWidth, height: originHeight, enableWrapping, overflowMode } = this; - const fontStr = TextUtils.getNativeFontString(this._font.name, this._fontSize, this._fontStyle); - const textMetrics = TextUtils.measureText( - this.text, - originWidth, - originHeight, - this.lineSpacing, - enableWrapping, - overflowMode, - fontStr - ); - TextUtils.updateText(textMetrics, fontStr, this.horizontalAlignment, this.verticalAlignment); - this._updateTexture(); - } - - private _updateTexture(): void { - const trimData = TextUtils.trimCanvas(); - const { width, height } = trimData; - const canvas = TextUtils.updateCanvas(width, height, trimData.data); - this._clearTexture(); - const { _sprite: sprite, horizontalAlignment, verticalAlignment } = this; - - // Handle the case that width or height of text is larger than real width or height. - const { pixelsPerUnit, pivot } = sprite; - switch (horizontalAlignment) { - case TextHorizontalAlignment.Left: - pivot.x = ((this.width * pixelsPerUnit) / width) * 0.5; - break; - case TextHorizontalAlignment.Right: - pivot.x = 1 - ((this.width * pixelsPerUnit) / width) * 0.5; - break; - case TextHorizontalAlignment.Center: - pivot.x = 0.5; - break; - } - switch (verticalAlignment) { - case TextVerticalAlignment.Top: - pivot.y = 1 - ((this.height * pixelsPerUnit) / height) * 0.5; - break; - case TextVerticalAlignment.Bottom: - pivot.y = ((this.height * pixelsPerUnit) / height) * 0.5; - break; - case TextVerticalAlignment.Center: - pivot.y = 0.5; - break; - } - sprite.pivot = pivot; - - // If add fail, set texture for sprite. - if (!this.engine._dynamicTextAtlasManager.addSprite(sprite, canvas)) { - const texture = new Texture2D(this.engine, width, height); - texture.setImageSource(canvas); - texture.generateMipmaps(); - sprite.texture = texture; - } - // Update sprite data. - sprite._updateMesh(); + /** + * @override + */ + protected _updateBounds(worldBounds: BoundingBox): void { + BoundingBox.transform(this._localBounds, this._entity.transform.worldMatrix, worldBounds); } private _updateStencilState(): void { @@ -438,30 +403,155 @@ export class TextRenderer extends Renderer { } } + private _resetCharFont(): void { + const lastCharFont = this._charFont; + if (lastCharFont) { + lastCharFont._addRefCount(-1); + lastCharFont.destroy(); + } + this._charFont = Font.createFromOS( + this.engine, + TextUtils.getNativeFontHash(this.font.name, this.fontSize, this.fontStyle) + ); + this._charFont._addRefCount(1); + } + private _updatePosition(): void { - const localPositions = this._sprite._positions; - const localVertexPos = TextRenderer._tempVec3; const worldMatrix = this.entity.transform.worldMatrix; - - const { _positions } = this; - for (let i = 0, n = _positions.length; i < n; i++) { - const curVertexPos = localPositions[i]; - localVertexPos.set(curVertexPos.x, curVertexPos.y, 0); - Vector3.transformToVec3(localVertexPos, worldMatrix, _positions[i]); + const charRenderDatas = this._charRenderDatas; + for (let i = 0, n = charRenderDatas.length; i < n; ++i) { + const { localPositions, renderData } = charRenderDatas[i]; + for (let j = 0; j < 4; ++j) { + Vector3.transformToVec3(localPositions[j], worldMatrix, renderData.positions[j]); + } } } - private _clearTexture(): void { - const { _sprite } = this; - // Remove sprite from dynamic atlas. - this.engine._dynamicTextAtlasManager.removeSprite(_sprite); - this.shaderData.setTexture(SpriteRenderer._textureProperty, null); - _sprite.atlasRegion = _sprite.region; + private _updateLocalData(): void { + const { color, horizontalAlignment, verticalAlignment, _charRenderDatas: charRenderDatas } = this; + const { min, max } = this._localBounds; + min.set(0, 0, 0); + max.set(0, 0, 0); + const { _pixelsPerUnit } = Engine; + const pixelsPerUnitReciprocal = 1.0 / _pixelsPerUnit; + const charFont = this._charFont; + const rendererWidth = this.width * _pixelsPerUnit; + const halfRendererWidth = rendererWidth * 0.5; + const rendererHeight = this.height * _pixelsPerUnit; + + const textMetrics = this.enableWrapping + ? TextUtils.measureTextWithWrap(this) + : TextUtils.measureTextWithoutWrap(this); + const { height, lines, lineWidths, lineHeight, lineMaxSizes } = textMetrics; + const charRenderDataPool = TextRenderer._charRenderDataPool; + const halfLineHeight = lineHeight * 0.5; + const linesLen = lines.length; + + let startY = 0; + const topDiff = lineHeight * 0.5 - lineMaxSizes[0].ascent; + const bottomDiff = lineHeight * 0.5 - lineMaxSizes[linesLen - 1].descent - 1; + switch (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 renderDataCount = 0; + let minX = Number.MAX_SAFE_INTEGER; + let minY = Number.MAX_SAFE_INTEGER; + let maxX = Number.MIN_SAFE_INTEGER; + let maxY = Number.MIN_SAFE_INTEGER; + let lastLineIndex = linesLen - 1; + for (let i = 0; i < linesLen; ++i) { + const line = lines[i]; + const lineWidth = lineWidths[i]; + + let startX = 0; + 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, m = line.length - 1; j <= m; ++j) { + const char = line[j]; + const charInfo = charFont._getCharInfo(char); + + if (charInfo.h > 0) { + const charRenderData = charRenderDatas[renderDataCount] || charRenderDataPool.get(); + const { renderData, localPositions } = charRenderData; + charRenderData.texture = charFont._getTextureByIndex(charInfo.index); + renderData.color = color; + + const { uvs } = renderData; + const { w, u0, v0, u1, v1, ascent, descent } = charInfo; + + const left = startX * pixelsPerUnitReciprocal; + const right = (startX + w) * pixelsPerUnitReciprocal; + const top = (startY + ascent) * pixelsPerUnitReciprocal; + const bottom = (startY - descent + 1) * pixelsPerUnitReciprocal; + // Top-left. + localPositions[0].set(left, top, 0); + uvs[0].set(u0, v0); + // Top-right. + localPositions[1].set(right, top, 0); + uvs[1].set(u1, v0); + // Bottom-right. + localPositions[2].set(right, bottom, 0); + uvs[2].set(u1, v1); + // Bottom-left. + localPositions[3].set(left, bottom, 0); + uvs[3].set(u0, v1); + + charRenderDatas[renderDataCount] = charRenderData; + renderDataCount++; + + i === 0 && (maxY = Math.max(maxY, top)); + i === lastLineIndex && (minY = Math.min(minY, bottom)); + j === 0 && (minX = Math.min(minX, left)); + j === m && (maxX = Math.max(maxX, right)); + } + startX += charInfo.xAdvance; + } + + startY -= lineHeight; + } + + min.set(minX, minY, 0); + max.set(maxX, maxY, 0); + + // Revert excess render data to pool. + const lastRenderDataCount = charRenderDatas.length; + if (lastRenderDataCount > renderDataCount) { + for (let i = renderDataCount; i < lastRenderDataCount; ++i) { + charRenderDataPool.put(charRenderDatas[i]); + } + charRenderDatas.length = renderDataCount; + } + + charFont._getLastIndex() > 0 && + charRenderDatas.sort((a, b) => { + return a.texture.instanceId - b.texture.instanceId; + }); } } -enum DirtyFlag { - Property = 0x1, - MaskInteraction = 0x2, - All = 0x3 +export enum DirtyFlag { + Font = 0x1, + LocalPositionBounds = 0x2, + WorldPosition = 0x4, + WorldBounds = 0x8, + MaskInteraction = 0x10 } diff --git a/packages/core/src/2d/text/TextUtils.ts b/packages/core/src/2d/text/TextUtils.ts index a3592aea97..0c2425c95e 100644 --- a/packages/core/src/2d/text/TextUtils.ts +++ b/packages/core/src/2d/text/TextUtils.ts @@ -1,34 +1,16 @@ -import { Vector2 } from "@oasis-engine/math"; +import { Engine } from "../../Engine"; +import { CharInfo } from "./CharInfo"; import { FontStyle } from "../enums/FontStyle"; -import { TextHorizontalAlignment, TextVerticalAlignment } from "../enums/TextAlignment"; import { OverflowMode } from "../enums/TextOverflow"; - -/** - * @internal - * TextContext. - */ -export interface TextContext { - canvas: HTMLCanvasElement | OffscreenCanvas; - context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; -} - -/** - * @internal - * TextMetrics. - */ -export interface TextMetrics { - width: number; - height: number; - lines: Array; - lineWidths: Array; - lineHeight: number; -} +import { Font } from "./Font"; +import { TextRenderer } from "./TextRenderer"; /** * @internal * TextUtils includes some helper function for text. */ export class TextUtils { + /** @internal */ static _genericFontFamilies: Array = [ "serif", "sans-serif", @@ -45,18 +27,14 @@ export class TextUtils { private static _measureBaseline: string = "M"; private static _heightMultiplier: number = 2; private static _baselineMultiplier: number = 1.4; - private static _maxWidth: number = 2048; - private static _maxHeight: number = 2048; - private static _pixelsPerUnit: number = 128; - private static _fontSizeCache: Record = {}; + private static _fontSizeInfoCache: Record = {}; private static _textContext: TextContext = null; - private static _tempVec2: Vector2 = new Vector2(); /** * The instance function to get an object includes 2d context and canvas. * @returns the TextContext object */ - public static textContext(): TextContext { + static textContext(): TextContext { let { _textContext: textContext } = TextUtils; if (!textContext) { let canvas: HTMLCanvasElement | OffscreenCanvas; @@ -74,117 +52,116 @@ export class TextUtils { /** * Measure the font. - * @param font - the string of the font - * @returns the font size + * @param fontString - the string of the font + * @returns the font size info */ - public static measureFont(font: string): number { - const { _fontSizeCache: fontSizeCache } = TextUtils; - let fontSize = fontSizeCache[font]; - if (fontSize) { - return fontSize; - } - - const { canvas, context } = TextUtils.textContext(); - context.font = font; - const measureString = TextUtils._measureString; - const width = Math.ceil(context.measureText(measureString).width); - let baseline = Math.ceil(context.measureText(TextUtils._measureBaseline).width); - const height = baseline * TextUtils._heightMultiplier; - baseline = (TextUtils._baselineMultiplier * baseline) | 0; - - canvas.width = width; - canvas.height = height; - - context.font = font; - context.fillStyle = "#000"; - context.clearRect(0, 0, width, height); - context.textBaseline = "alphabetic"; - context.fillStyle = "#f00"; - context.fillText(measureString, 0, baseline); - - const imgData = context.getImageData(0, 0, width, height).data; - const lineDataCount = width * 4; - let stop = false; - let i = 0; - let offset = 0; - - for (i = 0; i < baseline; ++i) { - offset = i * lineDataCount; - for (let j = 0; j < lineDataCount; j += 4) { - if (imgData[offset + j] !== 0) { - stop = true; - break; - } - } - if (stop) { - break; - } + static measureFont(fontString: string): FontSizeInfo { + const { _fontSizeInfoCache: fontSizeInfoCache } = TextUtils; + let info = fontSizeInfoCache[fontString]; + if (info) { + return info; } - const ascent = baseline - i; - stop = false; + info = TextUtils._measureFontOrChar(fontString); + fontSizeInfoCache[fontString] = info; + return info; + } - for (i = height - 1; i >= baseline; --i) { - offset = i * lineDataCount; - for (let j = 0; j < lineDataCount; j += 4) { - if (imgData[offset + j] !== 0) { - stop = true; - break; - } - } - if (stop) { - break; - } + /** + * Get native font string. + * @param fontName - The font name + * @param fontSize - The font size + * @param style - The font style + * @returns The native font string + */ + static getNativeFontString(fontName: string, fontSize: number, style: FontStyle): string { + let str = style & FontStyle.Bold ? "bold " : ""; + style & FontStyle.Italic && (str += "italic "); + // Check if font already contains strings + if (!/([\"\'])[^\'\"]+\1/.test(fontName) && TextUtils._genericFontFamilies.indexOf(fontName) == -1) { + fontName = `"${fontName}"`; } + str += `${fontSize}px ${fontName}`; + return str; + } - const descent = i - baseline + 1; - fontSize = ascent + descent; - fontSizeCache[font] = fontSize; - return fontSize; + static measureChar(char: string, fontString: string): CharInfo { + return TextUtils._measureFontOrChar(fontString, char); } - /** - * Measure the text. - * @param text - rendering string - * @param originWidth - the width of the TextRenderer - * @param originHeight - the height of the TextRenderer - * @param lineSpacing - the space between two lines - * @param enableWrapping - whether wrap text to next line when exceeds the width of the container - * @param overflowMode - the overflow mode - * @param fontString - the font string - * @returns the TextMetrics object - */ - public static measureText( - text: string, - originWidth: number, - originHeight: number, - lineSpacing: number, - enableWrapping: boolean, - overflowMode: OverflowMode, - fontString: string - ): TextMetrics { - const { _pixelsPerUnit } = TextUtils; - const fontSize = TextUtils.measureFont(fontString); - const context = TextUtils.textContext().context; - const lines = TextUtils._wordWrap(text, originWidth, enableWrapping, fontString); - const lineCount = lines.length; + static measureTextWithWrap(renderer: TextRenderer): TextMetrics { + const { fontSize, fontStyle } = renderer; + const { name } = renderer.font; + const fontString = TextUtils.getNativeFontString(name, fontSize, fontStyle); + const charFont = renderer._charFont; + const fontSizeInfo = TextUtils.measureFont(fontString); + const subTexts = renderer.text.split(/(?:\r\n|\r|\n)/); + const lines = new Array(); const lineWidths = new Array(); - const lineHeight = fontSize + lineSpacing * _pixelsPerUnit; - context.font = fontString; - // Calculate max width of all lines. + const lineMaxSizes = new Array(); + const { _pixelsPerUnit } = Engine; + const lineHeight = fontSizeInfo.size + renderer.lineSpacing * _pixelsPerUnit; + const wrapWidth = renderer.width * _pixelsPerUnit; let width = 0; - for (let i = 0; i < lineCount; ++i) { - const lineWidth = Math.ceil(context.measureText(lines[i]).width); - if (lineWidth > width) { - width = lineWidth; + + for (let i = 0, n = subTexts.length; i < n; ++i) { + const subText = subTexts[i]; + let chars = ""; + let charsWidth = 0; + let maxAscent = -1; + let maxDescent = -1; + + for (let j = 0, m = subText.length; j < m; ++j) { + const char = subText[j]; + const charInfo = TextUtils._getCharInfo(char, fontString, charFont); + const { w, offsetY } = charInfo; + const halfH = charInfo.h * 0.5; + const ascent = halfH + offsetY; + const descent = halfH - offsetY; + if (charsWidth + w > wrapWidth) { + if (charsWidth === 0) { + lines.push(char); + lineWidths.push(w); + lineMaxSizes.push({ + ascent, + descent, + size: ascent + descent + }); + } else { + lines.push(chars); + lineWidths.push(charsWidth); + lineMaxSizes.push({ + ascent: maxAscent, + descent: maxDescent, + size: maxAscent + maxDescent + }); + chars = char; + charsWidth = charInfo.xAdvance; + maxAscent = ascent; + maxDescent = descent; + } + } else { + chars += char; + charsWidth += charInfo.xAdvance; + maxAscent < ascent && (maxAscent = ascent); + maxDescent < descent && (maxDescent = descent); + } + } + + if (charsWidth > 0) { + lines.push(chars); + lineWidths.push(charsWidth); + lineMaxSizes.push({ + ascent: maxAscent, + descent: maxDescent, + size: maxAscent + maxDescent + }); } - lineWidths.push(lineWidth); } - // Reset width and height. - let height = originHeight * _pixelsPerUnit; - if (overflowMode === OverflowMode.Overflow) { - height = Math.min(lineHeight * lineCount, TextUtils._maxHeight); + let height = renderer.height * _pixelsPerUnit; + if (renderer.overflowMode === OverflowMode.Overflow) { + height = lineHeight * lines.length; } return { @@ -192,139 +169,84 @@ export class TextUtils { height, lines, lineWidths, - lineHeight + lineHeight, + lineMaxSizes }; } - /** - * Trim canvas. - * @returns the width and height after trim, and the image data - */ - public static trimCanvas(): { width: number; height: number; data?: ImageData } { - // https://gist.github.com/remy/784508 - - const { canvas, context } = TextUtils.textContext(); - let { width, height } = canvas; - - const imageData = context.getImageData(0, 0, width, height).data; - const len = imageData.length; - - let top = -1; - let bottom = -1; - let left = width; - let right = -1; - let data = null; - let x; - let y; - - for (let i = 0; i < len; i += 4) { - if (imageData[i + 3] !== 0) { - const idx = i / 4; - x = idx % width; - y = ~~(idx / width); - - if (top === -1) { - top = y; - } - - if (x < left) { - left = x; - } - - if (x > right) { - right = x; - } - - if (y > bottom) { - bottom = y; - } - } + static measureTextWithoutWrap(renderer: TextRenderer): TextMetrics { + const { fontSize, fontStyle } = renderer; + const { name } = renderer.font; + const fontString = TextUtils.getNativeFontString(name, fontSize, fontStyle); + const charFont = renderer._charFont; + const fontSizeInfo = TextUtils.measureFont(fontString); + const lines = renderer.text.split(/(?:\r\n|\r|\n)/); + const lineCount = lines.length; + const lineWidths = new Array(); + const lineMaxSizes = new Array(); + const { _pixelsPerUnit } = Engine; + const lineHeight = fontSizeInfo.size + renderer.lineSpacing * _pixelsPerUnit; + let width = 0; + let height = renderer.height * _pixelsPerUnit; + if (renderer.overflowMode === OverflowMode.Overflow) { + height = lineHeight * lineCount; } - if (top !== -1) { - top = Math.max(0, top - 1); - bottom = Math.min(height - 1, bottom + 1); - left = Math.max(0, left - 1); - right = Math.min(width - 1, right + 1); - width = right - left + 1; - height = bottom - top + 1; - data = context.getImageData(left, top, width, height); + for (let i = 0; i < lineCount; ++i) { + const line = lines[i]; + let curWidth = 0; + let maxAscent = -1; + let maxDescent = -1; + + for (let j = 0, m = line.length; j < m; ++j) { + const charInfo = TextUtils._getCharInfo(line[j], fontString, charFont); + curWidth += charInfo.xAdvance; + const { offsetY } = charInfo; + const halfH = charInfo.h * 0.5; + const ascent = halfH + offsetY; + const descent = halfH - offsetY; + maxAscent < ascent && (maxAscent = ascent); + maxDescent < descent && (maxDescent = descent); + } + lineWidths[i] = curWidth; + lineMaxSizes[i] = { + ascent: maxAscent, + descent: maxDescent, + size: maxAscent + maxDescent + }; + if (curWidth > width) { + width = curWidth; + } } return { width, height, - data + lines, + lineWidths, + lineHeight, + lineMaxSizes }; } /** - * Get native font string. + * Get native font hash. * @param fontName - The font name * @param fontSize - The font size * @param style - The font style - * @returns The native font string + * @returns The native font hash */ - public static getNativeFontString(fontName: string, fontSize: number, style: FontStyle): string { - let str = style & FontStyle.Bold ? "bold " : ""; - style & FontStyle.Italic && (str += "italic "); + static getNativeFontHash(fontName: string, fontSize: number, style: FontStyle): string { + let str = style & FontStyle.Bold ? "bold" : ""; + style & FontStyle.Italic && (str += "italic"); // Check if font already contains strings if (!/([\"\'])[^\'\"]+\1/.test(fontName) && TextUtils._genericFontFamilies.indexOf(fontName) == -1) { - fontName = `"${fontName}"`; + fontName = `${fontName}`; } - str += `${fontSize}px ${fontName}`; + str += `${fontSize}px${fontName}`; return str; } - /** - * Update text. - * @param textMetrics - the text metrics object - * @param fontStr - the font string - * @param horizontalAlignment - the horizontal alignment - * @param verticalAlignment - the vertical alignment - */ - public static updateText( - textMetrics: TextMetrics, - fontStr: string, - horizontalAlignment: TextHorizontalAlignment, - verticalAlignment: TextVerticalAlignment - ): void { - const { canvas, context } = TextUtils.textContext(); - const { width, height } = textMetrics; - // reset canvas's width and height. - canvas.width = width; - canvas.height = height; - // clear canvas. - context.font = fontStr; - context.clearRect(0, 0, width, height); - // set canvas font info. - context.textBaseline = "middle"; - context.fillStyle = "#fff"; - - // draw lines. - const { lines, lineHeight, lineWidths } = textMetrics; - const halfLineHeight = lineHeight * 0.5; - for (let i = 0, l = lines.length; i < l; ++i) { - const lineWidth = lineWidths[i]; - const pos = TextUtils._tempVec2; - TextUtils._calculateLinePosition( - width, - height, - lineWidth, - lineHeight, - i, - l, - horizontalAlignment, - verticalAlignment, - pos - ); - const { x, y } = pos; - if (y + lineHeight >= 0 && y < height) { - context.fillText(lines[i], x, y + halfLineHeight); - } - } - } - /** * Update canvas with the data. * @param width - the new width of canvas @@ -332,7 +254,7 @@ export class TextUtils { * @param data - the new data of canvas * @returns the canvas after update */ - public static updateCanvas(width: number, height: number, data: ImageData): HTMLCanvasElement | OffscreenCanvas { + static updateCanvas(width: number, height: number, data: ImageData): HTMLCanvasElement | OffscreenCanvas { const { canvas, context } = TextUtils.textContext(); canvas.width = width; canvas.height = height; @@ -340,87 +262,124 @@ export class TextUtils { return canvas; } - private static _wordWrap(text: string, width: number, enableWrapping: boolean, fontString: string): Array { - const { context } = TextUtils.textContext(); - const { _maxWidth: maxWidth } = TextUtils; - const widthInPixel = width * TextUtils._pixelsPerUnit; - const wrapWidth = Math.min(widthInPixel, maxWidth); - const wrappedSubTexts = new Array(); - const subTexts = text.split(/(?:\r\n|\r|\n)/); + private static _measureFontOrChar(fontString: string, char: string = ""): FontSizeInfo | CharInfo { + const { canvas, context } = TextUtils.textContext(); context.font = fontString; + const measureString = char || TextUtils._measureString; + const width = context.measureText(measureString).width; + let baseline = Math.ceil(context.measureText(TextUtils._measureBaseline).width); + const height = baseline * TextUtils._heightMultiplier; + baseline = (TextUtils._baselineMultiplier * baseline) | 0; - for (let i = 0, n = subTexts.length; i < n; ++i) { - const subText = subTexts[i]; - const subWidth = Math.ceil(context.measureText(subText).width); - const needWrap = enableWrapping || subWidth > maxWidth; - if (needWrap) { - if (subWidth <= wrapWidth) { - wrappedSubTexts.push(subText); - } else { - let chars = ""; - let charsWidth = 0; - for (let j = 0, m = subText.length; j < m; ++j) { - const char = subText[j]; - const charWidth = Math.ceil(context.measureText(char).width); - if (charsWidth + charWidth > wrapWidth) { - // The width of text renderer is shorter than current char. - if (charsWidth === 0) { - wrappedSubTexts.push(char); - } else { - wrappedSubTexts.push(chars); - chars = char; - charsWidth = charWidth; - } - } else { - chars += char; - charsWidth += charWidth; - } - } - if (charsWidth > 0) { - wrappedSubTexts.push(chars); - } + canvas.width = width; + canvas.height = height; + + context.font = fontString; + context.fillStyle = "#000"; + context.clearRect(0, 0, width, height); + context.textBaseline = "middle"; + context.fillStyle = "#fff"; + context.fillText(measureString, 0, baseline); + + const imageData = context.getImageData(0, 0, width, height).data; + const len = imageData.length; + + let top = -1; + let bottom = -1; + let y; + let ascent = 0; + let descent = 0; + let size = 0; + + const integerW = canvas.width; + for (let i = 0; i < len; i += 4) { + if (imageData[i + 3] !== 0) { + const idx = i / 4; + y = ~~(idx / integerW); + + if (top === -1) { + top = y; + } + + if (y > bottom) { + bottom = y; } - } else { - wrappedSubTexts.push(subText); } } - return wrappedSubTexts; - } + if (top !== -1 && bottom !== -1) { + ascent = baseline - top; + descent = bottom - baseline + 1; + size = ascent + descent; + } + const sizeInfo = { ascent, descent, size }; - private static _calculateLinePosition( - width: number, - height: number, - lineWidth: number, - lineHeight: number, - index: number, - length: number, - horizontalAlignment: TextHorizontalAlignment, - verticalAlignment: TextVerticalAlignment, - out: Vector2 - ): void { - switch (verticalAlignment) { - case TextVerticalAlignment.Top: - out.y = index * lineHeight; - break; - case TextVerticalAlignment.Bottom: - out.y = height - (length - index) * lineHeight; - break; - default: - out.y = 0.5 * height - 0.5 * length * lineHeight + index * lineHeight; - break; + if (char) { + if (size > 0) { + const data = context.getImageData(0, top, width, size); + TextUtils.updateCanvas(width, size, data); + } + return { + x: 0, + y: 0, + w: width, + h: size, + offsetX: 0, + offsetY: (ascent - descent) * 0.5, + xAdvance: width, + u0: 0, + v0: 0, + u1: 0, + v1: 0, + ascent, + descent, + index: 0 + }; + } else { + return sizeInfo; } + } - switch (horizontalAlignment) { - case TextHorizontalAlignment.Left: - out.x = 0; - break; - case TextHorizontalAlignment.Right: - out.x = width - lineWidth; - break; - default: - out.x = (width - lineWidth) * 0.5; - break; + private static _getCharInfo(char: string, fontString: string, font: Font): CharInfo { + let charInfo = font._getCharInfo(char); + if (!charInfo) { + charInfo = TextUtils.measureChar(char, fontString); + font._uploadCharTexture(charInfo, TextUtils.textContext().canvas); + font._addCharInfo(char, charInfo); } + + return charInfo; } } + +/** + * @internal + * TextContext. + */ +export interface TextContext { + canvas: HTMLCanvasElement | OffscreenCanvas; + context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; +} + +/** + * @internal + * FontSizeInfo. + */ +export interface FontSizeInfo { + ascent: number; + descent: number; + size: number; +} + +/** + * @internal + * TextMetrics. + */ +export interface TextMetrics { + width: number; + height: number; + lines: Array; + lineWidths: Array; + lineHeight: number; + lineMaxSizes?: Array; +} diff --git a/packages/core/src/Camera.ts b/packages/core/src/Camera.ts index 0b680020ab..1ad0840081 100644 --- a/packages/core/src/Camera.ts +++ b/packages/core/src/Camera.ts @@ -47,9 +47,9 @@ export class Camera extends Component { /** * Determining what to clear when rendering by a Camera. - * @defaultValue `CameraClearFlags.DepthColor` + * @defaultValue `CameraClearFlags.All` */ - clearFlags: CameraClearFlags = CameraClearFlags.DepthColor; + clearFlags: CameraClearFlags = CameraClearFlags.All; /** * Culling mask - which layers the camera renders. diff --git a/packages/core/src/Engine.ts b/packages/core/src/Engine.ts index 4d072611e8..9102f14c1f 100644 --- a/packages/core/src/Engine.ts +++ b/packages/core/src/Engine.ts @@ -1,5 +1,4 @@ import { ColorSpace } from "."; -import { DynamicTextAtlasManager } from "./2d/dynamic-atlas/DynamicTextAtlasManager"; import { ResourceManager } from "./asset/ResourceManager"; import { Event, EventDispatcher, Logger, Time } from "./base"; import { Canvas } from "./Canvas"; @@ -44,6 +43,8 @@ ShaderPool.init(); export class Engine extends EventDispatcher { /** @internal */ static _gammaMacro: ShaderMacro = Shader.getMacroByName("OASIS_COLORSPACE_GAMMA"); + /** @internal Conversion of space units to pixel units for 2D. */ + static _pixelsPerUnit: number = 100; /** Physics manager of Engine. */ readonly physicsManager: PhysicsManager; @@ -75,8 +76,6 @@ export class Engine extends EventDispatcher { _spriteMaskManager: SpriteMaskManager; /** @internal */ _macroCollection: ShaderMacroCollection = new ShaderMacroCollection(); - /** @internal */ - _dynamicTextAtlasManager: DynamicTextAtlasManager = new DynamicTextAtlasManager(this); protected _canvas: Canvas; @@ -280,9 +279,7 @@ export class Engine extends EventDispatcher { scene._activeCameras.sort((camera1, camera2) => camera1.priority - camera2.priority); componentsManager.callScriptOnStart(); - if (this.physicsManager._initialized) { - this.physicsManager._update(deltaTime / 1000.0); - } + this.physicsManager._initialized && this.physicsManager._update(deltaTime / 1000.0); this.inputManager._update(); componentsManager.callScriptOnUpdate(deltaTime); componentsManager.callAnimationUpdate(deltaTime); diff --git a/packages/core/src/RenderPipeline/Basic2DBatcher.ts b/packages/core/src/RenderPipeline/Basic2DBatcher.ts index c9811d00ec..ff76a839e0 100644 --- a/packages/core/src/RenderPipeline/Basic2DBatcher.ts +++ b/packages/core/src/RenderPipeline/Basic2DBatcher.ts @@ -1,3 +1,4 @@ +import { Camera } from "../Camera"; import { Engine } from "../Engine"; import { Buffer, BufferBindFlag, BufferUsage, IndexFormat, MeshTopology, SubMesh, VertexElement } from "../graphic"; import { BufferMesh } from "../mesh"; @@ -13,6 +14,8 @@ export abstract class Basic2DBatcher { static MAX_VERTEX_COUNT: number = 4096; static _canUploadSameBuffer: boolean = true; + /** @internal */ + _engine: Engine; /** @internal */ _subMeshPool: ClassPool = new ClassPool(SubMesh); /** @internal */ @@ -37,6 +40,8 @@ export abstract class Basic2DBatcher { _elementCount: number = 0; constructor(engine: Engine) { + this._engine = engine; + const { MAX_VERTEX_COUNT } = Basic2DBatcher; this._vertices = new Float32Array(MAX_VERTEX_COUNT * 9); this._indices = new Uint16Array(MAX_VERTEX_COUNT * 3); @@ -47,25 +52,24 @@ export abstract class Basic2DBatcher { } } - drawElement(element: Element): void { - const len = element.positions.length; + drawElement(element: Element, camera: Camera): void { + const len = element.renderData.vertexCount; if (this._vertexCount + len > Basic2DBatcher.MAX_VERTEX_COUNT) { - this.flush(element.camera.engine); + this.flush(camera); } this._vertexCount += len; this._batchedQueue[this._elementCount++] = element; } - flush(engine: Engine): void { + flush(camera: Camera): void { const batchedQueue = this._batchedQueue; if (batchedQueue.length === 0) { return; } - - this._updateData(engine); - this.drawBatches(engine); + this._updateData(this._engine); + this.drawBatches(camera); if (!Basic2DBatcher._canUploadSameBuffer) { this._flushId++; @@ -159,13 +163,13 @@ export abstract class Basic2DBatcher { vertexIndex = this.updateVertices(curElement, vertices, vertexIndex); // Batch indice - const { triangles } = curElement; + const { triangles } = curElement.renderData; const triangleNum = triangles.length; for (let j = 0; j < triangleNum; j++) { indices[indiceIndex++] = triangles[j] + curIndiceStartIndex; } - curIndiceStartIndex += curElement.positions.length; + curIndiceStartIndex += curElement.renderData.vertexCount; if (preElement === null) { vertexCount += triangleNum; @@ -216,5 +220,5 @@ export abstract class Basic2DBatcher { /** * @internal */ - abstract drawBatches(engine: Engine): void; + abstract drawBatches(camera: Camera): void; } diff --git a/packages/core/src/RenderPipeline/BasicRenderPipeline.ts b/packages/core/src/RenderPipeline/BasicRenderPipeline.ts index ad87cb5eb2..e48a7ad4d2 100644 --- a/packages/core/src/RenderPipeline/BasicRenderPipeline.ts +++ b/packages/core/src/RenderPipeline/BasicRenderPipeline.ts @@ -180,7 +180,7 @@ export class BasicRenderPipeline { } else { this._opaqueQueue.render(camera, pass.replaceMaterial, pass.mask); this._alphaTestQueue.render(camera, pass.replaceMaterial, pass.mask); - if (camera.clearFlags === CameraClearFlags.DepthColor) { + if (camera.clearFlags & CameraClearFlags.Color) { if (background.mode === BackgroundMode.Sky) { this._drawSky(engine, camera, background.sky); } else if (background.mode === BackgroundMode.Texture && background.texture) { diff --git a/packages/core/src/RenderPipeline/RenderQueue.ts b/packages/core/src/RenderPipeline/RenderQueue.ts index 94ec986093..d432130d71 100644 --- a/packages/core/src/RenderPipeline/RenderQueue.ts +++ b/packages/core/src/RenderPipeline/RenderQueue.ts @@ -69,7 +69,7 @@ export class RenderQueue { } if (!!(item as RenderElement).mesh) { - this._spriteBatcher.flush(engine); + this._spriteBatcher.flush(camera); const compileMacros = Shader._compileMacros; const element = item; @@ -135,16 +135,16 @@ export class RenderQueue { program.uploadUnGroupTextures(); } } - material.renderState._apply(camera.engine, renderer.entity.transform._isFrontFaceInvert()); + material.renderState._apply(engine, renderer.entity.transform._isFrontFaceInvert()); rhi.drawPrimitive(element.mesh, element.subMesh, program); } else { const spriteElement = item; - this._spriteBatcher.drawElement(spriteElement); + this._spriteBatcher.drawElement(spriteElement, camera); } } - this._spriteBatcher.flush(engine); + this._spriteBatcher.flush(camera); } /** diff --git a/packages/core/src/RenderPipeline/SpriteBatcher.ts b/packages/core/src/RenderPipeline/SpriteBatcher.ts index c382bfbca0..1f3cbd311c 100644 --- a/packages/core/src/RenderPipeline/SpriteBatcher.ts +++ b/packages/core/src/RenderPipeline/SpriteBatcher.ts @@ -1,5 +1,6 @@ import { SpriteMaskInteraction } from "../2d/enums/SpriteMaskInteraction"; import { SpriteRenderer } from "../2d/sprite/SpriteRenderer"; +import { Camera } from "../Camera"; import { Engine } from "../Engine"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; import { VertexElement } from "../graphic/VertexElement"; @@ -31,9 +32,8 @@ export class SpriteBatcher extends Basic2DBatcher { return false; } - // Compare renderer property - const textureProperty = SpriteBatcher._textureProperty; - if (preRenderer.shaderData.getTexture(textureProperty) !== curRenderer.shaderData.getTexture(textureProperty)) { + // Compare texture + if (preElement.texture !== curElement.texture) { return false; } @@ -54,12 +54,10 @@ export class SpriteBatcher extends Basic2DBatcher { } updateVertices(element: SpriteElement, vertices: Float32Array, vertexIndex: number): number { - const { positions, uv, color } = element; - const verticesNum = positions.length; - for (let i = 0; i < verticesNum; i++) { + const { positions, uvs, color, vertexCount } = element.renderData; + for (let i = 0; i < vertexCount; i++) { const curPos = positions[i]; - const curUV = uv[i]; - + const curUV = uvs[i]; vertices[vertexIndex++] = curPos.x; vertices[vertexIndex++] = curPos.y; vertices[vertexIndex++] = curPos.z; @@ -74,11 +72,13 @@ export class SpriteBatcher extends Basic2DBatcher { return vertexIndex; } - drawBatches(engine: Engine): void { + drawBatches(camera: Camera): void { + const { _engine: engine, _batchedQueue: batchedQueue } = this; const mesh = this._meshes[this._flushId]; const subMeshes = mesh.subMeshes; - const batchedQueue = this._batchedQueue; const maskManager = engine._spriteMaskManager; + const sceneData = camera.scene.shaderData; + const cameraData = camera.shaderData; for (let i = 0, len = subMeshes.length; i < len; i++) { const subMesh = subMeshes[i]; @@ -89,7 +89,6 @@ export class SpriteBatcher extends Basic2DBatcher { } const renderer = spriteElement.component; - const camera = spriteElement.camera; const material = spriteElement.material; maskManager.preRender(camera, renderer); @@ -106,14 +105,16 @@ export class SpriteBatcher extends Basic2DBatcher { return; } + renderer.shaderData.setTexture(SpriteBatcher._textureProperty, spriteElement.texture); + program.bind(); program.groupingOtherUniformBlock(); - program.uploadAll(program.sceneUniformBlock, camera.scene.shaderData); - program.uploadAll(program.cameraUniformBlock, camera.shaderData); + program.uploadAll(program.sceneUniformBlock, sceneData); + program.uploadAll(program.cameraUniformBlock, cameraData); program.uploadAll(program.rendererUniformBlock, renderer.shaderData); program.uploadAll(program.materialUniformBlock, material.shaderData); - material.renderState._apply(engine,false); + material.renderState._apply(engine, false); engine._hardwareRenderer.drawPrimitive(mesh, subMesh, program); diff --git a/packages/core/src/RenderPipeline/SpriteElement.ts b/packages/core/src/RenderPipeline/SpriteElement.ts index 133770318b..953a81f20b 100644 --- a/packages/core/src/RenderPipeline/SpriteElement.ts +++ b/packages/core/src/RenderPipeline/SpriteElement.ts @@ -1,32 +1,18 @@ -import { Color, Vector2, Vector3 } from "@oasis-engine/math"; -import { Camera } from "../Camera"; +import { RenderData2D } from "../2d/data/RenderData2D"; import { Material } from "../material/Material"; import { Renderer } from "../Renderer"; +import { Texture2D } from "../texture"; export class SpriteElement { component: Renderer; - positions: Vector3[]; - uv: Vector2[]; - triangles: number[]; - color: Color; + renderData: RenderData2D; material: Material; - camera: Camera; + texture: Texture2D; - setValue( - component: Renderer, - positions: Vector3[], - uv: Vector2[], - triangles: number[], - color: Color, - material: Material, - camera: Camera - ): void { + setValue(component: Renderer, renderDate: RenderData2D, material: Material, texture: Texture2D = null): void { this.component = component; - this.positions = positions; - this.uv = uv; - this.triangles = triangles; - this.color = color; + this.renderData = renderDate; this.material = material; - this.camera = camera; + this.texture = texture; } } diff --git a/packages/core/src/RenderPipeline/SpriteMaskBatcher.ts b/packages/core/src/RenderPipeline/SpriteMaskBatcher.ts index 4236e6533c..254e7bf1a7 100644 --- a/packages/core/src/RenderPipeline/SpriteMaskBatcher.ts +++ b/packages/core/src/RenderPipeline/SpriteMaskBatcher.ts @@ -1,4 +1,5 @@ import { SpriteMask } from "../2d/sprite/SpriteMask"; +import { Camera } from "../Camera"; import { Engine } from "../Engine"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; import { VertexElement } from "../graphic/VertexElement"; @@ -33,12 +34,10 @@ export class SpriteMaskBatcher extends Basic2DBatcher { } updateVertices(element: SpriteMaskElement, vertices: Float32Array, vertexIndex: number): number { - const { positions, uv } = element; - const verticesNum = positions.length; - for (let i = 0; i < verticesNum; i++) { + const { positions, uvs, vertexCount } = element.renderData; + for (let i = 0; i < vertexCount; i++) { const curPos = positions[i]; - const curUV = uv[i]; - + const curUV = uvs[i]; vertices[vertexIndex++] = curPos.x; vertices[vertexIndex++] = curPos.y; vertices[vertexIndex++] = curPos.z; @@ -49,10 +48,12 @@ export class SpriteMaskBatcher extends Basic2DBatcher { return vertexIndex; } - drawBatches(engine: Engine): void { + drawBatches(camera: Camera): void { + const { _engine: engine, _batchedQueue: batchedQueue } = this; const mesh = this._meshes[this._flushId]; const subMeshes = mesh.subMeshes; - const batchedQueue = this._batchedQueue; + const sceneData = camera.scene.shaderData; + const cameraData = camera.shaderData; for (let i = 0, len = subMeshes.length; i < len; i++) { const subMesh = subMeshes[i]; @@ -84,12 +85,10 @@ export class SpriteMaskBatcher extends Basic2DBatcher { return; } - const camera = spriteMaskElement.camera; - program.bind(); program.groupingOtherUniformBlock(); - program.uploadAll(program.sceneUniformBlock, camera.scene.shaderData); - program.uploadAll(program.cameraUniformBlock, camera.shaderData); + program.uploadAll(program.sceneUniformBlock, sceneData); + program.uploadAll(program.cameraUniformBlock, cameraData); program.uploadAll(program.rendererUniformBlock, renderer.shaderData); program.uploadAll(program.materialUniformBlock, material.shaderData); diff --git a/packages/core/src/RenderPipeline/SpriteMaskElement.ts b/packages/core/src/RenderPipeline/SpriteMaskElement.ts index 4f83015466..1f6ec69300 100644 --- a/packages/core/src/RenderPipeline/SpriteMaskElement.ts +++ b/packages/core/src/RenderPipeline/SpriteMaskElement.ts @@ -1,22 +1,18 @@ import { Vector2, Vector3 } from "@oasis-engine/math"; +import { RenderData2D } from "../2d/data/RenderData2D"; import { Camera } from "../Camera"; import { Component } from "../Component"; import { Material } from "../material/Material"; export class SpriteMaskElement { component: Component; - positions: Vector3[]; - uv: Vector2[]; - triangles: number[]; + renderData: RenderData2D; material: Material; isAdd: boolean = true; - camera: Camera; - setValue(component: Component, positions: Vector3[], uv: Vector2[], triangles: number[], material: Material): void { + setValue(component: Component, renderData: RenderData2D, material: Material): void { this.component = component; - this.positions = positions; - this.uv = uv; - this.triangles = triangles; + this.renderData = renderData; this.material = material; } } diff --git a/packages/core/src/RenderPipeline/SpriteMaskManager.ts b/packages/core/src/RenderPipeline/SpriteMaskManager.ts index a1631c4101..8117b7c712 100644 --- a/packages/core/src/RenderPipeline/SpriteMaskManager.ts +++ b/packages/core/src/RenderPipeline/SpriteMaskManager.ts @@ -28,7 +28,7 @@ export class SpriteMaskManager { this._batcher.clear(); this._processMasksDiff(camera, renderer); - this._batcher.flush(camera.engine); + this._batcher.flush(camera); } postRender(renderer: SpriteRenderer): void { @@ -65,14 +65,14 @@ export class SpriteMaskManager { if (influenceLayers & addLayer) { const maskRenderElement = mask._maskElement; maskRenderElement.isAdd = true; - this._batcher.drawElement(maskRenderElement); + this._batcher.drawElement(maskRenderElement, camera); continue; } if (influenceLayers & reduceLayer) { const maskRenderElement = mask._maskElement; maskRenderElement.isAdd = false; - this._batcher.drawElement(maskRenderElement); + this._batcher.drawElement(maskRenderElement, camera); } } } diff --git a/packages/core/src/Renderer.ts b/packages/core/src/Renderer.ts index 051d1759fa..e5418242fa 100644 --- a/packages/core/src/Renderer.ts +++ b/packages/core/src/Renderer.ts @@ -44,16 +44,18 @@ export class Renderer extends Component { /** @internal */ @ignoreClone _globalShaderMacro: ShaderMacroCollection = new ShaderMacroCollection(); + /** @internal */ + @ignoreClone + _transformChangeFlag: BoolUpdateFlag; + /** @internal */ + @deepClone + _bounds: BoundingBox = new BoundingBox(); @ignoreClone protected _overrideUpdate: boolean = false; @shallowClone protected _materials: Material[] = []; - @ignoreClone - private _transformChangeFlag: BoolUpdateFlag; - @deepClone - private _bounds: BoundingBox = new BoundingBox(new Vector3(), new Vector3()); @ignoreClone private _mvMatrix: Matrix = new Matrix(); @ignoreClone diff --git a/packages/core/src/Transform.ts b/packages/core/src/Transform.ts index eb1732740b..8126d9e507 100644 --- a/packages/core/src/Transform.ts +++ b/packages/core/src/Transform.ts @@ -3,6 +3,7 @@ import { BoolUpdateFlag } from "./BoolUpdateFlag"; import { deepClone, ignoreClone } from "./clone/CloneManager"; import { Component } from "./Component"; import { Entity } from "./Entity"; +import { ListenerUpdateFlag } from "./ListenerUpdateFlag"; import { UpdateFlagManager } from "./UpdateFlagManager"; /** @@ -429,7 +430,7 @@ export class Transform extends Component { /** * Translate in the direction and distance of the translation. * @param translation - Direction and distance of translation - * @param relativeToLocal - Is relative to the local coordinate system + * @param relativeToLocal = `true` - Is relative to the local coordinate system */ translate(translation: Vector3, relativeToLocal?: boolean): void; @@ -438,7 +439,7 @@ export class Transform extends Component { * @param x - Distance along the x axis * @param y - Distance along the y axis * @param z - Distance along the z axis - * @param relativeToLocal - Is relative to the local coordinate system + * @param relativeToLocal = `true` - Is relative to the local coordinate system */ translate(x: number, y: number, z: number, relativeToLocal?: boolean): void; @@ -460,7 +461,7 @@ export class Transform extends Component { /** * Rotate around the passed Vector3. * @param rotation - Euler angle in degrees - * @param relativeToLocal - Is relative to the local coordinate system + * @param relativeToLocal = `true` - Is relative to the local coordinate system */ rotate(rotation: Vector3, relativeToLocal?: boolean): void; @@ -469,7 +470,7 @@ export class Transform extends Component { * @param x - Rotation along x axis, in degrees * @param y - Rotation along y axis, in degrees * @param z - Rotation along z axis, in degrees - * @param relativeToLocal - Is relative to the local coordinate system + * @param relativeToLocal = `true` - Is relative to the local coordinate system */ rotate(x: number, y: number, z: number, relativeToLocal?: boolean): void; @@ -490,7 +491,7 @@ export class Transform extends Component { * Rotate around the specified axis according to the specified angle. * @param axis - Rotate axis * @param angle - Rotate angle in degrees - * @param relativeToLocal - Relative to local space + * @param relativeToLocal = `true` - Relative to local space */ rotateByAxis(axis: Vector3, angle: number, relativeToLocal: boolean = true): void { const rad = angle * MathUtil.degreeToRadFactor; @@ -545,6 +546,13 @@ export class Transform extends Component { return this._updateFlagManager.createFlag(BoolUpdateFlag); } + /** + * @intenral + */ + _registerWorldChangeListenser(): ListenerUpdateFlag { + return this._updateFlagManager.createFlag(ListenerUpdateFlag); + } + /** * @internal */ diff --git a/packages/core/src/enums/CameraClearFlags.ts b/packages/core/src/enums/CameraClearFlags.ts index 7651e1a595..7646cfbe19 100644 --- a/packages/core/src/enums/CameraClearFlags.ts +++ b/packages/core/src/enums/CameraClearFlags.ts @@ -2,10 +2,22 @@ * Camera clear flags enumeration. */ export enum CameraClearFlags { - /* Clear depth and color from background. */ - DepthColor, - /* Clear depth only. */ - Depth, /* Do nothing. */ - None + None = 0x0, + /* Clear color with scene background. */ + Color = 0x1, + /* Clear depth only. */ + Depth = 0x2, + /* Clear depth only. */ + Stencil = 0x4, + + /* Clear color with scene background and depth. */ + ColorDepth = 0x3, + /* Clear color with scene background and stencil. */ + ColorStencil = 0x5, + /* Clear depth and stencil. */ + DepthStencil = 0x6, + + /* Clear color with scene background, depth, and stencil. */ + All = 0x7 } diff --git a/packages/core/src/input/InputManager.ts b/packages/core/src/input/InputManager.ts index ad989bdf59..0a1268e32d 100644 --- a/packages/core/src/input/InputManager.ts +++ b/packages/core/src/input/InputManager.ts @@ -3,70 +3,164 @@ import { KeyboardManager } from "./keyboard/KeyboardManager"; import { Keys } from "./enums/Keys"; import { Pointer } from "./pointer/Pointer"; import { PointerManager } from "./pointer/PointerManager"; +import { PointerButton } from "./enums/PointerButton"; +import { WheelManager } from "./wheel/WheelManager"; +import { Vector2, Vector3 } from "@oasis-engine/math"; /** * InputManager manages device input such as mouse, touch, keyboard, etc. */ export class InputManager { - /** Disable input for offscreen rendering. */ - private _enabled: boolean = true; + /** Sometimes the input module will not be initialized, such as off-screen rendering. */ + private _initialized: boolean = false; + private _curFrameCount: number = 0; + private _wheelManager: WheelManager; private _pointerManager: PointerManager; private _keyboardManager: KeyboardManager; /** * Pointer List. */ - get pointers(): Readonly { - return this._enabled ? this._pointerManager._pointers : null; + get pointers(): Readonly { + return this._initialized ? this._pointerManager._pointers : null; } /** * Whether to handle multi-pointer. */ get multiPointerEnabled(): boolean { - return this._enabled ? this._pointerManager._multiPointerEnabled : false; + return this._initialized ? this._pointerManager._multiPointerEnabled : false; } set multiPointerEnabled(enabled: boolean) { - this._enabled && (this._pointerManager._multiPointerEnabled = enabled); + this._initialized && (this._pointerManager._multiPointerEnabled = enabled); + } + + /** + * Get the change of the scroll wheel on the x-axis. + * @returns Change value + */ + get wheelDelta(): Readonly { + return this._initialized ? this._wheelManager._delta : null; + } + + /** + * Get the change of the pointer. + * @returns Change value + */ + get pointerMovingDelta(): Readonly { + return this._initialized ? this._pointerManager._movingDelta : null; + } + + /** + * Get the position of the pointer. + * @returns The position of the pointer + */ + get pointerPosition(): Readonly { + return this._initialized && this._pointerManager._pointers.length > 0 + ? this._pointerManager._currentPosition + : null; } /** * Whether the key is being held down, if there is no parameter, return whether any key is being held down. * @param key - The keys of the keyboard - * @returns Whether the key is being held down. + * @returns Whether the key is being held down */ isKeyHeldDown(key?: Keys): boolean { - if (key === undefined) { - return this._keyboardManager._curFrameHeldDownList.length > 0; + if (this._initialized) { + if (key === undefined) { + return this._keyboardManager._curFrameHeldDownList.length > 0; + } else { + return this._keyboardManager._curHeldDownKeyToIndexMap[key] != null; + } } else { - return !!this._keyboardManager._curHeldDownKeyToIndexMap[key]; + return false; } } /** * Whether the key starts to be pressed down during the current frame, if there is no parameter, return whether any key starts to be pressed down during the current frame. * @param key - The keys of the keyboard - * @returns Whether the key starts to be pressed down during the current frame. + * @returns Whether the key starts to be pressed down during the current frame */ isKeyDown(key?: Keys): boolean { - if (key === undefined) { - return this._keyboardManager._curFrameDownList.length > 0; + if (this._initialized) { + if (key === undefined) { + return this._keyboardManager._curFrameDownList.length > 0; + } else { + return this._keyboardManager._downKeyToFrameCountMap[key] === this._curFrameCount; + } } else { - return this._keyboardManager._downKeyToFrameCountMap[key] === this._keyboardManager._curFrameCount; + return false; } } /** * Whether the key is released during the current frame, if there is no parameter, return whether any key released during the current frame. * @param key - The keys of the keyboard - * @returns Whether the key is released during the current frame. + * @returns Whether the key is released during the current frame */ isKeyUp(key?: Keys): boolean { - if (key === undefined) { - return this._keyboardManager._curFrameUpList.length > 0; + if (this._initialized) { + if (key === undefined) { + return this._keyboardManager._curFrameUpList.length > 0; + } else { + return this._keyboardManager._upKeyToFrameCountMap[key] === this._curFrameCount; + } + } else { + return false; + } + } + + /** + * Whether the pointer is being held down, if there is no parameter, return whether any pointer is being held down. + * @param pointerButton - The pointerButton on a pointer device + * @returns Whether the pointer is being held down + */ + isPointerHeldDown(pointerButton?: PointerButton): boolean { + if (this._initialized) { + if (pointerButton === undefined) { + return this._pointerManager._buttons !== 0; + } else { + return (this._pointerManager._buttons & PointerManager.Buttons[pointerButton]) !== 0; + } } else { - return this._keyboardManager._upKeyToFrameCountMap[key] === this._keyboardManager._curFrameCount; + return false; + } + } + + /** + * Whether the pointer starts to be pressed down during the current frame, if there is no parameter, return whether any pointer starts to be pressed down during the current frame. + * @param pointerButton - The pointerButton on a pointer device + * @returns Whether the pointer starts to be pressed down during the current frame + */ + isPointerDown(pointerButton: PointerButton): boolean { + if (this._initialized) { + if (pointerButton === undefined) { + return this._pointerManager._downList.length > 0; + } else { + return this._pointerManager._downMap[pointerButton] === this._curFrameCount; + } + } else { + return false; + } + } + + /** + * Whether the pointer is released during the current frame, if there is no parameter, return whether any pointer released during the current frame. + * @param pointerButton - The pointerButtons on a mouse device + * @returns Whether the pointer is released during the current frame + */ + isPointerUp(pointerButton: PointerButton): boolean { + if (this._initialized) { + if (pointerButton === undefined) { + return this._pointerManager._upList.length > 0; + } else { + return this._pointerManager._upMap[pointerButton] === this._curFrameCount; + } + } else { + return false; } } @@ -77,13 +171,14 @@ export class InputManager { // @ts-ignore const canvas = engine._canvas._webCanvas; if (canvas instanceof HTMLCanvasElement) { - this._enabled = true; + this._wheelManager = new WheelManager(canvas); this._pointerManager = new PointerManager(engine, canvas); this._keyboardManager = new KeyboardManager(); this._onBlur = this._onBlur.bind(this); window.addEventListener("blur", this._onBlur); - } else { - this._enabled = false; + this._onFocus = this._onFocus.bind(this); + window.addEventListener("focus", this._onFocus); + this._initialized = true; } } @@ -91,9 +186,11 @@ export class InputManager { * @internal */ _update(): void { - if (this._enabled) { - this._pointerManager._update(); - this._keyboardManager._update(); + if (this._initialized) { + ++this._curFrameCount; + this._wheelManager._update(); + this._pointerManager._update(this._curFrameCount); + this._keyboardManager._update(this._curFrameCount); } } @@ -101,16 +198,24 @@ export class InputManager { * @internal */ _destroy(): void { - if (this._enabled) { + if (this._initialized) { window.removeEventListener("blur", this._onBlur); + window.removeEventListener("focus", this._onFocus); + this._wheelManager._destroy(); this._pointerManager._destroy(); this._keyboardManager._destroy(); } } private _onBlur(): void { - if (this._enabled) { - this._keyboardManager._onBlur(); - } + this._wheelManager._onBlur(); + this._pointerManager._onBlur(); + this._keyboardManager._onBlur(); + } + + private _onFocus(): void { + this._wheelManager._onFocus(); + this._pointerManager._onFocus(); + this._keyboardManager._onFocus(); } } diff --git a/packages/core/src/input/enums/PointerButton.ts b/packages/core/src/input/enums/PointerButton.ts new file mode 100644 index 0000000000..8060aabe7e --- /dev/null +++ b/packages/core/src/input/enums/PointerButton.ts @@ -0,0 +1,29 @@ +/** + * Defines values that specify the buttons on a pointer device. + * Refer to the W3C standards.(https://www.w3.org/TR/uievents/#dom-mouseevent-button) + * Refer to Microsoft's documentation.(https://docs.microsoft.com/en-us/dotnet/api/system.windows.input.mousebutton?view=windowsdesktop-6.0) + */ +export enum PointerButton { + /** Indicate the primary pointer of the device (in general, the left button or the only button on single-button devices, used to activate a user interface control or select text) or the un-initialized value. */ + Primary = 0, + /** Indicate the auxiliary pointer (in general, the middle button, often combined with a mouse wheel). */ + Auxiliary = 1, + /** Indicate the secondary pointer (in general, the right button, often used to display a context menu). */ + Secondary = 2, + /** Indicate the X1 (back) pointer. */ + XButton1 = 3, + /** Indicate the X2 (forward) pointer. */ + XButton2 = 4, + /** Indicate the X3 pointer. */ + XButton3 = 5, + /** Indicate the X4 pointer. */ + XButton4 = 6, + /** Indicate the X5 pointer. */ + XButton5 = 7, + /** Indicate the X6 pointer. */ + XButton6 = 8, + /** Indicate the X7 pointer. */ + XButton7 = 9, + /** Indicate the X8 pointer. */ + XButton8 = 10 +} diff --git a/packages/core/src/input/index.ts b/packages/core/src/input/index.ts index eccbaf0429..8edd5391dc 100644 --- a/packages/core/src/input/index.ts +++ b/packages/core/src/input/index.ts @@ -1,4 +1,5 @@ export { PointerPhase } from "./enums/PointerPhase"; export { Pointer } from "./pointer/Pointer"; export { InputManager } from "./InputManager"; -export { Keys } from "./enums/Keys"; \ No newline at end of file +export { Keys } from "./enums/Keys"; +export { PointerButton } from "./enums/PointerButton"; diff --git a/packages/core/src/input/interface/IInput.ts b/packages/core/src/input/interface/IInput.ts new file mode 100644 index 0000000000..cfc0a20867 --- /dev/null +++ b/packages/core/src/input/interface/IInput.ts @@ -0,0 +1,18 @@ +export interface IInput { + /** + * Handler function updated every frame. + */ + _update(frameCount?: number): void; + /** + * Function called when the engine is destroyed. + */ + _destroy(): void; + /** + * Function called when focused. + */ + _onFocus(): void; + /** + * Function called when focus is lost. + */ + _onBlur(): void; +} diff --git a/packages/core/src/input/keyboard/KeyboardManager.ts b/packages/core/src/input/keyboard/KeyboardManager.ts index a36c45fd16..c1cbd90c0f 100644 --- a/packages/core/src/input/keyboard/KeyboardManager.ts +++ b/packages/core/src/input/keyboard/KeyboardManager.ts @@ -1,107 +1,131 @@ - import { DisorderedArray } from "../../DisorderedArray"; import { Keys } from "../enums/Keys"; +import { IInput } from "../interface/IInput"; /** * Keyboard Manager. * @internal */ -export class KeyboardManager { - /** @internal */ - _curFrameCount: number = 0; - /** @internal */ - _curHeldDownKeyToIndexMap: number[] = []; - /** @internal */ - _upKeyToFrameCountMap: number[] = []; - /** @internal */ - _downKeyToFrameCountMap: number[] = []; +export class KeyboardManager implements IInput { + /** @internal */ + _curHeldDownKeyToIndexMap: number[] = []; + /** @internal */ + _upKeyToFrameCountMap: number[] = []; + /** @internal */ + _downKeyToFrameCountMap: number[] = []; - /** @internal */ - _curFrameHeldDownList: DisorderedArray = new DisorderedArray(); - /** @internal */ - _curFrameDownList: DisorderedArray = new DisorderedArray(); - /** @internal */ - _curFrameUpList: DisorderedArray = new DisorderedArray(); + /** @internal */ + _curFrameHeldDownList: DisorderedArray = new DisorderedArray(); + /** @internal */ + _curFrameDownList: DisorderedArray = new DisorderedArray(); + /** @internal */ + _curFrameUpList: DisorderedArray = new DisorderedArray(); - private _nativeEvents: KeyboardEvent[] = []; - private _onKeyEvent: (evt: KeyboardEvent) => void; + private _nativeEvents: KeyboardEvent[] = []; + private _hadListener: boolean = false; - /** - * Create a KeyboardManager. - */ - constructor() { - this._onKeyEvent = (evt: KeyboardEvent) => { - this._nativeEvents.push(evt); - } - window.addEventListener('keydown', this._onKeyEvent); - window.addEventListener('keyup', this._onKeyEvent); - } + /** + * Create a KeyboardManager. + */ + constructor() { + this._onKeyEvent = this._onKeyEvent.bind(this); + window.addEventListener("keydown", this._onKeyEvent); + window.addEventListener("keyup", this._onKeyEvent); + this._hadListener = true; + } - /** - * @internal - */ - _update(): void { - const curFrameCount = ++this._curFrameCount; - const { _nativeEvents: nativeEvents, _curFrameDownList: curFrameDownList, _curFrameUpList: curFrameUpList } = this; - curFrameDownList.length = 0; - curFrameUpList.length = 0; - if (nativeEvents.length > 0) { - const { _curHeldDownKeyToIndexMap: curHeldDownKeyToIndexMap, _curFrameHeldDownList: curFrameHeldDownList, _downKeyToFrameCountMap: downKeyToFrameCountMap, _upKeyToFrameCountMap: upKeyToFrameCountMap } = this; - for (let i = 0, n = nativeEvents.length; i < n; i++) { - const evt = nativeEvents[i]; - const codeKey = Keys[evt.code]; - switch (evt.type) { - case 'keydown': - // Filter the repeated triggers of the keyboard. - if (curHeldDownKeyToIndexMap[codeKey] == null) { - curFrameDownList.add(codeKey); - curFrameHeldDownList.add(codeKey); - curHeldDownKeyToIndexMap[codeKey] = curFrameHeldDownList.length - 1; - downKeyToFrameCountMap[codeKey] = curFrameCount; - } - break; - case 'keyup': - const delIndex = curHeldDownKeyToIndexMap[codeKey]; - if (delIndex != null) { - curHeldDownKeyToIndexMap[codeKey] = null; - const swapCode = curFrameHeldDownList.deleteByIndex(delIndex); - swapCode && (curHeldDownKeyToIndexMap[swapCode] = delIndex); - } - curFrameUpList.add(codeKey); - upKeyToFrameCountMap[codeKey] = curFrameCount; - break; - default: - break; - } + /** + * @internal + */ + _update(frameCount: number): void { + const { _nativeEvents: nativeEvents, _curFrameDownList: curFrameDownList, _curFrameUpList: curFrameUpList } = this; + curFrameDownList.length = 0; + curFrameUpList.length = 0; + if (nativeEvents.length > 0) { + const { + _curHeldDownKeyToIndexMap: curHeldDownKeyToIndexMap, + _curFrameHeldDownList: curFrameHeldDownList, + _downKeyToFrameCountMap: downKeyToFrameCountMap, + _upKeyToFrameCountMap: upKeyToFrameCountMap + } = this; + for (let i = 0, n = nativeEvents.length; i < n; i++) { + const evt = nativeEvents[i]; + const codeKey = Keys[evt.code]; + switch (evt.type) { + case "keydown": + // Filter the repeated triggers of the keyboard. + if (curHeldDownKeyToIndexMap[codeKey] == null) { + curFrameDownList.add(codeKey); + curFrameHeldDownList.add(codeKey); + curHeldDownKeyToIndexMap[codeKey] = curFrameHeldDownList.length - 1; + downKeyToFrameCountMap[codeKey] = frameCount; + } + break; + case "keyup": + const delIndex = curHeldDownKeyToIndexMap[codeKey]; + if (delIndex != null) { + curHeldDownKeyToIndexMap[codeKey] = null; + const swapCode = curFrameHeldDownList.deleteByIndex(delIndex); + swapCode && (curHeldDownKeyToIndexMap[swapCode] = delIndex); } - nativeEvents.length = 0; + curFrameUpList.add(codeKey); + upKeyToFrameCountMap[codeKey] = frameCount; + break; + default: + break; } + } + nativeEvents.length = 0; } + } - /** - * @internal - */ - _onBlur(): void { - this._curHeldDownKeyToIndexMap.length = 0; - this._nativeEvents.length = 0; - this._curFrameHeldDownList.length = 0; - this._curFrameDownList.length = 0; - this._curFrameUpList.length = 0; + /** + * @internal + */ + _onFocus(): void { + if (!this._hadListener) { + window.addEventListener("keydown", this._onKeyEvent); + window.addEventListener("keyup", this._onKeyEvent); + this._hadListener = true; } + } - /** - * @internal - */ - _destroy(): void { - window.removeEventListener('keydown', this._onKeyEvent); - window.removeEventListener('keyup', this._onKeyEvent); - this._curHeldDownKeyToIndexMap = null; - this._upKeyToFrameCountMap = null; - this._downKeyToFrameCountMap = null; - this._nativeEvents = null; + /** + * @internal + */ + _onBlur(): void { + if (this._hadListener) { + window.removeEventListener("keydown", this._onKeyEvent); + window.removeEventListener("keyup", this._onKeyEvent); + this._curHeldDownKeyToIndexMap.length = 0; + this._curFrameHeldDownList.length = 0; + this._curFrameDownList.length = 0; + this._curFrameUpList.length = 0; + this._nativeEvents.length = 0; + this._hadListener = false; + } + } - this._curFrameHeldDownList = null; - this._curFrameDownList = null; - this._curFrameUpList = null; + /** + * @internal + */ + _destroy(): void { + if (this._hadListener) { + window.removeEventListener("keydown", this._onKeyEvent); + window.removeEventListener("keyup", this._onKeyEvent); + this._hadListener = false; } -} \ No newline at end of file + this._curHeldDownKeyToIndexMap = null; + this._upKeyToFrameCountMap = null; + this._downKeyToFrameCountMap = null; + this._nativeEvents = null; + + this._curFrameHeldDownList = null; + this._curFrameDownList = null; + this._curFrameUpList = null; + } + + private _onKeyEvent(evt: KeyboardEvent): void { + this._nativeEvents.push(evt); + } +} diff --git a/packages/core/src/input/pointer/PointerManager.ts b/packages/core/src/input/pointer/PointerManager.ts index 4ce88b4dc0..b453bf297e 100644 --- a/packages/core/src/input/pointer/PointerManager.ts +++ b/packages/core/src/input/pointer/PointerManager.ts @@ -1,17 +1,23 @@ import { Ray, Vector2 } from "@oasis-engine/math"; import { Canvas } from "../../Canvas"; +import { DisorderedArray } from "../../DisorderedArray"; import { Engine } from "../../Engine"; import { Entity } from "../../Entity"; import { CameraClearFlags } from "../../enums/CameraClearFlags"; -import { HitResult, PhysicsManager } from "../../physics"; +import { HitResult } from "../../physics"; import { PointerPhase } from "../enums/PointerPhase"; +import { PointerButton } from "../enums/PointerButton"; +import { IInput } from "../interface/IInput"; import { Pointer } from "./Pointer"; /** * Pointer Manager. * @internal */ -export class PointerManager { +export class PointerManager implements IInput { + /** Refer to the W3C standards.(https://www.w3.org/TR/uievents/#dom-mouseevent-buttons) */ + public static Buttons = [0x1, 0x4, 0x2, 0x8, 0x10, 0x20, 0x40, 0x80, 0x100, 0x200, 0x400]; + private static _tempRay: Ray = new Ray(); private static _tempPoint: Vector2 = new Vector2(); private static _tempHitResult: HitResult = new HitResult(); @@ -19,18 +25,34 @@ export class PointerManager { /** @internal */ _pointers: Pointer[] = []; /** @internal */ + _movingDelta: Vector2 = new Vector2(); + /** @internal */ _multiPointerEnabled: boolean = true; + /** @internal */ + _buttons: number = 0x0; + /** @internal */ + _upMap: number[] = []; + /** @internal */ + _downMap: number[] = []; + /** @internal */ + _downList: DisorderedArray = new DisorderedArray(); + /** @internal */ + _upList: DisorderedArray = new DisorderedArray(); + /** @internal */ + _currentPosition: Vector2 = new Vector2(); + + private _currentPressedEntity: Entity; + private _currentEnteredEntity: Entity; private _engine: Engine; private _canvas: Canvas; + private _htmlCanvas: HTMLCanvasElement; private _nativeEvents: PointerEvent[] = []; private _pointerPool: Pointer[]; private _keyEventList: number[] = []; private _keyEventCount: number = 0; private _needOverallPointers: boolean = false; - private _currentPosition: Vector2 = new Vector2(); - private _currentPressedEntity: Entity; - private _currentEnteredEntity: Entity; + private _hadListener: boolean = false; /** * Create a PointerManager. @@ -40,11 +62,17 @@ export class PointerManager { constructor(engine: Engine, htmlCanvas: HTMLCanvasElement) { this._engine = engine; this._canvas = engine.canvas; + this._htmlCanvas = htmlCanvas; htmlCanvas.style.touchAction = "none"; - // prettier-ignore - htmlCanvas.onpointerdown = htmlCanvas.onpointerup = htmlCanvas.onpointerout = htmlCanvas.onpointermove = (evt: PointerEvent) => { - this._nativeEvents.push(evt); + htmlCanvas.oncontextmenu = (event: UIEvent) => { + return false; }; + const onPointerEvent = (this._onPointerEvent = this._onPointerEvent.bind(this)); + htmlCanvas.addEventListener("pointerdown", onPointerEvent); + htmlCanvas.addEventListener("pointerup", onPointerEvent); + htmlCanvas.addEventListener("pointerout", onPointerEvent); + htmlCanvas.addEventListener("pointermove", onPointerEvent); + this._hadListener = true; // If there are no compatibility issues, navigator.maxTouchPoints should be used here. this._pointerPool = new Array(11); } @@ -52,9 +80,12 @@ export class PointerManager { /** * @internal */ - _update(): void { + _update(frameCount: number): void { this._needOverallPointers && this._overallPointers(); - this._nativeEvents.length > 0 && this._handlePointerEvent(this._nativeEvents); + this._downList.length = 0; + this._upList.length = 0; + this._movingDelta.set(0, 0); + this._nativeEvents.length > 0 && this._handlePointerEvent(this._nativeEvents, frameCount); if (this._engine.physicsManager._initialized) { const rayCastEntity = this._pointerRayCast(); const { _keyEventCount: keyEventCount } = this; @@ -80,13 +111,53 @@ export class PointerManager { } } + /** + * @internal + */ + _onFocus(): void { + if (!this._hadListener) { + const { _htmlCanvas: htmlCanvas, _onPointerEvent: onPointerEvent } = this; + htmlCanvas.addEventListener("pointerdown", onPointerEvent); + htmlCanvas.addEventListener("pointerup", onPointerEvent); + htmlCanvas.addEventListener("pointerout", onPointerEvent); + htmlCanvas.addEventListener("pointermove", onPointerEvent); + this._hadListener = true; + } + } + + /** + * @internal + */ + _onBlur(): void { + if (this._hadListener) { + const { _htmlCanvas: htmlCanvas, _onPointerEvent: onPointerEvent } = this; + htmlCanvas.removeEventListener("pointerdown", onPointerEvent); + htmlCanvas.removeEventListener("pointerup", onPointerEvent); + htmlCanvas.removeEventListener("pointerout", onPointerEvent); + htmlCanvas.removeEventListener("pointermove", onPointerEvent); + this._nativeEvents.length = 0; + this._pointerPool.length = 0; + this._currentEnteredEntity = null; + this._currentPressedEntity = null; + this._downList.length = 0; + this._upList.length = 0; + this._hadListener = false; + } + } + /** * @internal */ _destroy(): void { // @ts-ignore - const htmlCanvas = this._canvas._webCanvas as HTMLCanvasElement; - htmlCanvas.onpointerdown = htmlCanvas.onpointerup = htmlCanvas.onpointerout = htmlCanvas.onpointermove = null; + if (this._hadListener) { + const { _htmlCanvas: htmlCanvas, _onPointerEvent: onPointerEvent } = this; + htmlCanvas.removeEventListener("pointerdown", onPointerEvent); + htmlCanvas.removeEventListener("pointerup", onPointerEvent); + htmlCanvas.removeEventListener("pointerout", onPointerEvent); + htmlCanvas.removeEventListener("pointermove", onPointerEvent); + this._hadListener = false; + } this._nativeEvents.length = 0; this._pointerPool.length = 0; this._pointers.length = 0; @@ -97,6 +168,10 @@ export class PointerManager { this._canvas = null; } + private _onPointerEvent(evt: PointerEvent) { + this._nativeEvents.push(evt); + } + private _overallPointers(): void { const { _pointers: pointers } = this; let deleteCount = 0; @@ -149,7 +224,8 @@ export class PointerManager { } private _removePointer(pointerIndex: number): void { - this._pointers[pointerIndex].phase = PointerPhase.Leave; + const leavePointer = this._pointers[pointerIndex]; + leavePointer.phase = PointerPhase.Leave; } private _updatePointer(pointerIndex: number, x: number, y: number, phase: PointerPhase): void { @@ -159,53 +235,67 @@ export class PointerManager { updatedPointer.phase = phase; } - private _handlePointerEvent(nativeEvents: PointerEvent[]): void { - const { _pointers: pointers, _keyEventList: keyEventList } = this; + private _handlePointerEvent(nativeEvents: PointerEvent[], frameCount: number): void { + const { + _pointers: pointers, + _keyEventList: keyEventList, + _upMap: upMap, + _downMap: downMap, + _upList: upList, + _downList: downList + } = this; let activePointerCount = pointers.length; const nativeEventsLen = nativeEvents.length; - for (let i = 0; i < nativeEventsLen; i++) { - const evt = nativeEvents[i]; - let pointerIndex = this._getIndexByPointerID(evt.pointerId); - switch (evt.type) { - case "pointerdown": - if (pointerIndex === -1) { - this._addPointer(evt.pointerId, evt.offsetX, evt.offsetY, PointerPhase.Down); - activePointerCount++; - } else { - this._updatePointer(pointerIndex, evt.offsetX, evt.offsetY, PointerPhase.Down); - } - activePointerCount === 1 && (keyEventList[this._keyEventCount++] = PointerKeyEvent.Down); - break; - case "pointerup": - if (pointerIndex >= 0) { - this._updatePointer(pointerIndex, evt.offsetX, evt.offsetY, PointerPhase.Up); - activePointerCount === 1 && (keyEventList[this._keyEventCount++] = PointerKeyEvent.Up); - } - break; - case "pointermove": - if (pointerIndex === -1) { - this._addPointer(evt.pointerId, evt.offsetX, evt.offsetY, PointerPhase.Move); - activePointerCount++; - } else { - this._updatePointer(pointerIndex, evt.offsetX, evt.offsetY, PointerPhase.Move); - } - break; - case "pointerout": - if (pointerIndex >= 0) { - this._removePointer(pointerIndex); - --activePointerCount === 0 && (keyEventList[this._keyEventCount++] = PointerKeyEvent.Leave); - this._needOverallPointers = true; - } - break; + if (nativeEventsLen > 0) { + for (let i = 0; i < nativeEventsLen; i++) { + const evt = nativeEvents[i]; + const pointerButton: PointerButton = evt.button | PointerButton.Primary; + const pointerIndex = this._getIndexByPointerID(evt.pointerId); + switch (evt.type) { + case "pointerdown": + if (pointerIndex === -1) { + this._addPointer(evt.pointerId, evt.offsetX, evt.offsetY, PointerPhase.Down); + activePointerCount++; + } else { + this._updatePointer(pointerIndex, evt.offsetX, evt.offsetY, PointerPhase.Down); + } + activePointerCount === 1 && (keyEventList[this._keyEventCount++] = PointerKeyEvent.Down); + downList.add(pointerButton); + downMap[pointerButton] = frameCount; + break; + case "pointerup": + if (pointerIndex >= 0) { + this._updatePointer(pointerIndex, evt.offsetX, evt.offsetY, PointerPhase.Up); + activePointerCount === 1 && (keyEventList[this._keyEventCount++] = PointerKeyEvent.Up); + } + upList.add(pointerButton); + upMap[pointerButton] = frameCount; + break; + case "pointermove": + if (pointerIndex === -1) { + this._addPointer(evt.pointerId, evt.offsetX, evt.offsetY, PointerPhase.Move); + activePointerCount++; + } else { + this._updatePointer(pointerIndex, evt.offsetX, evt.offsetY, PointerPhase.Move); + } + break; + case "pointerout": + if (pointerIndex >= 0) { + this._removePointer(pointerIndex); + --activePointerCount === 0 && (keyEventList[this._keyEventCount++] = PointerKeyEvent.Leave); + this._needOverallPointers = true; + } + break; + } } + this._buttons = nativeEvents[nativeEventsLen - 1].buttons; } const pointerCount = pointers.length; if (pointerCount > 0) { - const { _canvas: canvas, _currentPosition: currentPosition } = this; - // @ts-ignore - const pixelRatioWidth = canvas.width / (canvas._webCanvas as HTMLCanvasElement).clientWidth; - // @ts-ignore - const pixelRatioHeight = canvas.height / (canvas._webCanvas as HTMLCanvasElement).clientHeight; + const { _canvas: canvas, _currentPosition: currentPosition, _htmlCanvas: htmlCanvas } = this; + const { x: lastX, y: lastY } = currentPosition; + const pixelRatioWidth = canvas.width / htmlCanvas.clientWidth; + const pixelRatioHeight = canvas.height / htmlCanvas.clientHeight; if (activePointerCount === 0) { // Get the pointer coordinates when leaving, and use it to correctly dispatch the click event. const lastNativeEvent = nativeEvents[nativeEventsLen - 1]; @@ -223,6 +313,11 @@ export class PointerManager { } currentPosition.scale(1 / pointerCount); } + // Update pointer moving delta. + // Todo: Need to consider if the last coordinate is(0, 0). + if (lastX !== 0 || lastY !== 0) { + this._movingDelta.set(currentPosition.x - lastX, currentPosition.y - lastY); + } } nativeEvents.length = 0; } @@ -244,7 +339,7 @@ export class PointerManager { // TODO: Only check which colliders have listened to the input. if (this._engine.physicsManager.raycast(camera.viewportPointToRay(point, ray), hitResult)) { return hitResult.entity; - } else if (camera.clearFlags === CameraClearFlags.DepthColor) { + } else if (camera.clearFlags & CameraClearFlags.Color) { return null; } } diff --git a/packages/core/src/input/wheel/WheelManager.ts b/packages/core/src/input/wheel/WheelManager.ts new file mode 100644 index 0000000000..363f936604 --- /dev/null +++ b/packages/core/src/input/wheel/WheelManager.ts @@ -0,0 +1,80 @@ +import { Vector3 } from "@oasis-engine/math"; +import { IInput } from "../interface/IInput"; + +/** + * Wheel Manager. + * @internal + */ +export class WheelManager implements IInput { + /** @internal */ + _delta: Vector3 = new Vector3(); + + private _nativeEvents: WheelEvent[] = []; + private _canvas: HTMLCanvasElement; + private _hadListener: boolean; + + /** + * Create a KeyboardManager. + */ + constructor(htmlCanvas: HTMLCanvasElement) { + this._onWheelEvent = this._onWheelEvent.bind(this); + htmlCanvas.addEventListener("wheel", this._onWheelEvent); + this._canvas = htmlCanvas; + this._hadListener = true; + } + + /** + * @internal + */ + _update(): void { + const { _delta: delta } = this; + delta.set(0, 0, 0); + const { _nativeEvents: nativeEvents } = this; + if (nativeEvents.length > 0) { + for (let i = nativeEvents.length - 1; i >= 0; i--) { + const evt = nativeEvents[i]; + delta.x += evt.deltaX; + delta.y += evt.deltaY; + delta.z += evt.deltaZ; + } + nativeEvents.length = 0; + } + } + + /** + * @internal + */ + _onFocus(): void { + if (!this._hadListener) { + this._canvas.addEventListener("wheel", this._onWheelEvent); + this._hadListener = true; + } + } + + /** + * @internal + */ + _onBlur(): void { + if (this._hadListener) { + this._canvas.removeEventListener("wheel", this._onWheelEvent); + this._nativeEvents.length = 0; + this._delta.set(0, 0, 0); + this._hadListener = false; + } + } + + /** + * @internal + */ + _destroy(): void { + if (this._hadListener) { + this._canvas.removeEventListener("wheel", this._onWheelEvent); + this._hadListener = false; + } + this._nativeEvents = null; + } + + private _onWheelEvent(evt: WheelEvent): void { + this._nativeEvents.push(evt); + } +} diff --git a/packages/core/src/physics/PhysicsManager.ts b/packages/core/src/physics/PhysicsManager.ts index caf90e98a4..8954e4b3c3 100644 --- a/packages/core/src/physics/PhysicsManager.ts +++ b/packages/core/src/physics/PhysicsManager.ts @@ -9,7 +9,7 @@ import { HitResult } from "./HitResult"; import { ColliderShape } from "./shape"; /** - * A physics manager is a collection of bodies and constraints which can interact. + * A physics manager is a collection of colliders and constraints which can interact. */ export class PhysicsManager { /** @internal */ diff --git a/packages/core/src/physics/enums/HingeJointFlag.ts b/packages/core/src/physics/enums/HingeJointFlag.ts new file mode 100644 index 0000000000..140038f249 --- /dev/null +++ b/packages/core/src/physics/enums/HingeJointFlag.ts @@ -0,0 +1,12 @@ +/** + * Flags specific to the Hinge Joint. + * @internal + */ +export enum HingeJointFlag { + /** enable the limit */ + LimitEnabled = 1, + /** enable the drive */ + DriveEnabled = 2, + /** if the existing velocity is beyond the drive velocity, do not add force */ + DriveFreeSpin = 4 +} diff --git a/packages/core/src/physics/enums/index.ts b/packages/core/src/physics/enums/index.ts new file mode 100644 index 0000000000..23b168e2c4 --- /dev/null +++ b/packages/core/src/physics/enums/index.ts @@ -0,0 +1,4 @@ +export * from "./PhysicsMaterialCombineMode"; +export * from "./ColliderShapeUpAxis"; +export * from "./ControllerCollisionFlag"; +export * from "./ControllerNonWalkableMode"; diff --git a/packages/core/src/physics/index.ts b/packages/core/src/physics/index.ts index 1bfd79ad2d..07f7e4bfde 100644 --- a/packages/core/src/physics/index.ts +++ b/packages/core/src/physics/index.ts @@ -1,13 +1,10 @@ export { HitResult } from "./HitResult"; -export { PhysicsMaterialCombineMode } from "./enums/PhysicsMaterialCombineMode"; -export { ColliderShapeUpAxis } from "./enums/ColliderShapeUpAxis"; -export { ControllerCollisionFlag } from "./enums/ControllerCollisionFlag"; -export { ControllerNonWalkableMode } from "./enums/ControllerNonWalkableMode"; - export { PhysicsManager } from "./PhysicsManager"; export { PhysicsMaterial } from "./PhysicsMaterial"; export { CharacterController } from "./CharacterController"; export * from "./shape"; +export * from "./joint"; +export * from "./enums"; export { Collider } from "./Collider"; export { StaticCollider } from "./StaticCollider"; diff --git a/packages/core/src/physics/joint/FixedJoint.ts b/packages/core/src/physics/joint/FixedJoint.ts new file mode 100644 index 0000000000..4b78b75f89 --- /dev/null +++ b/packages/core/src/physics/joint/FixedJoint.ts @@ -0,0 +1,18 @@ +import { Joint } from "./Joint"; +import { PhysicsManager } from "../PhysicsManager"; +import { Collider } from "../Collider"; + +/* + * A fixed joint permits no relative movement between two colliders. ie the colliders are glued together. + */ +export class FixedJoint extends Joint { + /** + * @override + * @internal + */ + _onAwake() { + const collider = this._collider; + collider.collider = this.entity.getComponent(Collider); + this._nativeJoint = PhysicsManager._nativePhysics.createFixedJoint(collider.collider._nativeCollider); + } +} diff --git a/packages/core/src/physics/joint/HingeJoint.ts b/packages/core/src/physics/joint/HingeJoint.ts new file mode 100644 index 0000000000..02de0cdae8 --- /dev/null +++ b/packages/core/src/physics/joint/HingeJoint.ts @@ -0,0 +1,146 @@ +import { Joint } from "./Joint"; +import { IHingeJoint } from "@oasis-engine/design"; +import { PhysicsManager } from "../PhysicsManager"; +import { HingeJointFlag } from "../enums/HingeJointFlag"; +import { Collider } from "../Collider"; +import { Vector3 } from "@oasis-engine/math"; +import { JointMotor } from "./JointMotor"; +import { JointLimits } from "./JointLimits"; + +/** + * A joint which behaves in a similar way to a hinge or axle. + */ +export class HingeJoint extends Joint { + private _axis: Vector3 = new Vector3(1, 0, 0); + private _hingeFlags: number = 0; + private _useSpring: boolean = false; + private _jointMonitor: JointMotor; + private _limits: JointLimits; + + /** + * The anchor rotation. + */ + get axis(): Vector3 { + return this._axis; + } + + set axis(value: Vector3) { + const axis = this._axis; + if (value !== axis) { + axis.copyFrom(value); + } + (this._nativeJoint).setAxis(axis); + } + + /** + * The swing offset. + */ + get swingOffset(): Vector3 { + return this._collider.localPosition; + } + + set swingOffset(value: Vector3) { + const swingOffset = this._collider.localPosition; + if (value !== swingOffset) { + swingOffset.copyFrom(value); + } + (this._nativeJoint).setSwingOffset(swingOffset); + } + + /** + * The current angle in degrees of the joint relative to its rest position. + */ + get angle(): number { + return (this._nativeJoint).getAngle(); + } + + /** + * The angular velocity of the joint in degrees per second. + */ + get velocity(): Readonly { + return (this._nativeJoint).getVelocity(); + } + + /** + * Enables the joint's limits. Disabled by default. + */ + get useLimits(): boolean { + return (this._hingeFlags & HingeJointFlag.LimitEnabled) == HingeJointFlag.LimitEnabled; + } + + set useLimits(value: boolean) { + if (value !== this.useLimits) { + this._hingeFlags |= HingeJointFlag.LimitEnabled; + } + (this._nativeJoint).setHingeJointFlag(HingeJointFlag.LimitEnabled, value); + } + + /** + * Enables the joint's motor. Disabled by default. + */ + get useMotor(): boolean { + return (this._hingeFlags & HingeJointFlag.DriveEnabled) == HingeJointFlag.DriveEnabled; + } + + set useMotor(value: boolean) { + if (value !== this.useMotor) { + this._hingeFlags |= HingeJointFlag.DriveEnabled; + } + (this._nativeJoint).setHingeJointFlag(HingeJointFlag.DriveEnabled, value); + } + + /** + * Enables the joint's spring. Disabled by default. + */ + get useSpring(): boolean { + return this._useSpring; + } + + set useSpring(value: boolean) { + this._useSpring = value; + this.limits = this._limits; + } + + /** + * The motor will apply a force up to a maximum force to achieve the target velocity in degrees per second. + */ + get motor(): JointMotor { + return this._jointMonitor; + } + + set motor(value: JointMotor) { + this._jointMonitor = value; + (this._nativeJoint).setDriveVelocity(value.targetVelocity); + (this._nativeJoint).setDriveForceLimit(value.forceLimit); + (this._nativeJoint).setDriveGearRatio(value.gearRation); + (this._nativeJoint).setHingeJointFlag(HingeJointFlag.DriveFreeSpin, value.freeSpin); + } + + /** + * Limit of angular rotation (in degrees) on the hinge joint. + */ + get limits(): JointLimits { + return this._limits; + } + + set limits(value: JointLimits) { + this._limits = value; + if (this.useSpring) { + (this._nativeJoint).setSoftLimit(value.min, value.max, value.stiffness, value.damping); + } else { + (this._nativeJoint).setHardLimit(value.min, value.max, value.contactDistance); + } + } + + /** + * @override + * @internal + */ + _onAwake() { + const { _connectedCollider: connectedCollider, _collider: collider } = this; + connectedCollider.localPosition = new Vector3(); + collider.localPosition = new Vector3(); + collider.collider = this.entity.getComponent(Collider); + this._nativeJoint = PhysicsManager._nativePhysics.createHingeJoint(collider.collider._nativeCollider); + } +} diff --git a/packages/core/src/physics/joint/Joint.ts b/packages/core/src/physics/joint/Joint.ts new file mode 100644 index 0000000000..f5c79903be --- /dev/null +++ b/packages/core/src/physics/joint/Joint.ts @@ -0,0 +1,143 @@ +import { IJoint } from "@oasis-engine/design"; +import { Vector3, Quaternion } from "@oasis-engine/math"; +import { Component } from "../../Component"; +import { Collider } from "../Collider"; +import { dependentComponents } from "../../ComponentsDependencies"; + +/** + * A base class providing common functionality for joints. + * @decorator `@dependentComponents(Collider)` + */ +@dependentComponents(Collider) +export class Joint extends Component { + protected _connectedCollider = new JointCollider(); + protected _collider = new JointCollider(); + protected _nativeJoint: IJoint; + private _force: number = 0; + private _torque: number = 0; + + /** + * The connected collider. + */ + get connectedCollider(): Collider { + return this._connectedCollider.collider; + } + + set connectedCollider(value: Collider) { + if (this._connectedCollider.collider !== value) { + this._connectedCollider.collider = value; + this._nativeJoint.setConnectedCollider(value._nativeCollider); + } + } + + /** + * The connected anchor position. + * @remarks If connectedCollider is set, this anchor is relative offset, or the anchor is world position. + */ + get connectedAnchor(): Vector3 { + return this._connectedCollider.localPosition; + } + + set connectedAnchor(value: Vector3) { + const connectedAnchor = this._connectedCollider.localPosition; + if (value !== connectedAnchor) { + connectedAnchor.copyFrom(value); + } + this._nativeJoint.setConnectedAnchor(value); + } + + /** + * The scale to apply to the inverse mass of collider 0 for resolving this constraint. + */ + get connectedMassScale(): number { + return this._connectedCollider.massScale; + } + + set connectedMassScale(value: number) { + if (value !== this._connectedCollider.massScale) { + this._connectedCollider.massScale = value; + this._nativeJoint.setConnectedMassScale(value); + } + } + + /** + * The scale to apply to the inverse inertia of collider0 for resolving this constraint. + */ + get connectedInertiaScale(): number { + return this._connectedCollider.inertiaScale; + } + + set connectedInertiaScale(value: number) { + if (value !== this._connectedCollider.inertiaScale) { + this._connectedCollider.inertiaScale = value; + this._nativeJoint.setConnectedInertiaScale(value); + } + } + + /** + * The scale to apply to the inverse mass of collider 1 for resolving this constraint. + */ + get massScale(): number { + return this._collider.massScale; + } + + set massScale(value: number) { + if (value !== this._collider.massScale) { + this._collider.massScale = value; + this._nativeJoint.setMassScale(value); + } + } + + /** + * The scale to apply to the inverse inertia of collider1 for resolving this constraint. + */ + get inertiaScale(): number { + return this._collider.inertiaScale; + } + + set inertiaScale(value: number) { + if (value !== this._collider.inertiaScale) { + this._collider.inertiaScale = value; + this._nativeJoint.setInertiaScale(value); + } + } + + /** + * The maximum force the joint can apply before breaking. + */ + get breakForce(): number { + return this._force; + } + + set breakForce(value: number) { + if (value !== this._force) { + this._force = value; + this._nativeJoint.setBreakForce(value); + } + } + + /** + * The maximum torque the joint can apply before breaking. + */ + get breakTorque(): number { + return this._torque; + } + + set breakTorque(value: number) { + if (value !== this._torque) { + this._torque = value; + this._nativeJoint.setBreakTorque(value); + } + } +} + +/** + * @internal + */ +class JointCollider { + collider: Collider = null; + localPosition: Vector3; + localRotation: Quaternion; + massScale: number = 0; + inertiaScale: number = 0; +} diff --git a/packages/core/src/physics/joint/JointLimits.ts b/packages/core/src/physics/joint/JointLimits.ts new file mode 100644 index 0000000000..55836e7d65 --- /dev/null +++ b/packages/core/src/physics/joint/JointLimits.ts @@ -0,0 +1,16 @@ +/** + * JointLimits is used to limit the joints angle. + */ +export class JointLimits { + /** The upper angular limit (in degrees) of the joint. */ + max: number = 0; + /** The lower angular limit (in degrees) of the joint. */ + min: number = 0; + /** Distance inside the limit value at which the limit will be considered to be active by the solver. */ + contactDistance: number = -1; + + /** The spring forces used to reach the target position. */ + stiffness: number = 0; + /** The damper force uses to dampen the spring. */ + damping: number = 0; +} diff --git a/packages/core/src/physics/joint/JointMotor.ts b/packages/core/src/physics/joint/JointMotor.ts new file mode 100644 index 0000000000..e225af4f95 --- /dev/null +++ b/packages/core/src/physics/joint/JointMotor.ts @@ -0,0 +1,13 @@ +/** + * The JointMotor is used to motorize a joint. + */ +export class JointMotor { + /** The motor will apply a force up to force to achieve targetVelocity. */ + targetVelocity: number = 0; + /** The force limit.*/ + forceLimit: number = Number.MAX_VALUE; + /** Gear ration for the motor */ + gearRation: number = 1.0; + /** If freeSpin is enabled the motor will only accelerate but never slow down. */ + freeSpin: boolean = false; +} diff --git a/packages/core/src/physics/joint/SpringJoint.ts b/packages/core/src/physics/joint/SpringJoint.ts new file mode 100644 index 0000000000..efee6abf4f --- /dev/null +++ b/packages/core/src/physics/joint/SpringJoint.ts @@ -0,0 +1,103 @@ +import { Joint } from "./Joint"; +import { ISpringJoint } from "@oasis-engine/design"; +import { PhysicsManager } from "../PhysicsManager"; +import { Collider } from "../Collider"; +import { Vector3 } from "@oasis-engine/math"; + +/** + * A joint that maintains an upper or lower bound (or both) on the distance between two points on different objects. + */ +export class SpringJoint extends Joint { + private _minDistance: number = 0; + private _maxDistance: number = 0; + private _tolerance: number = 0.25; + private _stiffness: number = 0; + private _damping: number = 0; + + /** + * The swing offset. + */ + get swingOffset(): Vector3 { + return this._collider.localPosition; + } + + set swingOffset(value: Vector3) { + const swingOffset = this._collider.localPosition; + if (value !== swingOffset) { + swingOffset.copyFrom(value); + } + (this._nativeJoint).setSwingOffset(value); + } + + /** + * The minimum distance. + */ + get minDistance(): number { + return this._minDistance; + } + + set minDistance(value: number) { + this._minDistance = value; + (this._nativeJoint).setMinDistance(value); + } + + /** + * The maximum distance. + */ + get maxDistance(): number { + return this._maxDistance; + } + + set maxDistance(value: number) { + this._maxDistance = value; + (this._nativeJoint).setMaxDistance(value); + } + + /** + * The distance beyond the allowed range at which the joint becomes active. + */ + get tolerance(): number { + return this._tolerance; + } + + set tolerance(value: number) { + this._tolerance = value; + (this._nativeJoint).setTolerance(value); + } + + /** + * The spring strength of the joint. + */ + get stiffness(): number { + return this._stiffness; + } + + set stiffness(value: number) { + this._stiffness = value; + (this._nativeJoint).setStiffness(value); + } + + /** + * The degree of damping of the joint spring of the joint. + */ + get damping(): number { + return this._damping; + } + + set damping(value: number) { + this._damping = value; + (this._nativeJoint).setDamping(value); + } + + /** + * @override + * @internal + */ + _onAwake() { + const { _connectedCollider: connectedCollider, _collider: collider } = this; + connectedCollider.localPosition = new Vector3(); + collider.localPosition = new Vector3(); + collider.collider = this.entity.getComponent(Collider); + this._nativeJoint = PhysicsManager._nativePhysics.createSpringJoint(collider.collider._nativeCollider); + } +} diff --git a/packages/core/src/physics/joint/index.ts b/packages/core/src/physics/joint/index.ts new file mode 100644 index 0000000000..2ffd2e3cb2 --- /dev/null +++ b/packages/core/src/physics/joint/index.ts @@ -0,0 +1,7 @@ +export { Joint } from "./Joint"; +export { FixedJoint } from "./FixedJoint"; +export { HingeJoint } from "./HingeJoint"; +export { SpringJoint } from "./SpringJoint"; + +export { JointLimits } from "./JointLimits"; +export { JointMotor } from "./JointMotor"; diff --git a/packages/core/src/shaderlib/pbr/ibl_frag_define.glsl b/packages/core/src/shaderlib/pbr/ibl_frag_define.glsl index 9f46499bee..786293b776 100644 --- a/packages/core/src/shaderlib/pbr/ibl_frag_define.glsl +++ b/packages/core/src/shaderlib/pbr/ibl_frag_define.glsl @@ -2,6 +2,7 @@ // sh need be pre-scaled in CPU. vec3 getLightProbeIrradiance(vec3 sh[9], vec3 normal){ + normal.x = -normal.x; vec3 result = sh[0] + sh[1] * (normal.y) + diff --git a/packages/design/package.json b/packages/design/package.json index dc05beb4d1..27b4fe82f4 100755 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,6 +1,10 @@ { "name": "@oasis-engine/design", - "version": "0.7.0-beta.7", + "version": "0.8.0-alpha.3", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", @@ -14,6 +18,6 @@ ], "types": "types/index.d.ts", "dependencies": { - "@oasis-engine/math": "0.7.0-beta.7" + "@oasis-engine/math": "0.8.0-alpha.3" } } diff --git a/packages/design/src/physics/IPhysics.ts b/packages/design/src/physics/IPhysics.ts index d5badf7399..ad90d00da2 100644 --- a/packages/design/src/physics/IPhysics.ts +++ b/packages/design/src/physics/IPhysics.ts @@ -4,7 +4,9 @@ import { IBoxColliderShape, ISphereColliderShape, ICapsuleColliderShape, IPlaneC import { IDynamicCollider } from "./IDynamicCollider"; import { IStaticCollider } from "./IStaticCollider"; import { Quaternion, Vector3 } from "@oasis-engine/math"; +import { ICollider } from "./ICollider"; import { ICharacterController } from "./ICharacterController"; +import { IFixedJoint, IHingeJoint, ISpringJoint } from "./joints"; /** * The interface of physics creation. @@ -99,4 +101,22 @@ export interface IPhysics { height: number, material: IPhysicsMaterial ): ICapsuleColliderShape; + + /** + * Create fixed joint. + * @param collider - Affector of joint + */ + createFixedJoint(collider: ICollider): IFixedJoint; + + /** + * Create hinge joint. + * @param collider - Affector of joint + */ + createHingeJoint(collider: ICollider): IHingeJoint; + + /** + * Create spring joint + * @param collider - Affector of joint + */ + createSpringJoint(collider: ICollider): ISpringJoint; } diff --git a/packages/design/src/physics/index.ts b/packages/design/src/physics/index.ts index b73c672b12..334b731bc4 100644 --- a/packages/design/src/physics/index.ts +++ b/packages/design/src/physics/index.ts @@ -6,3 +6,4 @@ export type { IDynamicCollider } from "./IDynamicCollider"; export type { IPhysicsMaterial } from "./IPhysicsMaterial"; export type { ICharacterController } from "./ICharacterController"; export * from "./shape/index"; +export * from "./joints/index"; diff --git a/packages/design/src/physics/joints/IFixedJoint.ts b/packages/design/src/physics/joints/IFixedJoint.ts new file mode 100644 index 0000000000..d35cafb171 --- /dev/null +++ b/packages/design/src/physics/joints/IFixedJoint.ts @@ -0,0 +1,6 @@ +import { IJoint } from "./IJoint"; + +/* + A fixed joint permits no relative movement between two colliders. ie the colliders are glued together. + */ +export interface IFixedJoint extends IJoint {} diff --git a/packages/design/src/physics/joints/IHingeJoint.ts b/packages/design/src/physics/joints/IHingeJoint.ts new file mode 100644 index 0000000000..2e08154f12 --- /dev/null +++ b/packages/design/src/physics/joints/IHingeJoint.ts @@ -0,0 +1,69 @@ +import { IJoint } from "./IJoint"; +import { Vector3 } from "@oasis-engine/math"; + +/** + * A joint which behaves in a similar way to a hinge or axle. + */ +export interface IHingeJoint extends IJoint { + /** + * The anchor rotation. + */ + setAxis(value: Vector3): void; + + /** + * The swing offset. + */ + setSwingOffset(value: Vector3): void; + + /** + * The current angle in degrees of the joint relative to its rest position. + */ + getAngle(): number; + + /** + * The angular velocity of the joint in degrees per second. + */ + getVelocity(): Readonly; + + /** + * Set a cone hard limit. + * @param lowerLimit The lower angle of the limit + * @param upperLimit The upper angle of the limit + * @param contactDist The distance from the limit at which it becomes active. Default is the lesser of 0.1 radians, and 0.49 * the lower of the limit angles + */ + setHardLimit(lowerLimit: number, upperLimit: number, contactDist: number): void; + + /** + * Set a cone soft limit. + * @param lowerLimit The lower angle of the limit + * @param upperLimit The upper angle of the limit + * @param stiffness the spring strength of the drive + * @param damping the damping strength of the drive + */ + setSoftLimit(lowerLimit: number, upperLimit: number, stiffness: number, damping: number): void; + + /** + * set the target velocity for the drive model. + * @param velocity the drive target velocity + */ + setDriveVelocity(velocity: number): void; + + /** + * sets the maximum torque the drive can exert. + * @param limit the maximum torque + */ + setDriveForceLimit(limit: number): void; + + /** + * sets the gear ratio for the drive. + * @param ratio the gear ratio + */ + setDriveGearRatio(ratio: number): void; + + /** + * sets a single flag specific to a Hinge Joint. + * @param flag The flag to set or clear. + * @param value the value to which to set the flag + */ + setHingeJointFlag(flag: number, value: boolean): void; +} diff --git a/packages/design/src/physics/joints/IJoint.ts b/packages/design/src/physics/joints/IJoint.ts new file mode 100644 index 0000000000..d88a551e9a --- /dev/null +++ b/packages/design/src/physics/joints/IJoint.ts @@ -0,0 +1,48 @@ +import { Vector3 } from "@oasis-engine/math"; +import { ICollider } from "../ICollider"; + +/** + * a base interface providing common functionality for joints. + */ +export interface IJoint { + /** + * The connected collider. + */ + setConnectedCollider(value: ICollider): void; + + /** + * The connected anchor position. + * @remarks If connectedCollider is set, this anchor is relative offset, or the anchor is world position. + */ + setConnectedAnchor(value: Vector3): void; + + /** + * The scale to apply to the inverse mass of collider 0 for resolving this constraint. + */ + setConnectedMassScale(value: number): void; + + /** + * The scale to apply to the inverse inertia of collider0 for resolving this constraint. + */ + setConnectedInertiaScale(value: number): void; + + /** + * The scale to apply to the inverse mass of collider 1 for resolving this constraint. + */ + setMassScale(value: number): void; + + /** + * The scale to apply to the inverse inertia of collider1 for resolving this constraint. + */ + setInertiaScale(value: number): void; + + /** + * The maximum force the joint can apply before breaking. + */ + setBreakForce(value: number): void; + + /** + * The maximum torque the joint can apply before breaking. + */ + setBreakTorque(value: number): void; +} diff --git a/packages/design/src/physics/joints/ISpringJoint.ts b/packages/design/src/physics/joints/ISpringJoint.ts new file mode 100644 index 0000000000..e8f4ecd469 --- /dev/null +++ b/packages/design/src/physics/joints/ISpringJoint.ts @@ -0,0 +1,42 @@ +import { IJoint } from "./IJoint"; +import { Vector3 } from "@oasis-engine/math"; + +/** + * a joint that maintains an upper or lower bound (or both) on the distance between two points on different objects + */ +export interface ISpringJoint extends IJoint { + /** + * The swing offset. + */ + setSwingOffset(value: Vector3): void; + + /** + * Set the allowed minimum distance for the joint. + * @param distance the minimum distance + */ + setMinDistance(distance: number): void; + + /** + * Set the allowed maximum distance for the joint. + * @param distance the maximum distance + */ + setMaxDistance(distance: number): void; + + /** + * Set the error tolerance of the joint. + * @param tolerance the distance beyond the allowed range at which the joint becomes active + */ + setTolerance(tolerance: number): void; + + /** + * Set the strength of the joint spring. + * @param stiffness the spring strength of the joint + */ + setStiffness(stiffness: number): void; + + /** + * Set the damping of the joint spring. + * @param damping the degree of damping of the joint spring of the joint + */ + setDamping(damping: number): void; +} diff --git a/packages/design/src/physics/joints/index.ts b/packages/design/src/physics/joints/index.ts new file mode 100644 index 0000000000..6a0b39f4e6 --- /dev/null +++ b/packages/design/src/physics/joints/index.ts @@ -0,0 +1,4 @@ +export type { IJoint } from "./IJoint"; +export type { IFixedJoint } from "./IFixedJoint"; +export type { IHingeJoint } from "./IHingeJoint"; +export type { ISpringJoint } from "./ISpringJoint"; diff --git a/packages/draco/package.json b/packages/draco/package.json index be37276ec0..8ef529e99f 100644 --- a/packages/draco/package.json +++ b/packages/draco/package.json @@ -1,6 +1,10 @@ { "name": "@oasis-engine/draco", - "version": "0.7.0-beta.7", + "version": "0.8.0-alpha.3", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "license": "MIT", "scripts": { "b:types": "tsc", @@ -14,6 +18,6 @@ "types/**/*" ], "dependencies": { - "@oasis-engine/core": "0.7.0-beta.7" + "@oasis-engine/core": "0.8.0-alpha.3" } } diff --git a/packages/loader/package.json b/packages/loader/package.json index a0a2733fb8..74cf9fbaf4 100755 --- a/packages/loader/package.json +++ b/packages/loader/package.json @@ -1,6 +1,10 @@ { "name": "@oasis-engine/loader", - "version": "0.7.0-beta.7", + "version": "0.8.0-alpha.3", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "license": "MIT", "types": "types/index.d.ts", "scripts": { @@ -15,10 +19,9 @@ "types/**/*" ], "dependencies": { - "@oasis-engine/resource-process": "0.7.0-beta.7", - "@oasis-engine/core": "0.7.0-beta.7", - "@oasis-engine/draco": "0.7.0-beta.7", - "@oasis-engine/math": "0.7.0-beta.7", - "@oasis-engine/rhi-webgl": "0.7.0-beta.7" + "@oasis-engine/core": "0.8.0-alpha.3", + "@oasis-engine/draco": "0.8.0-alpha.3", + "@oasis-engine/math": "0.8.0-alpha.3", + "@oasis-engine/rhi-webgl": "0.8.0-alpha.3" } } diff --git a/packages/loader/src/SpriteAtlasLoader.ts b/packages/loader/src/SpriteAtlasLoader.ts index 3101d08456..95b6d5d621 100644 --- a/packages/loader/src/SpriteAtlasLoader.ts +++ b/packages/loader/src/SpriteAtlasLoader.ts @@ -1,7 +1,13 @@ import { AssetPromise, - AssetType, Loader, LoadItem, resourceLoader, ResourceManager, Sprite, - SpriteAtlas, Texture2D + AssetType, + Loader, + LoadItem, + resourceLoader, + ResourceManager, + Sprite, + SpriteAtlas, + Texture2D } from "@oasis-engine/core"; import { AtlasConfig } from "@oasis-engine/core/types/2d/atlas/types"; import { Rect, Vector2 } from "@oasis-engine/math"; @@ -9,6 +15,8 @@ import { GLTFUtil } from "./gltf/GLTFUtil"; @resourceLoader(AssetType.SpriteAtlas, ["atlas"], false) class SpriteAtlasLoader extends Loader { + private _tempRect: Rect = new Rect(); + private _tempVec2: Vector2 = new Vector2(); load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { return new AssetPromise((resolve, reject) => { this.request(item.url, { @@ -28,8 +36,7 @@ class SpriteAtlasLoader extends Loader { ).then((imgs) => { const { engine } = resourceManager; // Generate a SpriteAtlas object. - const tempRect = new Rect(); - const tempVect2 = new Vector2(); + const { _tempRect: tempRect, _tempVec2: tempVec2 } = this; const spriteAtlas = new SpriteAtlas(engine); for (let i = 0; i < atlasItemsLen; i++) { // Generate Texture2D according to configuration. @@ -45,14 +52,13 @@ class SpriteAtlasLoader extends Loader { const sourceHeightReciprocal = 1.0 / height; for (let j = sprites.length - 1; j >= 0; j--) { const atlasSprite = sprites[j]; - // @ts-ignore - const { region, pivot, atlasRegionOffset, atlasRegion, id } = atlasSprite; + const { region, atlasRegionOffset, atlasRegion, id, pivot } = atlasSprite; const sprite = new Sprite( engine, texture, region ? tempRect.set(region.x, region.y, region.w, region.h) : undefined, - pivot ? tempVect2.set(pivot.x, pivot.y) : undefined, - atlasSprite.pixelsPerUnit || undefined, + pivot ? tempVec2.set(pivot.x, pivot.y) : undefined, + undefined, atlasSprite.name ); sprite.atlasRegion.set( @@ -64,19 +70,11 @@ class SpriteAtlasLoader extends Loader { atlasSprite.atlasRotated && (sprite.atlasRotated = true); if (atlasRegionOffset) { const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = atlasRegionOffset; - let originalWReciprocal: number, originalHReciprocal: number; - if (atlasSprite.atlasRotated) { - originalWReciprocal = 1 / (offsetLeft + atlasRegion.h + offsetRight); - originalHReciprocal = 1 / (offsetTop + atlasRegion.w + offsetBottom); - } else { - originalWReciprocal = 1 / (offsetLeft + atlasRegion.w + offsetRight); - originalHReciprocal = 1 / (offsetTop + atlasRegion.h + offsetBottom); - } sprite.atlasRegionOffset.set( - offsetLeft * originalWReciprocal, - offsetTop * originalHReciprocal, - offsetRight * originalWReciprocal, - offsetBottom * originalHReciprocal + offsetLeft * sourceWidthReciprocal, + offsetTop * sourceHeightReciprocal, + offsetRight * sourceWidthReciprocal, + offsetBottom * sourceHeightReciprocal ); } if (id !== undefined) { diff --git a/packages/math/package.json b/packages/math/package.json index 67f64fcc40..6ff2a1d12f 100755 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -1,6 +1,10 @@ { "name": "@oasis-engine/math", - "version": "0.7.0-beta.7", + "version": "0.8.0-alpha.3", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", diff --git a/packages/math/src/CollisionUtil.ts b/packages/math/src/CollisionUtil.ts index 350ee14520..9eb68efe89 100644 --- a/packages/math/src/CollisionUtil.ts +++ b/packages/math/src/CollisionUtil.ts @@ -324,7 +324,7 @@ export class CollisionUtil { const normal = plane.normal; back.set(normal.x >= 0 ? min.x : max.x, normal.y >= 0 ? min.y : max.y, normal.z >= 0 ? min.z : max.z); - if (Vector3.dot(plane.normal, back) > -plane.distance) { + if (Vector3.dot(normal, back) > -plane.distance) { return false; } } diff --git a/packages/math/src/Color.ts b/packages/math/src/Color.ts index e4fa91e5cb..8edbd21ff4 100644 --- a/packages/math/src/Color.ts +++ b/packages/math/src/Color.ts @@ -216,3 +216,14 @@ interface ColorLike { /** {@inheritDoc Color.a} */ a: number; } + +interface ColorLike { + /** {@inheritDoc Color.r} */ + r: number; + /** {@inheritDoc Color.g} */ + g: number; + /** {@inheritDoc Color.b} */ + b: number; + /** {@inheritDoc Color.a} */ + a: number; +} diff --git a/packages/math/src/Vector2.ts b/packages/math/src/Vector2.ts index 9cae563497..fb14ba63e6 100644 --- a/packages/math/src/Vector2.ts +++ b/packages/math/src/Vector2.ts @@ -365,17 +365,6 @@ export class Vector2 implements IClone, ICopy { return this; } - toObject() { - return { - x: this.x, - y: this.y - }; - } - - setFromObject(obj: { x: number; y: number }) { - this.x = obj.x; - this.y = obj.y; - } /** * Copy the value of this vector to an array. * @param out - The array @@ -385,6 +374,13 @@ export class Vector2 implements IClone, ICopy { out[outOffset] = this._x; out[outOffset + 1] = this._y; } + + toObject() { + return { + x: this.x, + y: this.y + }; + } } interface Vector2Like { diff --git a/packages/math/src/Vector3.ts b/packages/math/src/Vector3.ts index c41d2de1dc..e63fba2ba4 100644 --- a/packages/math/src/Vector3.ts +++ b/packages/math/src/Vector3.ts @@ -540,9 +540,6 @@ export class Vector3 implements IClone, ICopy { return this; } - toObject() { - return { x: this.x, y: this.y, z: this.z }; - } /** * Creates a clone of this vector. * @returns A clone of this vector @@ -588,6 +585,10 @@ export class Vector3 implements IClone, ICopy { out[outOffset + 1] = this._y; out[outOffset + 2] = this._z; } + + toObject() { + return { x: this.x, y: this.y, z: this.z }; + } } interface Vector3Like { diff --git a/packages/math/src/Vector4.ts b/packages/math/src/Vector4.ts index 82ba70a40d..87069ac08d 100644 --- a/packages/math/src/Vector4.ts +++ b/packages/math/src/Vector4.ts @@ -492,14 +492,6 @@ export class Vector4 implements IClone, ICopy { return this; } - toObject() { - return { - x: this.x, - y: this.y, - z: this.z, - w: this.w - }; - } /** * Copy the value of this vector to an array. * @param out - The array @@ -511,6 +503,15 @@ export class Vector4 implements IClone, ICopy { out[outOffset + 2] = this._z; out[outOffset + 3] = this._w; } + + toObject() { + return { + x: this.x, + y: this.y, + z: this.z, + w: this.w + }; + } } interface Vector4Like { diff --git a/packages/oasis-engine/package.json b/packages/oasis-engine/package.json index 8be0130bcd..e1d14fa39e 100755 --- a/packages/oasis-engine/package.json +++ b/packages/oasis-engine/package.json @@ -1,6 +1,10 @@ { "name": "oasis-engine", - "version": "0.7.0-beta.7", + "version": "0.8.0-alpha.3", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "license": "MIT", "scripts": { "b:types": "tsc", @@ -15,9 +19,9 @@ "types/**/*" ], "dependencies": { - "@oasis-engine/core": "0.7.0-beta.7", - "@oasis-engine/loader": "0.7.0-beta.7", - "@oasis-engine/math": "0.7.0-beta.7", - "@oasis-engine/rhi-webgl": "0.7.0-beta.7" + "@oasis-engine/core": "0.8.0-alpha.3", + "@oasis-engine/loader": "0.8.0-alpha.3", + "@oasis-engine/math": "0.8.0-alpha.3", + "@oasis-engine/rhi-webgl": "0.8.0-alpha.3" } } diff --git a/packages/physics-lite/package.json b/packages/physics-lite/package.json index c4fe9fc769..990a5877a5 100644 --- a/packages/physics-lite/package.json +++ b/packages/physics-lite/package.json @@ -1,6 +1,10 @@ { "name": "@oasis-engine/physics-lite", - "version": "0.7.0-beta.7", + "version": "0.8.0-alpha.3", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", @@ -16,10 +20,7 @@ "types/**/*" ], "dependencies": { - "@oasis-engine/design": "0.7.0-beta.7", - "oasis-engine": "0.7.0-beta.7" - }, - "publishConfig": { - "access": "public" + "@oasis-engine/design": "0.8.0-alpha.3", + "oasis-engine": "0.8.0-alpha.3" } } diff --git a/packages/physics-lite/src/LitePhysics.ts b/packages/physics-lite/src/LitePhysics.ts index 858037c8ef..6cc92369b3 100644 --- a/packages/physics-lite/src/LitePhysics.ts +++ b/packages/physics-lite/src/LitePhysics.ts @@ -8,7 +8,10 @@ import { IPhysicsMaterial, IPlaneColliderShape, ISphereColliderShape, - IStaticCollider + IStaticCollider, + IFixedJoint, + IHingeJoint, + ISpringJoint, } from "@oasis-engine/design"; import { Quaternion, Vector3 } from "oasis-engine"; import { LiteDynamicCollider } from "./LiteDynamicCollider"; @@ -17,6 +20,7 @@ import { LitePhysicsMaterial } from "./LitePhysicsMaterial"; import { LiteStaticCollider } from "./LiteStaticCollider"; import { LiteBoxColliderShape } from "./shape/LiteBoxColliderShape"; import { LiteSphereColliderShape } from "./shape/LiteSphereColliderShape"; +import { LiteCollider } from "./LiteCollider"; import { StaticInterfaceImplement } from "./StaticInterfaceImplement"; @StaticInterfaceImplement() @@ -112,4 +116,25 @@ export class LitePhysics { ): ICapsuleColliderShape { throw "Physics-lite don't support CapsuleColliderShape. Use Physics-PhysX instead!"; } + + /** + * {@inheritDoc IPhysics.createFixedJoint } + */ + static createFixedJoint(collider: LiteCollider): IFixedJoint { + throw "Physics-lite don't support CapsuleColliderShape. Use Physics-PhysX instead!"; + } + + /** + * {@inheritDoc IPhysics.createHingeJoint } + */ + static createHingeJoint(collider: LiteCollider): IHingeJoint { + throw "Physics-lite don't support CapsuleColliderShape. Use Physics-PhysX instead!"; + } + + /** + * {@inheritDoc IPhysics.createSpringJoint } + */ + static createSpringJoint(collider: LiteCollider): ISpringJoint { + throw "Physics-lite don't support CapsuleColliderShape. Use Physics-PhysX instead!"; + } } diff --git a/packages/physics-lite/src/LitePhysicsManager.ts b/packages/physics-lite/src/LitePhysicsManager.ts index c7e9d36488..1093086002 100644 --- a/packages/physics-lite/src/LitePhysicsManager.ts +++ b/packages/physics-lite/src/LitePhysicsManager.ts @@ -8,7 +8,7 @@ import { LiteColliderShape } from "./shape/LiteColliderShape"; import { LiteSphereColliderShape } from "./shape/LiteSphereColliderShape"; /** - * A manager is a collection of bodies and constraints which can interact. + * A manager is a collection of colliders and constraints which can interact. */ export class LitePhysicsManager implements IPhysicsManager { private static _tempSphere: BoundingSphere = new BoundingSphere(); diff --git a/packages/physics-physx/package.json b/packages/physics-physx/package.json index 4806e541dd..8358b4e320 100644 --- a/packages/physics-physx/package.json +++ b/packages/physics-physx/package.json @@ -1,6 +1,10 @@ { "name": "@oasis-engine/physics-physx", - "version": "0.7.0-beta.7", + "version": "0.8.0-alpha.3", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", @@ -16,10 +20,7 @@ "types/**/*" ], "dependencies": { - "@oasis-engine/design": "0.7.0-beta.7", - "oasis-engine": "0.7.0-beta.7" - }, - "publishConfig": { - "access": "public" + "@oasis-engine/design": "0.8.0-alpha.3", + "oasis-engine": "0.8.0-alpha.3" } } diff --git a/packages/physics-physx/src/PhysXPhysics.ts b/packages/physics-physx/src/PhysXPhysics.ts index f5042a6e8c..8665354e7e 100644 --- a/packages/physics-physx/src/PhysXPhysics.ts +++ b/packages/physics-physx/src/PhysXPhysics.ts @@ -8,7 +8,10 @@ import { IPhysicsMaterial, IPlaneColliderShape, ISphereColliderShape, - IStaticCollider + IStaticCollider, + IFixedJoint, + IHingeJoint, + ISpringJoint } from "@oasis-engine/design"; import { Quaternion, Vector3 } from "oasis-engine"; import { PhysXRuntimeMode } from "./enum/PhysXRuntimeMode"; @@ -21,7 +24,11 @@ import { PhysXBoxColliderShape } from "./shape/PhysXBoxColliderShape"; import { PhysXCapsuleColliderShape } from "./shape/PhysXCapsuleColliderShape"; import { PhysXPlaneColliderShape } from "./shape/PhysXPlaneColliderShape"; import { PhysXSphereColliderShape } from "./shape/PhysXSphereColliderShape"; +import { PhysXFixedJoint } from "./joint/PhysXFixedJoint"; +import { PhysXHingeJoint } from "./joint/PhysXHingeJoint"; +import { PhysXSpringJoint } from "./joint/PhysXSpringJoint"; import { StaticInterfaceImplement } from "./StaticInterfaceImplement"; +import { PhysXCollider } from "./PhysXCollider"; /** * PhysX object creation. @@ -186,6 +193,27 @@ export class PhysXPhysics { return new PhysXCapsuleColliderShape(uniqueID, radius, height, material); } + /** + * {@inheritDoc IPhysics.createFixedJoint } + */ + static createFixedJoint(collider: PhysXCollider): IFixedJoint { + return new PhysXFixedJoint(collider); + } + + /** + * {@inheritDoc IPhysics.createHingeJoint } + */ + static createHingeJoint(collider: PhysXCollider): IHingeJoint { + return new PhysXHingeJoint(collider); + } + + /** + * {@inheritDoc IPhysics.createSpringJoint } + */ + static createSpringJoint(collider: PhysXCollider): ISpringJoint { + return new PhysXSpringJoint(collider); + } + private static _init(PHYSX: any): void { PhysXPhysics._physX = PHYSX; const version = PhysXPhysics._physX.PX_PHYSICS_VERSION; diff --git a/packages/physics-physx/src/PhysXPhysicsManager.ts b/packages/physics-physx/src/PhysXPhysicsManager.ts index 8ca699c03d..92c17163d5 100644 --- a/packages/physics-physx/src/PhysXPhysicsManager.ts +++ b/packages/physics-physx/src/PhysXPhysicsManager.ts @@ -7,7 +7,7 @@ import { PhysXPhysics } from "./PhysXPhysics"; import { PhysXColliderShape } from "./shape/PhysXColliderShape"; /** - * A manager is a collection of bodies and constraints which can interact. + * A manager is a collection of colliders and constraints which can interact. */ export class PhysXPhysicsManager implements IPhysicsManager { /** @internal */ diff --git a/packages/physics-physx/src/joint/PhysXFixedJoint.ts b/packages/physics-physx/src/joint/PhysXFixedJoint.ts new file mode 100644 index 0000000000..ad9c17a452 --- /dev/null +++ b/packages/physics-physx/src/joint/PhysXFixedJoint.ts @@ -0,0 +1,22 @@ +import { PhysXJoint } from "./PhysXJoint"; +import { IFixedJoint } from "@oasis-engine/design"; +import { PhysXCollider } from "../PhysXCollider"; +import { PhysXPhysics } from "../PhysXPhysics"; + +/** + * A fixed joint permits no relative movement between two colliders. ie the bodies are glued together. + */ +export class PhysXFixedJoint extends PhysXJoint implements IFixedJoint { + constructor(collider: PhysXCollider) { + super(); + this._collider = collider; + this._pxJoint = PhysXPhysics._pxPhysics.createFixedJoint( + null, + PhysXJoint._tempVector, + PhysXJoint._tempQuat, + collider._pxActor, + PhysXJoint._tempVector, + PhysXJoint._tempQuat + ); + } +} diff --git a/packages/physics-physx/src/joint/PhysXHingeJoint.ts b/packages/physics-physx/src/joint/PhysXHingeJoint.ts new file mode 100644 index 0000000000..33903addc7 --- /dev/null +++ b/packages/physics-physx/src/joint/PhysXHingeJoint.ts @@ -0,0 +1,108 @@ +import { PhysXCollider } from "../PhysXCollider"; +import { PhysXJoint } from "./PhysXJoint"; +import { IHingeJoint } from "@oasis-engine/design"; +import { PhysXPhysics } from "../PhysXPhysics"; +import { Quaternion, Vector3 } from "oasis-engine"; + +/** + * A joint which behaves in a similar way to a hinge or axle. + */ +export class PhysXHingeJoint extends PhysXJoint implements IHingeJoint { + private _axisRotationQuaternion = new Quaternion(); + private _swingOffset = new Vector3(); + private _velocity = new Vector3(); + + constructor(collider: PhysXCollider) { + super(); + this._collider = collider; + this._pxJoint = PhysXPhysics._pxPhysics.createRevoluteJoint( + null, + PhysXJoint._tempVector, + PhysXJoint._tempQuat, + collider._pxActor, + PhysXJoint._tempVector, + PhysXJoint._tempQuat + ); + } + + /** + * {@inheritDoc IHingeJoint.setAxis } + */ + setAxis(value: Vector3): void { + const tempVector = PhysXJoint._tempVector; + const axisRotationQuaternion = this._axisRotationQuaternion; + tempVector.set(1, 0, 0); + value.normalize(); + const angle = Math.acos(Vector3.dot(tempVector, value)); + Vector3.cross(tempVector, value, tempVector); + Quaternion.rotationAxisAngle(tempVector, angle, axisRotationQuaternion); + + this._setLocalPose(0, this._swingOffset, axisRotationQuaternion); + } + + /** + * {@inheritDoc IHingeJoint.setSwingOffset } + */ + setSwingOffset(value: Vector3): void { + this._swingOffset.copyFrom(value); + this._setLocalPose(1, this._swingOffset, this._axisRotationQuaternion); + } + + /** + * {@inheritDoc IHingeJoint.getAngle } + */ + getAngle(): number { + return this._pxJoint.getAngle(); + } + + /** + * {@inheritDoc IHingeJoint.getVelocity } + */ + getVelocity(): Readonly { + const velocity = this._velocity; + velocity.copyFrom(this._pxJoint.getVelocity()); + return velocity; + } + + /** + * {@inheritDoc IHingeJoint.setHardLimitCone } + */ + setHardLimit(lowerLimit: number, upperLimit: number, contactDist: number): void { + this._pxJoint.setHardLimit(lowerLimit, upperLimit, contactDist); + } + + /** + * {@inheritDoc IHingeJoint.setHardLimitCone } + */ + setSoftLimit(lowerLimit: number, upperLimit: number, stiffness: number, damping: number): void { + this._pxJoint.setSoftLimit(lowerLimit, upperLimit, stiffness, damping); + } + + /** + * {@inheritDoc IHingeJoint.setDriveVelocity } + */ + setDriveVelocity(velocity: number): void { + this._pxJoint.setDriveVelocity(velocity); + } + + /** + * {@inheritDoc IHingeJoint.setDriveForceLimit } + */ + setDriveForceLimit(limit: number): void { + this._pxJoint.setDriveForceLimit(limit); + } + + /** + * {@inheritDoc IHingeJoint.setDriveGearRatio } + */ + setDriveGearRatio(ratio: number): void { + this._pxJoint.setDriveGearRatio(ratio); + } + + /** + * {@inheritDoc IHingeJoint.setHingeJointFlag } + */ + setHingeJointFlag(flag: number, value: boolean): void { + this._pxJoint.setRevoluteJointFlag(flag, value); + } +} diff --git a/packages/physics-physx/src/joint/PhysXJoint.ts b/packages/physics-physx/src/joint/PhysXJoint.ts new file mode 100644 index 0000000000..b54a1c72aa --- /dev/null +++ b/packages/physics-physx/src/joint/PhysXJoint.ts @@ -0,0 +1,86 @@ +import { IJoint } from "@oasis-engine/design"; +import { PhysXCollider } from "../PhysXCollider"; +import { Quaternion, Vector3 } from "oasis-engine"; + +/** + * a base interface providing common functionality for PhysX joints + */ +export class PhysXJoint implements IJoint { + protected static _tempVector = new Vector3(1, 0, 0); + protected static _tempQuat = new Quaternion(); + + protected _pxJoint: any; + protected _collider: PhysXCollider; + private _connectedAnchor = new Vector3(); + private _breakForce: number = Number.MAX_VALUE; + private _breakTorque: number = Number.MAX_VALUE; + + /** + * {@inheritDoc IJoint.setConnectedCollider } + */ + setConnectedCollider(value: PhysXCollider): void { + this._pxJoint.setActors(value?._pxActor || null, this._collider?._pxActor || null); + } + + /** + * {@inheritDoc IJoint.setConnectedAnchor } + */ + setConnectedAnchor(value: Vector3): void { + this._connectedAnchor.copyFrom(value); + this._setLocalPose(0, value, PhysXJoint._tempQuat); + } + + /** + * {@inheritDoc IJoint.setConnectedMassScale } + */ + setConnectedMassScale(value: number): void { + this._pxJoint.setInvMassScale0(1 / value); + } + + /** + * {@inheritDoc IJoint.setConnectedInertiaScale } + */ + setConnectedInertiaScale(value: number): void { + this._pxJoint.setInvInertiaScale0(1 / value); + } + + /** + * {@inheritDoc IJoint.setMassScale } + */ + setMassScale(value: number): void { + this._pxJoint.setInvMassScale1(1 / value); + } + + /** + * {@inheritDoc IJoint.setInertiaScale } + */ + setInertiaScale(value: number): void { + this._pxJoint.setInvInertiaScale1(1 / value); + } + + /** + * {@inheritDoc IJoint.setBreakForce } + */ + setBreakForce(value: number): void { + this._breakForce = value; + this._pxJoint.setBreakForce(this._breakForce, this._breakTorque); + } + + /** + * {@inheritDoc IJoint.setBreakTorque } + */ + setBreakTorque(value: number): void { + this._breakTorque = value; + this._pxJoint.setBreakForce(this._breakForce, this._breakTorque); + } + + /** + * Set the joint local pose for an actor. + * @param actor 0 for the first actor, 1 for the second actor. + * @param position the local position for the actor this joint + * @param rotation the local rotation for the actor this joint + */ + protected _setLocalPose(actor: number, position: Vector3, rotation: Quaternion): void { + this._pxJoint.setLocalPose(actor, position, rotation); + } +} diff --git a/packages/physics-physx/src/joint/PhysXSpringJoint.ts b/packages/physics-physx/src/joint/PhysXSpringJoint.ts new file mode 100644 index 0000000000..e09ae9a525 --- /dev/null +++ b/packages/physics-physx/src/joint/PhysXSpringJoint.ts @@ -0,0 +1,71 @@ +import { PhysXPhysics } from "../PhysXPhysics"; +import { PhysXJoint } from "./PhysXJoint"; +import { ISpringJoint } from "@oasis-engine/design"; +import { PhysXCollider } from "../PhysXCollider"; +import { Vector3 } from "oasis-engine"; + +/** + * a joint that maintains an upper or lower bound (or both) on the distance between two points on different objects + */ +export class PhysXSpringJoint extends PhysXJoint implements ISpringJoint { + private _swingOffset = new Vector3(); + + constructor(collider: PhysXCollider) { + super(); + this._collider = collider; + this._pxJoint = PhysXPhysics._pxPhysics.createDistanceJoint( + null, + PhysXJoint._tempVector, + PhysXJoint._tempQuat, + collider._pxActor, + PhysXJoint._tempVector, + PhysXJoint._tempQuat + ); + this._pxJoint.setDistanceJointFlag(1, true); // enable max distance; + this._pxJoint.setDistanceJointFlag(2, true); // enable min distance; + this._pxJoint.setDistanceJointFlag(4, true); // enable spring; + } + + /** + * {@inheritDoc ISpringJoint.setSwingOffset } + */ + setSwingOffset(value: Vector3): void { + this._swingOffset.copyFrom(value); + this._setLocalPose(1, value, PhysXJoint._tempQuat); + } + + /** + * {@inheritDoc ISpringJoint.setMinDistance } + */ + setMinDistance(distance: number): void { + this._pxJoint.setMinDistance(distance); + } + + /** + * {@inheritDoc ISpringJoint.setMaxDistance } + */ + setMaxDistance(distance: number): void { + this._pxJoint.setMaxDistance(distance); + } + + /** + * {@inheritDoc ISpringJoint.setTolerance } + */ + setTolerance(tolerance: number): void { + this._pxJoint.setTolerance(tolerance); + } + + /** + * {@inheritDoc ISpringJoint.setStiffness } + */ + setStiffness(stiffness: number): void { + this._pxJoint.setStiffness(stiffness); + } + + /** + * {@inheritDoc ISpringJoint.setDamping } + */ + setDamping(damping: number): void { + this._pxJoint.setDamping(damping); + } +} diff --git a/packages/rhi-webgl/package.json b/packages/rhi-webgl/package.json index 47e8f15872..d7b7b317da 100755 --- a/packages/rhi-webgl/package.json +++ b/packages/rhi-webgl/package.json @@ -1,6 +1,10 @@ { "name": "@oasis-engine/rhi-webgl", - "version": "0.7.0-beta.7", + "version": "0.8.0-alpha.3", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", @@ -15,10 +19,10 @@ "types/**/*" ], "dependencies": { - "@oasis-engine/core": "0.7.0-beta.7", - "@oasis-engine/math": "0.7.0-beta.7" + "@oasis-engine/core": "0.8.0-alpha.3", + "@oasis-engine/math": "0.8.0-alpha.3" }, "devDependencies": { - "@oasis-engine/design": "0.7.0-beta.7" + "@oasis-engine/design": "0.8.0-alpha.3" } } diff --git a/packages/rhi-webgl/src/WebGLRenderer.ts b/packages/rhi-webgl/src/WebGLRenderer.ts index 63498418ee..b6ce70250e 100644 --- a/packages/rhi-webgl/src/WebGLRenderer.ts +++ b/packages/rhi-webgl/src/WebGLRenderer.ts @@ -66,6 +66,7 @@ export class WebGLRenderer implements IHardwareRenderer { private _extensions; private _capability: GLCapability; private _isWebGL2: boolean; + private _webCanvas: WebCanvas; private _activeTextureID: number; private _activeTextures: GLTexture[] = new Array(32); @@ -73,6 +74,7 @@ export class WebGLRenderer implements IHardwareRenderer { // cache value private _lastViewport: Vector4 = new Vector4(null, null, null, null); private _lastClearColor: Color = new Color(null, null, null, null); + private _scissorEnable: boolean = false; get isWebGL2() { return this._isWebGL2; @@ -106,8 +108,7 @@ export class WebGLRenderer implements IHardwareRenderer { const option = this._options; option.alpha === undefined && (option.alpha = false); option.stencil === undefined && (option.stencil = true); - - const webCanvas = (canvas as WebCanvas)._webCanvas; + const webCanvas = (this._webCanvas = (canvas as WebCanvas)._webCanvas); const webGLMode = option.webGLMode || WebGLMode.Auto; let gl: (WebGLRenderingContext & WebGLExtension) | WebGL2RenderingContext; @@ -182,12 +183,21 @@ export class WebGLRenderer implements IHardwareRenderer { } viewport(x: number, y: number, width: number, height: number): void { - // gl.enable(gl.SCISSOR_TEST); - // gl.scissor(x, transformY, width, height); - const gl = this._gl; - const lv = this._lastViewport; - + const { _gl: gl, _lastViewport: lv } = this; if (x !== lv.x || y !== lv.y || width !== lv.z || height !== lv.w) { + const { _webCanvas: webCanvas } = this; + if (x === 0 && y === 0 && width === webCanvas.width && height === webCanvas.height) { + if (this._scissorEnable) { + gl.disable(gl.SCISSOR_TEST); + this._scissorEnable = false; + } + } else { + if (!this._scissorEnable) { + gl.enable(gl.SCISSOR_TEST); + this._scissorEnable = true; + } + gl.scissor(x, y, width, height); + } gl.viewport(x, y, width, height); lv.set(x, y, width, height); } @@ -197,26 +207,19 @@ export class WebGLRenderer implements IHardwareRenderer { this._gl.colorMask(r, g, b, a); } - clearRenderTarget( - engine: Engine, - clearFlags: CameraClearFlags.Depth | CameraClearFlags.DepthColor, - clearColor: Color - ) { + clearRenderTarget(engine: Engine, clearFlags: CameraClearFlags, clearColor: Color) { const gl = this._gl; const { blendState: { targetBlendState }, depthState, stencilState } = engine._lastRenderState; - - let clearFlag = gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT; - - if (clearFlags === CameraClearFlags.DepthColor) { + let clearFlag = 0; + if (clearFlags & CameraClearFlags.Color) { clearFlag |= gl.COLOR_BUFFER_BIT; const lc = this._lastClearColor; const { r, g, b, a } = clearColor; - if (clearColor && (r !== lc.r || g !== lc.g || b !== lc.b || a !== lc.a)) { gl.clearColor(r, g, b, a); lc.set(r, g, b, a); @@ -227,17 +230,20 @@ export class WebGLRenderer implements IHardwareRenderer { targetBlendState.colorWriteMask = ColorWriteMask.All; } } - - if (depthState.writeEnabled !== true) { - gl.depthMask(true); - depthState.writeEnabled = true; + if (clearFlags & CameraClearFlags.Depth) { + clearFlag |= gl.DEPTH_BUFFER_BIT; + if (depthState.writeEnabled !== true) { + gl.depthMask(true); + depthState.writeEnabled = true; + } } - - if (stencilState.writeMask !== 0xff) { - gl.stencilMask(0xff); - stencilState.writeMask = 0xff; + if (clearFlags & CameraClearFlags.Stencil) { + clearFlag |= gl.STENCIL_BUFFER_BIT; + if (stencilState.writeMask !== 0xff) { + gl.stencilMask(0xff); + stencilState.writeMask = 0xff; + } } - gl.clear(clearFlag); } diff --git a/tests/package.json b/tests/package.json index 751e91f30c..cbce095724 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,6 +1,7 @@ { "name": "@oasis-engine/tests", - "version": "0.7.0-beta.7", + "private": true, + "version": "0.8.0-alpha.3", "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", @@ -14,11 +15,11 @@ "types/**/*" ], "dependencies": { - "@oasis-engine/math": "0.7.0-beta.7", - "@oasis-engine/core": "0.7.0-beta.7", - "@oasis-engine/rhi-webgl": "0.7.0-beta.7" + "@oasis-engine/core": "0.7.0-beta.4", + "@oasis-engine/math": "0.7.0-beta.4", + "@oasis-engine/rhi-webgl": "0.7.0-beta.4" }, "devDependencies": { - "@oasis-engine/design": "0.7.0-beta.7" + "@oasis-engine/design": "0.7.0-beta.4" } }