From a1b56e81145e7e9837ef5bcd90656ed216e6d8cc Mon Sep 17 00:00:00 2001 From: ChenMo Date: Fri, 22 Apr 2022 16:34:41 +0800 Subject: [PATCH] Merge `dev/0.7` to `main` (#754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: delete deprecate code (#630) * Optimize the use of `Transform` API (#638) * refactor: opt transform API * Merge latest main to dev/0.6 (#672) * fix: normal attributer is need in unlit (#637) * fix: normal attributer is need in unlit * fix: physx bug (#651) * fix: physx bug * fix: opt collider shape scale * v0.6.7 * fix: get pixel buffer use mip level should right shift (#656) Co-authored-by: gz65555 * Update AnimatorState.ts (#657) fix: fix comment * Rename `ability` to `component`. (#608) * fix: resize max uniform vector count (#660) * fix: resize max uniform vector count Co-authored-by: shensi.zxd * fix(2d): can not update world position for sprite renderer when call … (#635) * fix(2d): can not update wrold position for sprite renderer when call bounds in the same frame Co-authored-by: zhuxudong Co-authored-by: yangfengzzz Co-authored-by: Hu Song Co-authored-by: gz65555 Co-authored-by: luzhuang <364439895@qq.com> Co-authored-by: Bob <768442443@qq.com> Co-authored-by: shensi.zxd Co-authored-by: singlecoder * fix: trigger event update (#673) * fix: trigger event update * fix: ignore scale (#670) * fix: camera ignore scale * Fix transform bug (#678) * fix: transform * Fix: ModelMesh._indicesChangeFlag (#685) * Fix: ModelMesh._indicesChangeFlag * feat: non-trigger script and dynamic collider (#680) * feat: non-trigger script and dynamic collider * fix: rotation not normalized bug (#693) * Offscreen canvas requires no input interaction. (#689) * fix:InputManager when use OffScreenCanvas * feat: keyboardevent (#677) * feat: keyboardevent * feat: physics fix-time update (#700) * feat: fix-time physics update * feat: add physicsUpdate in Script * Optimize: Optimize the lookat function of transform. (#701) * fix:transform lookat * fix: transoform `rotate` bug when `relativeToLocal` is true (#702) * fix: wordRotationQuatenion bug (#703) * Feat:The first version of text system (#686) * feat(text): init TextRenderer * fix: fix text renderer clone error (#716) * fix(text): fix text renderer clone error * Optimization texture structure (#715) * feat: opt texture struct * fix: script destroy bug (#720) * fix: script destroy bug * Feat: support multi-uv and occlusion texture coordinate (#683) * feat: support multi uv and occlusion texture coord * Optimization physics init (#722) * refactor: opt physics int * fix: physics update order and destroy method (#727) * fix: physcis event remove when collider shape destroy (#738) * fix: physcis event remove when collider shape destroy * physics test (#742) * Fix RenderTaget type problem (#741) * Test: text system (#740) * feat(text): test for text renderer * Fix unit test and compile bug (#746) * fix: component unit test * test: test for text renderer (#745) * feat(text): fix test for text renderer * FloatTexture version Blendshape (#737) * feat: add texture-2d-array and rename TextureCubeMap to TextureCube * feat: opt blendshape animation and improve attribute version * Opt ModelMesh code (#739) * refactor: opt `ModelMesh` code * refactor: opt code * refactor: opt code (#747) * Feat: support load hdr to cubemap in RGBM format and invert x axis (#733) * feat: add HDR loader * Feat: support pbr clearcoat (#669) * feat: support clearcoat * feat: support Specular AA * feat: animator play backwards (#730) * feat: animator play backwward * refactor: fix code * refactor: opt code * Merge latest `main` to `dev/0.7` (#751) * refactor: merge latest `main` to `dev/0.7 * feat: update wasm version (#753) * refactor:opt code Co-authored-by: zhuxudong Co-authored-by: yangfengzzz Co-authored-by: Hu Song Co-authored-by: gz65555 Co-authored-by: luzhuang <364439895@qq.com> Co-authored-by: Bob <768442443@qq.com> Co-authored-by: shensi.zxd Co-authored-by: singlecoder Co-authored-by: AZhan --- lerna.json | 2 +- package.json | 1 + packages/controls/package.json | 4 +- packages/controls/src/FreeControl.ts | 2 +- packages/controls/src/Spherical.ts | 9 +- packages/core/package.json | 6 +- .../src/2d/dynamic-atlas/DynamicTextAtlas.ts | 95 ++++ .../dynamic-atlas/DynamicTextAtlasManager.ts | 126 +++++ packages/core/src/2d/enums/FontStyle.ts | 11 + packages/core/src/2d/enums/TextAlignment.ts | 23 + packages/core/src/2d/enums/TextOverflow.ts | 9 + packages/core/src/2d/index.ts | 4 + packages/core/src/2d/sprite/Sprite.ts | 10 +- packages/core/src/2d/sprite/SpriteMask.ts | 5 +- packages/core/src/2d/sprite/SpriteRenderer.ts | 9 +- packages/core/src/2d/text/Font.ts | 45 ++ packages/core/src/2d/text/TextRenderer.ts | 443 ++++++++++++++++ packages/core/src/2d/text/TextUtils.ts | 426 ++++++++++++++++ packages/core/src/2d/text/index.ts | 2 + packages/core/src/BoolUpdateFlag.ts | 16 + packages/core/src/Camera.ts | 71 ++- packages/core/src/Component.ts | 14 - packages/core/src/ComponentsManager.ts | 105 ++-- packages/core/src/Engine.ts | 56 +- packages/core/src/Entity.ts | 69 +-- packages/core/src/ListenerUpdateFlag.ts | 16 + packages/core/src/Renderer.ts | 3 +- packages/core/src/Script.ts | 108 ++-- packages/core/src/Transform.ts | 277 ++++++---- packages/core/src/UpdateFlag.ts | 33 +- packages/core/src/UpdateFlagManager.ts | 29 +- packages/core/src/animation/Animator.ts | 82 ++- .../core/src/animation/AnimatorController.ts | 8 +- .../internal/AnimatorStatePlayData.ts | 20 +- packages/core/src/asset/AssetType.ts | 4 +- packages/core/src/env-probe/Probe.ts | 11 +- packages/core/src/graphic/Mesh.ts | 42 +- packages/core/src/input/InputManager.ts | 72 ++- packages/core/src/input/enums/Keys.ts | 410 +++++++++++++++ packages/core/src/input/index.ts | 1 + .../src/input/keyboard/KeyboardManager.ts | 107 ++++ .../core/src/input/pointer/PointerManager.ts | 32 +- packages/core/src/lighting/AmbientLight.ts | 8 +- packages/core/src/material/PBRBaseMaterial.ts | 110 +++- packages/core/src/material/PBRMaterial.ts | 8 +- .../src/material/enums/TextureCoordinate.ts | 13 + packages/core/src/material/index.ts | 1 + packages/core/src/mesh/BlendShape.ts | 50 +- packages/core/src/mesh/BlendShapeManager.ts | 341 +++++++++++++ packages/core/src/mesh/MeshRenderer.ts | 9 +- packages/core/src/mesh/ModelMesh.ts | 278 ++++------ packages/core/src/mesh/SkinnedMeshRenderer.ts | 23 +- packages/core/src/physics/Collider.ts | 10 +- packages/core/src/physics/DynamicCollider.ts | 277 +++++++++- packages/core/src/physics/PhysicsManager.ts | 132 ++++- packages/core/src/physics/PhysicsMaterial.ts | 7 + packages/core/src/physics/index.ts | 2 +- .../core/src/physics/shape/ColliderShape.ts | 8 + .../IPlatformRenderColorTexture.ts | 27 - .../IPlatformRenderDepthTexture.ts | 6 - .../IPlatformTexture2D.ts | 18 +- .../IPlatformTexture2DArray.ts | 68 +++ ...tureCubeMap.ts => IPlatformTextureCube.ts} | 20 +- .../src/renderingHardwareInterface/index.ts | 5 +- packages/core/src/shader/ShaderProgram.ts | 20 +- packages/core/src/shaderlib/ShaderFactory.ts | 12 +- .../core/src/shaderlib/begin_normal_vert.glsl | 12 +- .../core/src/shaderlib/blendShape_input.glsl | 50 +- .../core/src/shaderlib/blendShape_vert.glsl | 52 +- packages/core/src/shaderlib/common.glsl | 4 + packages/core/src/shaderlib/common_vert.glsl | 4 + .../src/shaderlib/mobile_blinnphong_frag.glsl | 8 +- packages/core/src/shaderlib/normal_get.glsl | 84 +-- packages/core/src/shaderlib/normal_share.glsl | 19 +- packages/core/src/shaderlib/normal_vert.glsl | 19 +- packages/core/src/shaderlib/pbr/brdf.glsl | 18 +- .../pbr/direct_irradiance_frag_define.glsl | 29 +- .../src/shaderlib/pbr/ibl_frag_define.glsl | 4 +- packages/core/src/shaderlib/pbr/pbr_frag.glsl | 35 +- .../src/shaderlib/pbr/pbr_frag_define.glsl | 41 +- .../core/src/shaderlib/pbr/pbr_helper.glsl | 93 +++- .../core/src/shaderlib/skinning_vert.glsl | 2 +- packages/core/src/shaderlib/uv_share.glsl | 4 + packages/core/src/shaderlib/uv_vert.glsl | 12 +- packages/core/src/shadow/LightShadow.ts | 8 +- packages/core/src/sky/SkyBoxMaterial.ts | 8 +- .../core/src/texture/RenderColorTexture.ts | 95 ---- .../core/src/texture/RenderDepthTexture.ts | 71 --- packages/core/src/texture/RenderTarget.ts | 59 ++- packages/core/src/texture/Texture.ts | 9 + packages/core/src/texture/Texture2D.ts | 23 +- packages/core/src/texture/Texture2DArray.ts | 214 ++++++++ .../{TextureCubeMap.ts => TextureCube.ts} | 41 +- .../texture/enums/RenderBufferColorFormat.ts | 21 - .../texture/enums/RenderBufferDepthFormat.ts | 16 +- .../core/src/texture/enums/TextureFormat.ts | 22 +- packages/core/src/texture/index.ts | 9 +- packages/core/src/trail/TrailRenderer.ts | 4 +- packages/core/tests/Background.test.ts | 8 +- packages/core/tests/Camera.test.ts | 20 +- packages/core/tests/Component.test.ts | 18 +- packages/core/tests/Entity.test.ts | 3 +- packages/core/tests/ModelMesh.test.ts | 6 +- packages/core/tests/TextRenderer.test.ts | 59 +++ packages/core/tests/Transform.test.ts | 482 +++++++----------- .../core/tests/physics/PhysicsManager.test.ts | 46 ++ packages/core/tests/script.test.ts | 28 +- .../tests/texture/RenderColorTexture.test.ts | 66 --- .../tests/texture/RenderDepthTexture.test.ts | 80 --- .../core/tests/texture/RenderTarget.test.ts | 20 +- .../core/tests/texture/TextureCubeMap.test.ts | 26 +- packages/design/package.json | 4 +- packages/design/src/physics/ICollider.ts | 5 + .../design/src/physics/IDynamicCollider.ts | 114 ++++- .../design/src/physics/IPhysicsMaterial.ts | 5 + .../src/physics/shape/IColliderShape.ts | 5 + packages/draco/package.json | 4 +- packages/framebuffer-picker/package.json | 4 +- .../framebuffer-picker/src/ColorRenderPass.ts | 4 +- .../src/FramebufferPicker.ts | 9 +- packages/loader/package.json | 10 +- packages/loader/src/EnvLoader.ts | 4 +- packages/loader/src/HDRLoader.ts | 400 +++++++++++++++ packages/loader/src/KTXCubeLoader.ts | 8 +- packages/loader/src/TextureCubeLoader.ts | 8 +- .../extensions/KHR_materials_clearcoat.ts | 36 ++ .../loader/src/gltf/parser/MaterialParser.ts | 23 +- packages/loader/src/index.ts | 1 + packages/loader/src/scene-loader/GLTFModel.ts | 11 +- packages/math/package.json | 7 +- packages/math/src/BoundingFrustum.ts | 26 +- packages/math/src/CollisionUtil.ts | 4 +- packages/math/src/Matrix.ts | 121 ++--- packages/math/src/Matrix3x3.ts | 14 +- packages/math/src/Plane.ts | 7 +- packages/math/src/Quaternion.ts | 462 ++++++++++------- packages/math/src/SphericalHarmonics3.ts | 4 +- packages/math/src/Vector2.ts | 165 +++--- packages/math/src/Vector3.ts | 305 ++++++----- packages/math/src/Vector4.ts | 325 +++++++----- packages/oasis-engine/package.json | 10 +- packages/oasis-engine/src/index.ts | 2 + packages/physics-lite/package.json | 6 +- packages/physics-lite/src/LiteCollider.ts | 5 + .../physics-lite/src/LiteDynamicCollider.ts | 126 ++++- .../physics-lite/src/LitePhysicsManager.ts | 49 +- .../physics-lite/src/LitePhysicsMaterial.ts | 5 + .../src/shape/LiteColliderShape.ts | 5 + .../physics-physx/dist/physx.release.wasm | Bin 2465815 -> 2472213 bytes packages/physics-physx/package.json | 6 +- packages/physics-physx/src/PhysXCollider.ts | 7 + .../physics-physx/src/PhysXDynamicCollider.ts | 361 +++---------- packages/physics-physx/src/PhysXPhysics.ts | 22 +- .../physics-physx/src/PhysXPhysicsManager.ts | 43 +- .../physics-physx/src/PhysXPhysicsMaterial.ts | 7 + .../src/shape/PhysXBoxColliderShape.ts | 8 +- .../src/shape/PhysXCapsuleColliderShape.ts | 7 +- .../src/shape/PhysXColliderShape.ts | 15 +- .../src/shape/PhysXPlaneColliderShape.ts | 7 +- .../src/shape/PhysXSphereColliderShape.ts | 5 +- packages/rhi-webgl/package.json | 8 +- packages/rhi-webgl/src/GLCapability.ts | 4 + .../rhi-webgl/src/GLRenderColorTexture.ts | 70 --- .../rhi-webgl/src/GLRenderDepthTexture.ts | 41 -- packages/rhi-webgl/src/GLRenderTarget.ts | 56 +- packages/rhi-webgl/src/GLTexture.ts | 142 ++---- packages/rhi-webgl/src/GLTexture2D.ts | 48 +- packages/rhi-webgl/src/GLTexture2DArray.ts | 125 +++++ .../{GLTextureCubeMap.ts => GLTextureCube.ts} | 59 +-- packages/rhi-webgl/src/WebGLEngine.ts | 10 +- packages/rhi-webgl/src/WebGLRenderer.ts | 33 +- packages/stats/package.json | 4 +- 172 files changed, 6612 insertions(+), 3002 deletions(-) create mode 100644 packages/core/src/2d/dynamic-atlas/DynamicTextAtlas.ts create mode 100644 packages/core/src/2d/dynamic-atlas/DynamicTextAtlasManager.ts create mode 100644 packages/core/src/2d/enums/FontStyle.ts create mode 100644 packages/core/src/2d/enums/TextAlignment.ts create mode 100644 packages/core/src/2d/enums/TextOverflow.ts create mode 100644 packages/core/src/2d/text/Font.ts create mode 100644 packages/core/src/2d/text/TextRenderer.ts create mode 100644 packages/core/src/2d/text/TextUtils.ts create mode 100644 packages/core/src/2d/text/index.ts create mode 100644 packages/core/src/BoolUpdateFlag.ts create mode 100644 packages/core/src/ListenerUpdateFlag.ts create mode 100644 packages/core/src/input/enums/Keys.ts create mode 100644 packages/core/src/input/keyboard/KeyboardManager.ts create mode 100644 packages/core/src/material/enums/TextureCoordinate.ts create mode 100644 packages/core/src/mesh/BlendShapeManager.ts delete mode 100644 packages/core/src/renderingHardwareInterface/IPlatformRenderColorTexture.ts delete mode 100644 packages/core/src/renderingHardwareInterface/IPlatformRenderDepthTexture.ts create mode 100644 packages/core/src/renderingHardwareInterface/IPlatformTexture2DArray.ts rename packages/core/src/renderingHardwareInterface/{IPlatformTextureCubeMap.ts => IPlatformTextureCube.ts} (89%) delete mode 100644 packages/core/src/texture/RenderColorTexture.ts delete mode 100644 packages/core/src/texture/RenderDepthTexture.ts create mode 100644 packages/core/src/texture/Texture2DArray.ts rename packages/core/src/texture/{TextureCubeMap.ts => TextureCube.ts} (84%) delete mode 100644 packages/core/src/texture/enums/RenderBufferColorFormat.ts create mode 100644 packages/core/tests/TextRenderer.test.ts create mode 100644 packages/core/tests/physics/PhysicsManager.test.ts delete mode 100644 packages/core/tests/texture/RenderColorTexture.test.ts delete mode 100644 packages/core/tests/texture/RenderDepthTexture.test.ts create mode 100644 packages/loader/src/HDRLoader.ts delete mode 100644 packages/rhi-webgl/src/GLRenderColorTexture.ts delete mode 100644 packages/rhi-webgl/src/GLRenderDepthTexture.ts create mode 100644 packages/rhi-webgl/src/GLTexture2DArray.ts rename packages/rhi-webgl/src/{GLTextureCubeMap.ts => GLTextureCube.ts} (59%) diff --git a/lerna.json b/lerna.json index fb6e85d3c7..af91a9da4a 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "npmClient": "npm", - "version": "0.6.8", + "version": "0.7.0-beta.0", "bootstrap": { "hoist": true }, diff --git a/package.json b/package.json index b258d61238..c530883295 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@rollup/plugin-node-resolve": "^11.0.1", "@rollup/plugin-replace": "^2.3.4", "@types/jest": "^26.0.20", + "@types/offscreencanvas": "^2019.6.4", "@typescript-eslint/eslint-plugin": "^4.12.0", "@typescript-eslint/parser": "^4.12.0", "babel-jest": "^26.6.3", diff --git a/packages/controls/package.json b/packages/controls/package.json index 57e35304e1..27f8eb1a97 100644 --- a/packages/controls/package.json +++ b/packages/controls/package.json @@ -1,6 +1,6 @@ { "name": "@oasis-engine/controls", - "version": "0.6.8", + "version": "0.7.0-beta.0", "license": "MIT", "scripts": { "b:types": "tsc", @@ -15,6 +15,6 @@ "types/**/*" ], "dependencies": { - "oasis-engine": "0.6.8" + "oasis-engine": "0.7.0-beta.0" } } diff --git a/packages/controls/src/FreeControl.ts b/packages/controls/src/FreeControl.ts index d31bea0868..05e2079b98 100644 --- a/packages/controls/src/FreeControl.ts +++ b/packages/controls/src/FreeControl.ts @@ -281,7 +281,7 @@ export class FreeControl extends Script { */ updateSpherical(): void { this._v3Cache.setValue(0, 0, -1); - Vector3.transformByQuat(this._v3Cache, this.camera.rotation, this._v3Cache); + Vector3.transformByQuat(this._v3Cache, this.camera.transform.rotationQuaternion, this._v3Cache); this._spherical.setFromVec3(this._v3Cache); this._theta = this._spherical.theta; this._phi = this._spherical.phi; diff --git a/packages/controls/src/Spherical.ts b/packages/controls/src/Spherical.ts index e1efbd3065..1d01aed262 100644 --- a/packages/controls/src/Spherical.ts +++ b/packages/controls/src/Spherical.ts @@ -43,10 +43,11 @@ export class Spherical { setToVec3(v3: Vector3) { const sinPhiRadius = Math.sin(this.phi) * this.radius; - - v3.x = sinPhiRadius * Math.sin(this.theta); - v3.y = Math.cos(this.phi) * this.radius; - v3.z = sinPhiRadius * Math.cos(this.theta); + v3.setValue( + sinPhiRadius * Math.sin(this.theta), + Math.cos(this.phi) * this.radius, + sinPhiRadius * Math.cos(this.theta) + ); return this; } diff --git a/packages/core/package.json b/packages/core/package.json index d67f8052f4..e03e350115 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@oasis-engine/core", - "version": "0.6.8", + "version": "0.7.0-beta.0", "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", @@ -14,9 +14,9 @@ "types/**/*" ], "dependencies": { - "@oasis-engine/math": "0.6.8" + "@oasis-engine/math": "0.7.0-beta.0" }, "devDependencies": { - "@oasis-engine/design": "0.6.8" + "@oasis-engine/design": "0.7.0-beta.0" } } diff --git a/packages/core/src/2d/dynamic-atlas/DynamicTextAtlas.ts b/packages/core/src/2d/dynamic-atlas/DynamicTextAtlas.ts new file mode 100644 index 0000000000..2c89a13c37 --- /dev/null +++ b/packages/core/src/2d/dynamic-atlas/DynamicTextAtlas.ts @@ -0,0 +1,95 @@ +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.setValue(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 new file mode 100644 index 0000000000..0032ac87c1 --- /dev/null +++ b/packages/core/src/2d/dynamic-atlas/DynamicTextAtlasManager.ts @@ -0,0 +1,126 @@ +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/FontStyle.ts b/packages/core/src/2d/enums/FontStyle.ts new file mode 100644 index 0000000000..08048cb5e9 --- /dev/null +++ b/packages/core/src/2d/enums/FontStyle.ts @@ -0,0 +1,11 @@ +/** + * The style of the font. + */ +export enum FontStyle { + /** Set font without style */ + None = 0x0, + /** Set font bold */ + Bold = 0x1, + /** Set font italic */ + Italic = 0x2 +} diff --git a/packages/core/src/2d/enums/TextAlignment.ts b/packages/core/src/2d/enums/TextAlignment.ts new file mode 100644 index 0000000000..fae82dd3d1 --- /dev/null +++ b/packages/core/src/2d/enums/TextAlignment.ts @@ -0,0 +1,23 @@ +/** + * The horizontal alignment of the text. + */ +export enum TextHorizontalAlignment { + /** Align left horizontally */ + Left = 0, + /** Align center horizontally */ + Center = 1, + /** Align right horizontally */ + Right = 2 +} + +/** + * The vertical alignment of the text. + */ +export enum TextVerticalAlignment { + /** Align top vertically */ + Top = 0, + /** Align center vertically */ + Center = 1, + /** Align bottom vertically */ + Bottom = 2 +} diff --git a/packages/core/src/2d/enums/TextOverflow.ts b/packages/core/src/2d/enums/TextOverflow.ts new file mode 100644 index 0000000000..adef863f1f --- /dev/null +++ b/packages/core/src/2d/enums/TextOverflow.ts @@ -0,0 +1,9 @@ +/** + * The way to handle the situation where wrapped text is too tall to fit in the height. + */ +export enum OverflowMode { + /** Overflow when the text is too tall */ + Overflow = 0, + /** Truncate with height when the text is too tall */ + Truncate = 1 +} diff --git a/packages/core/src/2d/index.ts b/packages/core/src/2d/index.ts index 31ba3b30bd..f58f66ada7 100644 --- a/packages/core/src/2d/index.ts +++ b/packages/core/src/2d/index.ts @@ -1,4 +1,8 @@ export { SpriteMaskInteraction } from "./enums/SpriteMaskInteraction"; export { SpriteMaskLayer } from "./enums/SpriteMaskLayer"; +export { TextHorizontalAlignment, TextVerticalAlignment } from "./enums/TextAlignment"; +export { OverflowMode } from "./enums/TextOverflow"; +export { FontStyle } from "./enums/FontStyle"; export { SpriteAtlas } from "./atlas/SpriteAtlas"; 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 cd2600de9e..ed24a791e1 100644 --- a/packages/core/src/2d/sprite/Sprite.ts +++ b/packages/core/src/2d/sprite/Sprite.ts @@ -1,8 +1,8 @@ import { BoundingBox, MathUtil, Rect, Vector2, Vector4 } from "@oasis-engine/math"; import { RefObject } from "../../asset/RefObject"; +import { BoolUpdateFlag } from "../../BoolUpdateFlag"; import { Engine } from "../../Engine"; import { Texture2D } from "../../texture/Texture2D"; -import { UpdateFlag } from "../../UpdateFlag"; import { UpdateFlagManager } from "../../UpdateFlagManager"; /** @@ -182,7 +182,7 @@ export class Sprite extends RefObject { * Clone. * @returns Cloned sprite */ - clone(): Sprite { + clone(): Sprite { const cloneSprite = new Sprite( this._engine, this._texture, @@ -201,8 +201,8 @@ export class Sprite extends RefObject { /** * @internal */ - _registerUpdateFlag(): UpdateFlag { - return this._updateFlagManager.register(); + _registerUpdateFlag(): BoolUpdateFlag { + return this._updateFlagManager.createFlag(BoolUpdateFlag); } /** @@ -363,7 +363,7 @@ export class Sprite extends RefObject { private _setDirtyFlagTrue(type: number): void { this._dirtyFlag |= type; - this._updateFlagManager.distribute(); + this._updateFlagManager.dispatch(); } private _setDirtyFlagFalse(type: number): void { diff --git a/packages/core/src/2d/sprite/SpriteMask.ts b/packages/core/src/2d/sprite/SpriteMask.ts index 796fa65b34..23ce3d2c79 100644 --- a/packages/core/src/2d/sprite/SpriteMask.ts +++ b/packages/core/src/2d/sprite/SpriteMask.ts @@ -1,4 +1,5 @@ import { Vector3 } from "@oasis-engine/math"; +import { BoolUpdateFlag } from "../../BoolUpdateFlag"; import { Camera } from "../../Camera"; import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; import { ICustomClone } from "../../clone/ComponentCloner"; @@ -28,13 +29,13 @@ export class SpriteMask extends Renderer implements ICustomClone { @deepClone private _positions: Vector3[] = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; @ignoreClone - private _worldMatrixDirtyFlag: UpdateFlag; + private _worldMatrixDirtyFlag: BoolUpdateFlag; @ignoreClone private _sprite: Sprite = null; @assignmentClone private _alphaCutoff: number = 0.5; @ignoreClone - private _spriteDirty: UpdateFlag; + private _spriteDirty: BoolUpdateFlag; /** The mask layers the sprite mask influence to. */ @assignmentClone diff --git a/packages/core/src/2d/sprite/SpriteRenderer.ts b/packages/core/src/2d/sprite/SpriteRenderer.ts index 4d94bd32cd..f916ac5993 100644 --- a/packages/core/src/2d/sprite/SpriteRenderer.ts +++ b/packages/core/src/2d/sprite/SpriteRenderer.ts @@ -1,4 +1,5 @@ import { BoundingBox, Color, Vector3 } from "@oasis-engine/math"; +import { BoolUpdateFlag } from "../../BoolUpdateFlag"; import { Camera } from "../../Camera"; import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; import { ICustomClone } from "../../clone/ComponentCloner"; @@ -16,7 +17,9 @@ import { Sprite } from "./Sprite"; * Renders a Sprite for 2D graphics. */ export class SpriteRenderer extends Renderer implements ICustomClone { - private static _textureProperty: ShaderProperty = Shader.getPropertyByName("u_spriteTexture"); + /** @internal */ + static _textureProperty: ShaderProperty = Shader.getPropertyByName("u_spriteTexture"); + private static _tempVec3: Vector3 = new Vector3(); /** @internal temp solution. */ @@ -43,9 +46,9 @@ export class SpriteRenderer extends Renderer implements ICustomClone { @ignoreClone private _dirtyFlag: number = 0; @ignoreClone - private _isWorldMatrixDirty: UpdateFlag; + private _isWorldMatrixDirty: BoolUpdateFlag; @ignoreClone - private _spriteDirty: UpdateFlag; + private _spriteDirty: BoolUpdateFlag; @assignmentClone private _maskInteraction: SpriteMaskInteraction = SpriteMaskInteraction.None; @assignmentClone diff --git a/packages/core/src/2d/text/Font.ts b/packages/core/src/2d/text/Font.ts new file mode 100644 index 0000000000..f56e36b27a --- /dev/null +++ b/packages/core/src/2d/text/Font.ts @@ -0,0 +1,45 @@ +import { RefObject } from "../../asset/RefObject"; +import { Engine } from "../../Engine"; + +/** + * Font. + */ +export class Font extends RefObject { + private static _fontMap: Record = {}; + + /** + * Create a font from OS. + * @param engine - Engine to which the font belongs + * @param fontName - The name of font + * @returns The font object has been create + */ + static createFromOS(engine: Engine, fontName: string = ""): Font { + const fontMap = Font._fontMap; + let font = fontMap[fontName]; + if (font) { + return font; + } + font = new Font(engine, fontName); + fontMap[fontName] = font; + return font; + } + + private _name: string = ""; + + /** + * The name of the font object. + */ + get name(): string { + return this._name; + } + + private constructor(engine: Engine, name: string = "") { + super(engine); + this._name = name; + } + + /** + * @override + */ + protected _onDestroy(): void {} +} diff --git a/packages/core/src/2d/text/TextRenderer.ts b/packages/core/src/2d/text/TextRenderer.ts new file mode 100644 index 0000000000..d54b446855 --- /dev/null +++ b/packages/core/src/2d/text/TextRenderer.ts @@ -0,0 +1,443 @@ +import { BoundingBox, Color, Vector3 } from "@oasis-engine/math"; +import { Sprite, SpriteMaskInteraction, SpriteMaskLayer, SpriteRenderer } from ".."; +import { CompareFunction, Renderer, UpdateFlag } from "../.."; +import { BoolUpdateFlag } from "../../BoolUpdateFlag"; +import { Camera } from "../../Camera"; +import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; +import { Entity } from "../../Entity"; +import { Texture2D } from "../../texture"; +import { FontStyle } from "../enums/FontStyle"; +import { TextHorizontalAlignment, TextVerticalAlignment } from "../enums/TextAlignment"; +import { OverflowMode } from "../enums/TextOverflow"; +import { Font } from "./Font"; +import { TextUtils } from "./TextUtils"; + +/** + * Renders a text for 2D graphics. + */ +export class TextRenderer extends Renderer { + private static _tempVec3: Vector3 = new Vector3(); + + /** @internal temp solution. */ + @ignoreClone + _customLocalBounds: BoundingBox = null; + /** @internal temp solution. */ + @ignoreClone + _customRootEntity: Entity = null; + + @ignoreClone + private _sprite: Sprite = null; + @deepClone + private _positions: Vector3[] = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; + @deepClone + private _color: Color = new Color(1, 1, 1, 1); + @assignmentClone + private _text: string = ""; + @assignmentClone + private _width: number = 0; + @assignmentClone + private _height: number = 0; + @assignmentClone + private _font: Font = null; + @assignmentClone + private _fontSize: number = 24; + @assignmentClone + private _fontStyle: FontStyle = FontStyle.None; + @assignmentClone + private _lineSpacing: number = 0; + @assignmentClone + private _horizontalAlignment: TextHorizontalAlignment = TextHorizontalAlignment.Center; + @assignmentClone + private _verticalAlignment: TextVerticalAlignment = TextVerticalAlignment.Center; + @assignmentClone + private _enableWrapping: boolean = false; + @assignmentClone + private _overflowMode: OverflowMode = OverflowMode.Overflow; + @ignoreClone + private _dirtyFlag: number = DirtyFlag.Property; + @ignoreClone + private _isWorldMatrixDirty: BoolUpdateFlag; + @assignmentClone + private _maskInteraction: SpriteMaskInteraction = SpriteMaskInteraction.None; + @assignmentClone + private _maskLayer: number = SpriteMaskLayer.Layer0; + + /** + * Rendering color for the Text. + */ + get color(): Color { + return this._color; + } + + set color(value: Color) { + if (this._color !== value) { + value.cloneTo(this._color); + } + } + + /** + * Rendering string for the Text. + */ + get text(): string { + return this._text; + } + + set text(value: string) { + value = value || ""; + if (this._text !== value) { + this._text = value; + this._setDirtyFlagTrue(DirtyFlag.Property); + } + } + + /** + * The width of the TextRenderer (in 3D world coordinates). + */ + get width(): number { + return this._width; + } + + set width(value: number) { + if (this._width !== value) { + this._width = value; + this._setDirtyFlagTrue(DirtyFlag.Property); + } + } + + /** + * The height of the TextRenderer (in 3D world coordinates). + */ + get height(): number { + return this._height; + } + + set height(value: number) { + if (this._height !== value) { + this._height = value; + this._setDirtyFlagTrue(DirtyFlag.Property); + } + } + + /** + * The font of the Text. + */ + get font(): Font { + return this._font; + } + + set font(value: Font) { + if (this._font !== value) { + this._font = value; + this._setDirtyFlagTrue(DirtyFlag.Property); + } + } + + /** + * The font size of the Text. + */ + get fontSize(): number { + return this._fontSize; + } + + set fontSize(value: number) { + if (this._fontSize !== value) { + this._fontSize = value; + this._setDirtyFlagTrue(DirtyFlag.Property); + } + } + + /** + * The style of the font. + */ + get fontStyle(): FontStyle { + return this._fontStyle; + } + + set fontStyle(value: FontStyle) { + if (this.fontStyle !== value) { + this._fontStyle = value; + this._setDirtyFlagTrue(DirtyFlag.Property); + } + } + + /** + * The space between two lines (in pixels). + */ + get lineSpacing(): number { + return this._lineSpacing; + } + + set lineSpacing(value: number) { + if (this._lineSpacing !== value) { + this._lineSpacing = value; + this._setDirtyFlagTrue(DirtyFlag.Property); + } + } + + /** + * The horizontal alignment. + */ + get horizontalAlignment(): TextHorizontalAlignment { + return this._horizontalAlignment; + } + + set horizontalAlignment(value: TextHorizontalAlignment) { + if (this._horizontalAlignment !== value) { + this._horizontalAlignment = value; + this._setDirtyFlagTrue(DirtyFlag.Property); + } + } + + /** + * The vertical alignment. + */ + get verticalAlignment(): TextVerticalAlignment { + return this._verticalAlignment; + } + + set verticalAlignment(value: TextVerticalAlignment) { + if (this._verticalAlignment !== value) { + this._verticalAlignment = value; + this._setDirtyFlagTrue(DirtyFlag.Property); + } + } + + /** + * Whether wrap text to next line when exceeds the width of the container. + */ + get enableWrapping(): boolean { + return this._enableWrapping; + } + + set enableWrapping(value: boolean) { + if (this._enableWrapping !== value) { + this._enableWrapping = value; + this._setDirtyFlagTrue(DirtyFlag.Property); + } + } + + /** + * The overflow mode. + */ + get overflowMode(): OverflowMode { + return this._overflowMode; + } + + set overflowMode(value: OverflowMode) { + if (this._overflowMode !== value) { + this._overflowMode = value; + this._setDirtyFlagTrue(DirtyFlag.Property); + } + } + + /** + * Interacts with the masks. + */ + get maskInteraction(): SpriteMaskInteraction { + return this._maskInteraction; + } + + set maskInteraction(value: SpriteMaskInteraction) { + if (this._maskInteraction !== value) { + this._maskInteraction = value; + this._setDirtyFlagTrue(DirtyFlag.MaskInteraction); + } + } + + /** + * The mask layer the sprite renderer belongs to. + */ + get maskLayer(): number { + return this._maskLayer; + } + + set maskLayer(value: number) { + this._maskLayer = value; + } + + constructor(entity: Entity) { + super(entity); + const { engine } = this; + this._isWorldMatrixDirty = entity.transform.registerWorldChangeFlag(); + this._sprite = new Sprite(engine); + this.font = Font.createFromOS(engine); + this.setMaterial(engine._spriteDefaultMaterial); + } + + /** + * @internal + */ + _render(camera: Camera): void { + if ( + this._text === "" || + (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._isWorldMatrixDirty.flag || isTextureDirty) { + this._updatePosition(); + this._isWorldMatrixDirty.flag = false; + } + + if (this._isContainDirtyFlag(DirtyFlag.MaskInteraction)) { + this._updateStencilState(); + this._setDirtyFlagFalse(DirtyFlag.MaskInteraction); + } + + 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); + } + + /** + * @internal + */ + _onDestroy(): void { + this.engine._dynamicTextAtlasManager.removeSprite(this._sprite); + this._isWorldMatrixDirty.destroy(); + super._onDestroy(); + } + + /** + * @internal + */ + _cloneTo(target: TextRenderer): void { + target.font = this._font; + } + + /** + * @override + */ + protected _updateBounds(worldBounds: BoundingBox): void { + const sprite = this._sprite; + if (sprite && sprite.texture) { + 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.setValue(0, 0, 0); + worldBounds.max.setValue(0, 0, 0); + } + } + + 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; + } + + 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 } = this; + // 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(); + } + + private _updateStencilState(): void { + // Update stencil. + const material = this.getInstanceMaterial(); + const stencilState = material.renderState.stencilState; + const maskInteraction = this._maskInteraction; + + if (maskInteraction === SpriteMaskInteraction.None) { + stencilState.enabled = false; + stencilState.writeMask = 0xff; + stencilState.referenceValue = 0; + stencilState.compareFunctionFront = stencilState.compareFunctionBack = CompareFunction.Always; + } else { + stencilState.enabled = true; + stencilState.writeMask = 0x00; + stencilState.referenceValue = 1; + const compare = + maskInteraction === SpriteMaskInteraction.VisibleInsideMask + ? CompareFunction.LessEqual + : CompareFunction.Greater; + stencilState.compareFunctionFront = compare; + stencilState.compareFunctionBack = compare; + } + } + + 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.setValue(curVertexPos.x, curVertexPos.y, 0); + Vector3.transformToVec3(localVertexPos, worldMatrix, _positions[i]); + } + } + + 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; + } +} + +enum DirtyFlag { + Property = 0x1, + MaskInteraction = 0x2, + All = 0x3 +} diff --git a/packages/core/src/2d/text/TextUtils.ts b/packages/core/src/2d/text/TextUtils.ts new file mode 100644 index 0000000000..a3592aea97 --- /dev/null +++ b/packages/core/src/2d/text/TextUtils.ts @@ -0,0 +1,426 @@ +import { Vector2 } from "@oasis-engine/math"; +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; +} + +/** + * @internal + * TextUtils includes some helper function for text. + */ +export class TextUtils { + static _genericFontFamilies: Array = [ + "serif", + "sans-serif", + "monospace", + "cursive", + "fantasy", + "system-ui", + "math", + "emoji", + "fangsong" + ]; + /** These characters are all tall to help calculate the height required for text. */ + private static _measureString: string = "|ÉqÅ"; + 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 _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 { + let { _textContext: textContext } = TextUtils; + if (!textContext) { + let canvas: HTMLCanvasElement | OffscreenCanvas; + try { + canvas = new OffscreenCanvas(0, 0); + } catch { + canvas = document.createElement("canvas"); + } + const context = canvas.getContext("2d"); + textContext = { canvas, context }; + TextUtils._textContext = textContext; + } + return textContext; + } + + /** + * Measure the font. + * @param font - the string of the font + * @returns the font size + */ + 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; + } + } + + const ascent = baseline - i; + stop = false; + + 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; + } + } + + const descent = i - baseline + 1; + fontSize = ascent + descent; + fontSizeCache[font] = fontSize; + return fontSize; + } + + /** + * 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; + const lineWidths = new Array(); + const lineHeight = fontSize + lineSpacing * _pixelsPerUnit; + context.font = fontString; + // Calculate max width of all lines. + let width = 0; + for (let i = 0; i < lineCount; ++i) { + const lineWidth = Math.ceil(context.measureText(lines[i]).width); + if (lineWidth > width) { + width = lineWidth; + } + lineWidths.push(lineWidth); + } + + // Reset width and height. + let height = originHeight * _pixelsPerUnit; + if (overflowMode === OverflowMode.Overflow) { + height = Math.min(lineHeight * lineCount, TextUtils._maxHeight); + } + + return { + width, + height, + lines, + lineWidths, + lineHeight + }; + } + + /** + * 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; + } + } + } + + 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); + } + + return { + width, + height, + data + }; + } + + /** + * Get native font string. + * @param fontName - The font name + * @param fontSize - The font size + * @param style - The font style + * @returns The native font string + */ + public 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; + } + + /** + * 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 + * @param height - the new height of canvas + * @param data - the new data of canvas + * @returns the canvas after update + */ + public static updateCanvas(width: number, height: number, data: ImageData): HTMLCanvasElement | OffscreenCanvas { + const { canvas, context } = TextUtils.textContext(); + canvas.width = width; + canvas.height = height; + context.putImageData(data, 0, 0); + 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)/); + context.font = fontString; + + 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); + } + } + } else { + wrappedSubTexts.push(subText); + } + } + + return wrappedSubTexts; + } + + 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; + } + + 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; + } + } +} diff --git a/packages/core/src/2d/text/index.ts b/packages/core/src/2d/text/index.ts new file mode 100644 index 0000000000..a2f6d22030 --- /dev/null +++ b/packages/core/src/2d/text/index.ts @@ -0,0 +1,2 @@ +export { Font } from "./Font"; +export { TextRenderer } from "./TextRenderer"; diff --git a/packages/core/src/BoolUpdateFlag.ts b/packages/core/src/BoolUpdateFlag.ts new file mode 100644 index 0000000000..4215c3a98c --- /dev/null +++ b/packages/core/src/BoolUpdateFlag.ts @@ -0,0 +1,16 @@ +import { UpdateFlag } from "./UpdateFlag"; + +/** + * Used to update tags. + */ +export class BoolUpdateFlag extends UpdateFlag { + /** Flag. */ + flag = true; + + /** + * @inheritdoc + */ + dispatch(): void { + this.flag = true; + } +} diff --git a/packages/core/src/Camera.ts b/packages/core/src/Camera.ts index 9b4baf5ef2..6f638ce8c0 100644 --- a/packages/core/src/Camera.ts +++ b/packages/core/src/Camera.ts @@ -1,5 +1,6 @@ -import { BoundingFrustum, MathUtil, Matrix, Ray, Vector2, Vector3, Vector4 } from "@oasis-engine/math"; +import { BoundingFrustum, MathUtil, Matrix, Quaternion, Ray, Vector2, Vector3, Vector4 } from "@oasis-engine/math"; import { Logger } from "./base"; +import { BoolUpdateFlag } from "./BoolUpdateFlag"; import { deepClone, ignoreClone } from "./clone/CloneManager"; import { Component } from "./Component"; import { dependencies } from "./ComponentsDependencies"; @@ -35,6 +36,52 @@ export class Camera extends Component { private static _inverseProjectionMatrixProperty = Shader.getPropertyByName("u_projInvMat"); private static _cameraPositionProperty = Shader.getPropertyByName("u_cameraPos"); + /** + * Compute the inverse of the rotation translation matrix. + * @param rotation - The rotation used to calculate matrix + * @param translation - The translation used to calculate matrix + * @param out - The calculated matrix + */ + private static _rotationTranslationInv(rotation: Quaternion, translation: Vector3, out: Matrix) { + const oe = out.elements; + const { x, y, z, w } = rotation; + let x2 = x + x; + let y2 = y + y; + let z2 = z + z; + + let xx = x * x2; + let xy = x * y2; + let xz = x * z2; + let yy = y * y2; + let yz = y * z2; + let zz = z * z2; + let wx = w * x2; + let wy = w * y2; + let wz = w * z2; + + oe[0] = 1 - (yy + zz); + oe[1] = xy + wz; + oe[2] = xz - wy; + oe[3] = 0; + + oe[4] = xy - wz; + oe[5] = 1 - (xx + zz); + oe[6] = yz + wx; + oe[7] = 0; + + oe[8] = xz + wy; + oe[9] = yz - wx; + oe[10] = 1 - (xx + yy); + oe[11] = 0; + + oe[12] = translation.x; + oe[13] = translation.y; + oe[14] = translation.z; + oe[15] = 1; + + out.invert(); + } + /** Shader data. */ readonly shaderData: ShaderData = new ShaderData(ShaderDataGroup.Camera); @@ -78,13 +125,13 @@ export class Camera extends Component { private _renderTarget: RenderTarget = null; @ignoreClone - private _frustumViewChangeFlag: UpdateFlag; + private _frustumViewChangeFlag: BoolUpdateFlag; @ignoreClone private _transform: Transform; @ignoreClone - private _isViewMatrixDirty: UpdateFlag; + private _isViewMatrixDirty: BoolUpdateFlag; @ignoreClone - private _isInvViewProjDirty: UpdateFlag; + private _isInvViewProjDirty: BoolUpdateFlag; @deepClone private _projectionMatrix: Matrix = new Matrix(); @deepClone @@ -191,10 +238,14 @@ export class Camera extends Component { * View matrix. */ get viewMatrix(): Readonly { - // Remove scale if (this._isViewMatrixDirty.flag) { this._isViewMatrixDirty.flag = false; - Matrix.invert(this._transform.worldMatrix, this._viewMatrix); + // Ignore scale. + Camera._rotationTranslationInv( + this._transform.worldRotationQuaternion, + this._transform.worldPosition, + this._viewMatrix + ); } return this._viewMatrix; } @@ -455,7 +506,7 @@ export class Camera extends Component { * @override * @inheritdoc */ - _onActive() { + _onEnable(): void { this.entity.scene._attachRenderCamera(this); } @@ -463,7 +514,7 @@ export class Camera extends Component { * @override * @inheritdoc */ - _onInActive() { + _onDisable(): void { this.entity.scene._detachRenderCamera(this); } @@ -471,14 +522,14 @@ export class Camera extends Component { * @override * @inheritdoc */ - _onDestroy() { + _onDestroy(): void { this._renderPipeline?.destroy(); this._isInvViewProjDirty.destroy(); this._isViewMatrixDirty.destroy(); this.shaderData._addRefCount(-1); } - private _projMatChange() { + private _projMatChange(): void { this._isFrustumProjectDirty = true; this._isProjectionDirty = true; this._isInvProjMatDirty = true; diff --git a/packages/core/src/Component.ts b/packages/core/src/Component.ts index e56ba632fc..7fbfba72c9 100644 --- a/packages/core/src/Component.ts +++ b/packages/core/src/Component.ts @@ -1,6 +1,5 @@ import { EngineObject } from "./base"; import { assignmentClone, ignoreClone } from "./clone/CloneManager"; -import { Engine } from "./Engine"; import { Entity } from "./Entity"; import { Scene } from "./Scene"; @@ -75,7 +74,6 @@ export abstract class Component extends EngineObject { this._entity._removeComponent(this); if (this._entity.isActiveInHierarchy) { this._enabled && this._onDisable(); - this._onInActive(); } this._destroyed = true; this._onDestroy(); @@ -101,16 +99,6 @@ export abstract class Component extends EngineObject { */ _onDestroy(): void {} - /** - * @internal - */ - _onActive(): void {} - - /** - * @internal - */ - _onInActive(): void {} - /** * @internal */ @@ -122,12 +110,10 @@ export abstract class Component extends EngineObject { } // You can do isActive = false in onAwake function. if (this._entity._isActiveInHierarchy) { - this._onActive(); this._enabled && this._onEnable(); } } else { this._enabled && this._onDisable(); - this._onInActive(); } } } diff --git a/packages/core/src/ComponentsManager.ts b/packages/core/src/ComponentsManager.ts index 6e37ffa600..09a75705b6 100644 --- a/packages/core/src/ComponentsManager.ts +++ b/packages/core/src/ComponentsManager.ts @@ -1,12 +1,12 @@ +import { Vector3 } from "@oasis-engine/math"; import { Camera } from "./Camera"; -import { DisorderedArray } from "./DisorderedArray"; import { Component } from "./Component"; +import { DisorderedArray } from "./DisorderedArray"; +import { Collider } from "./physics"; import { Renderer } from "./Renderer"; +import { RenderContext } from "./RenderPipeline/RenderContext"; import { Script } from "./Script"; import { ShaderMacroCollection } from "./shader/ShaderMacroCollection"; -import { RenderContext } from "./RenderPipeline/RenderContext"; -import { Vector3 } from "@oasis-engine/math"; -import { Collider } from "./physics/Collider"; /** * The manager of the components. @@ -19,7 +19,9 @@ export class ComponentsManager { private _onStartScripts: DisorderedArray