diff --git a/examples/src/2d-01-sprite-mask.ts b/examples/src/2d-01-sprite-mask.ts new file mode 100644 index 0000000000..cbaf613e2b --- /dev/null +++ b/examples/src/2d-01-sprite-mask.ts @@ -0,0 +1,131 @@ +/** + * @title 2D 01 - SpriteMask 遮罩 + * @category 2D 教程 + */ +import { + Camera, + Color, + Entity, + Logger, + Script, + Sprite, + SpriteMask, + SpriteMaskInteraction, + SpriteMaskLayer, + SpriteRenderer, + Texture2D, + Vector3, + WebGLEngine +} from "@galacean/engine"; + +function createCircleTexture(engine: WebGLEngine, size: number): Texture2D { + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = size; + const ctx = canvas.getContext("2d")!; + const center = size / 2; + ctx.beginPath(); + ctx.arc(center, center, center - 2, 0, Math.PI * 2); + ctx.fillStyle = "#ffffff"; + ctx.fill(); + const texture = new Texture2D(engine, size, size); + texture.setImageSource(canvas); + texture.generateMipmaps(); + return texture; +} + +function createColorTexture(engine: WebGLEngine, color: string, size: number = 128): Texture2D { + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = size; + const ctx = canvas.getContext("2d")!; + ctx.fillStyle = color; + ctx.fillRect(0, 0, size, size); + const texture = new Texture2D(engine, size, size); + texture.setImageSource(canvas); + texture.generateMipmaps(); + return texture; +} + +class OscillateScript extends Script { + amplitude = 2; + speed = 1; + private _time = 0; + private _startX = 0; + + onStart() { + this._startX = this.entity.transform.position.x; + } + + onUpdate(dt: number) { + this._time += dt; + const x = this._startX + Math.sin(this._time * this.speed) * this.amplitude; + const pos = this.entity.transform.position; + this.entity.transform.setPosition(x, pos.y, pos.z); + } +} + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(0, 0, 10); + cameraEntity.addComponent(Camera); + + const circleTexture = createCircleTexture(engine, 256); + const redTexture = createColorTexture(engine, "#e74c3c"); + const blueTexture = createColorTexture(engine, "#3498db"); + const greenTexture = createColorTexture(engine, "#2ecc71"); + + // --- SpriteMask (circle shape, oscillating) --- + const maskEntity = rootEntity.createChild("SpriteMask"); + maskEntity.transform.setPosition(0, 0, 0); + const mask = maskEntity.addComponent(SpriteMask); + mask.sprite = new Sprite(engine, circleTexture); + mask.alphaCutoff = 0.5; + mask.influenceLayers = SpriteMaskLayer.Layer0; + mask.width = 3; + mask.height = 3; + const oscillate = maskEntity.addComponent(OscillateScript); + oscillate.amplitude = 1.5; + oscillate.speed = 2; + + // --- Sprite: VisibleInsideMask --- + const insideEntity = rootEntity.createChild("InsideMask"); + insideEntity.transform.setPosition(-2, 0, 0); + const insideRenderer = insideEntity.addComponent(SpriteRenderer); + insideRenderer.sprite = new Sprite(engine, redTexture); + insideRenderer.width = 3; + insideRenderer.height = 3; + insideRenderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + insideRenderer.maskLayer = SpriteMaskLayer.Layer0; + + // --- Sprite: VisibleOutsideMask --- + const outsideEntity = rootEntity.createChild("OutsideMask"); + outsideEntity.transform.setPosition(2, 0, 0); + const outsideRenderer = outsideEntity.addComponent(SpriteRenderer); + outsideRenderer.sprite = new Sprite(engine, blueTexture); + outsideRenderer.width = 3; + outsideRenderer.height = 3; + outsideRenderer.maskInteraction = SpriteMaskInteraction.VisibleOutsideMask; + outsideRenderer.maskLayer = SpriteMaskLayer.Layer0; + + // --- Sprite: No mask interaction (always visible) --- + const normalEntity = rootEntity.createChild("NoMask"); + normalEntity.transform.setPosition(0, -3, 0); + const normalRenderer = normalEntity.addComponent(SpriteRenderer); + normalRenderer.sprite = new Sprite(engine, greenTexture); + normalRenderer.width = 2; + normalRenderer.height = 2; + normalRenderer.color = new Color(1, 1, 1, 0.8); + + engine.run(); + + console.log("2D 01 - SpriteMask 遮罩"); + console.log("- 圆形 SpriteMask 左右摆动"); + console.log("- 红色: VisibleInsideMask (只在 mask 内可见)"); + console.log("- 蓝色: VisibleOutsideMask (只在 mask 外可见)"); + console.log("- 绿色: 无遮罩交互 (始终可见)"); + console.log("- 2D 遮罩使用 maskInteraction + maskLayer,与 UI 层级式遮罩不同"); +}); diff --git a/examples/src/ui-01-basic.ts b/examples/src/ui-01-basic.ts new file mode 100644 index 0000000000..ea669d978d --- /dev/null +++ b/examples/src/ui-01-basic.ts @@ -0,0 +1,96 @@ +/** + * @title UI 01 - 基础 UI 组件 + * @category UI 教程 + */ +import { + Camera, + Color, + Entity, + Font, + Logger, + Sprite, + Texture2D, + Vector2, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { CanvasRenderMode, Image, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +function createColorTexture(engine: WebGLEngine, r: number, g: number, b: number, a: number = 255): Texture2D { + const texture = new Texture2D(engine, 4, 4); + const data = new Uint8Array(4 * 4 * 4); + for (let i = 0; i < 64; i += 4) { + data[i] = r; + data[i + 1] = g; + data[i + 2] = b; + data[i + 3] = a; + } + texture.setPixelBuffer(data); + return texture; +} + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + // UI Canvas (ScreenSpace Overlay) + const canvasEntity = rootEntity.createChild("Canvas"); + const canvas = canvasEntity.addComponent(UICanvas); + canvas.renderMode = CanvasRenderMode.ScreenSpaceOverlay; + canvas.referenceResolution = new Vector2(800, 600); + canvas.renderCamera = camera; + + // --- Background Panel --- + const panelEntity = canvasEntity.createChild("Panel"); + const panelTransform = panelEntity.transform; + panelTransform.size = new Vector2(400, 300); + const panelImage = panelEntity.addComponent(Image); + panelImage.sprite = new Sprite(engine, createColorTexture(engine, 40, 40, 60)); + panelImage.color = new Color(1, 1, 1, 0.9); + + // --- Title Text --- + const titleEntity = panelEntity.createChild("Title"); + const titleTransform = titleEntity.transform; + titleTransform.size = new Vector2(300, 50); + titleEntity.transform.setPosition(0, 100, 0); + const title = titleEntity.addComponent(Text); + title.text = "Galacean UI"; + title.fontSize = 32; + title.color = new Color(1, 1, 1, 1); + + // --- Colored Images --- + const colors = [ + { r: 231, g: 76, b: 60 }, + { r: 46, g: 204, b: 113 }, + { r: 52, g: 152, b: 219 } + ]; + const labels = ["Red", "Green", "Blue"]; + for (let i = 0; i < 3; i++) { + const itemEntity = panelEntity.createChild(`Item${i}`); + const itemTransform = itemEntity.transform; + itemTransform.size = new Vector2(100, 100); + itemEntity.transform.setPosition(-120 + i * 120, -20, 0); + + const img = itemEntity.addComponent(Image); + const { r, g, b } = colors[i]; + img.sprite = new Sprite(engine, createColorTexture(engine, r, g, b)); + + const labelEntity = itemEntity.createChild("Label"); + const labelTransform = labelEntity.transform; + labelTransform.size = new Vector2(100, 30); + labelEntity.transform.setPosition(0, -70, 0); + const label = labelEntity.addComponent(Text); + label.text = labels[i]; + label.fontSize = 18; + label.color = new Color(0.8, 0.8, 0.8, 1); + } + + engine.run(); +}); diff --git a/examples/src/ui-02-mask.ts b/examples/src/ui-02-mask.ts new file mode 100644 index 0000000000..6f66ecec70 --- /dev/null +++ b/examples/src/ui-02-mask.ts @@ -0,0 +1,131 @@ +/** + * @title UI 02 - Mask 遮罩 + * @category UI 教程 + */ +import { + Camera, + Color, + Entity, + Logger, + Script, + Sprite, + Texture2D, + Vector2, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { CanvasRenderMode, Image, Mask, UICanvas, UITransform } from "@galacean/engine-ui"; + +function createCircleTexture(engine: WebGLEngine, size: number): Texture2D { + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = size; + const ctx = canvas.getContext("2d")!; + const center = size / 2; + const radius = center - 2; + ctx.beginPath(); + ctx.arc(center, center, radius, 0, Math.PI * 2); + ctx.fillStyle = "#ffffff"; + ctx.fill(); + const texture = new Texture2D(engine, size, size); + texture.setImageSource(canvas); + texture.generateMipmaps(); + return texture; +} + +function createGradientTexture(engine: WebGLEngine, size: number): Texture2D { + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = size; + const ctx = canvas.getContext("2d")!; + const gradient = ctx.createLinearGradient(0, 0, size, size); + gradient.addColorStop(0, "#e74c3c"); + gradient.addColorStop(0.5, "#f39c12"); + gradient.addColorStop(1, "#2ecc71"); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, size, size); + const texture = new Texture2D(engine, size, size); + texture.setImageSource(canvas); + texture.generateMipmaps(); + return texture; +} + +function createCheckerTexture(engine: WebGLEngine, size: number): Texture2D { + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = size; + const ctx = canvas.getContext("2d")!; + const tileSize = size / 8; + for (let x = 0; x < 8; x++) { + for (let y = 0; y < 8; y++) { + ctx.fillStyle = (x + y) % 2 === 0 ? "#3498db" : "#2c3e50"; + ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize); + } + } + const texture = new Texture2D(engine, size, size); + texture.setImageSource(canvas); + texture.generateMipmaps(); + return texture; +} + +class RotateScript extends Script { + speed = 30; + onUpdate(dt: number) { + this.entity.transform.rotate(new Vector3(0, 0, this.speed * dt)); + } +} + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = rootEntity.createChild("Canvas"); + const canvas = canvasEntity.addComponent(UICanvas); + canvas.renderMode = CanvasRenderMode.ScreenSpaceOverlay; + canvas.referenceResolution = new Vector2(800, 600); + canvas.renderCamera = camera; + + const circleTexture = createCircleTexture(engine, 256); + const gradientTexture = createGradientTexture(engine, 256); + const checkerTexture = createCheckerTexture(engine, 256); + + // --- Example 1: Circle mask clipping a gradient --- + const mask1Entity = canvasEntity.createChild("CircleMask"); + const mask1Transform = mask1Entity.transform; + mask1Transform.size = new Vector2(200, 200); + mask1Entity.transform.setPosition(-200, 50, 0); + const mask1 = mask1Entity.addComponent(Mask); + mask1.sprite = new Sprite(engine, circleTexture); + + const content1 = mask1Entity.createChild("Content"); + const content1Transform = content1.transform; + content1Transform.size = new Vector2(250, 250); + const content1Image = content1.addComponent(Image); + content1Image.sprite = new Sprite(engine, gradientTexture); + content1.addComponent(RotateScript).speed = 20; + + // --- Example 2: Circle mask clipping a checker pattern --- + const mask2Entity = canvasEntity.createChild("CheckerMask"); + const mask2Transform = mask2Entity.transform; + mask2Transform.size = new Vector2(200, 200); + mask2Entity.transform.setPosition(200, 50, 0); + const mask2 = mask2Entity.addComponent(Mask); + mask2.sprite = new Sprite(engine, circleTexture); + + const content2 = mask2Entity.createChild("Content"); + const content2Transform = content2.transform; + content2Transform.size = new Vector2(300, 300); + const content2Image = content2.addComponent(Image); + content2Image.sprite = new Sprite(engine, checkerTexture); + content2.addComponent(RotateScript).speed = -15; + + engine.run(); + + console.log("UI 02 - Mask 遮罩"); + console.log("- 左: 圆形 Mask 裁剪渐变图片 (旋转)"); + console.log("- 右: 圆形 Mask 裁剪棋盘格 (旋转)"); + console.log("- 子节点自动被 Mask 裁剪,无需手动配置 maskInteraction"); +}); diff --git a/examples/src/ui-03-rectmask.ts b/examples/src/ui-03-rectmask.ts new file mode 100644 index 0000000000..c33b152b0d --- /dev/null +++ b/examples/src/ui-03-rectmask.ts @@ -0,0 +1,171 @@ +/** + * @title UI 03 - RectMask2D 矩形裁剪 + * @category UI 教程 + */ +import { + Camera, + Color, + Entity, + Logger, + Script, + Sprite, + Texture2D, + Vector2, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { CanvasRenderMode, Image, RectMask2D, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +function createColorTexture(engine: WebGLEngine, r: number, g: number, b: number): Texture2D { + const texture = new Texture2D(engine, 4, 4); + const data = new Uint8Array(4 * 4 * 4); + for (let i = 0; i < 64; i += 4) { + data[i] = r; + data[i + 1] = g; + data[i + 2] = b; + data[i + 3] = 255; + } + texture.setPixelBuffer(data); + return texture; +} + +function createGradientTexture(engine: WebGLEngine, size: number): Texture2D { + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = size; + const ctx = canvas.getContext("2d")!; + const gradient = ctx.createLinearGradient(0, 0, size, size); + gradient.addColorStop(0, "#e74c3c"); + gradient.addColorStop(0.25, "#f39c12"); + gradient.addColorStop(0.5, "#2ecc71"); + gradient.addColorStop(0.75, "#3498db"); + gradient.addColorStop(1, "#9b59b6"); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, size, size); + const texture = new Texture2D(engine, size, size); + texture.setImageSource(canvas); + texture.generateMipmaps(); + return texture; +} + +/** Scrolls content up and down to show clipping effect */ +class ScrollScript extends Script { + speed = 60; + range = 100; + private _time = 0; + + onUpdate(dt: number) { + this._time += dt; + const y = Math.sin(this._time * this.speed * 0.02) * this.range; + const pos = this.entity.transform.position; + this.entity.transform.setPosition(pos.x, y, pos.z); + } +} + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = rootEntity.createChild("Canvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceOverlay; + uiCanvas.referenceResolution = new Vector2(800, 600); + uiCanvas.renderCamera = camera; + + const solidTexture = createColorTexture(engine, 255, 255, 255); + const gradientTexture = createGradientTexture(engine, 256); + + // ========== Left: Hard clip (alphaClip = true) ========== + const leftFrame = canvasEntity.createChild("LeftFrame"); + (leftFrame.transform).size = new Vector2(240, 300); + leftFrame.transform.setPosition(-180, 20, 0); + const leftBg = leftFrame.addComponent(Image); + leftBg.sprite = new Sprite(engine, solidTexture); + leftBg.color = new Color(0.12, 0.14, 0.18, 1); + + const leftViewport = leftFrame.createChild("Viewport"); + (leftViewport.transform).size = new Vector2(200, 220); + leftViewport.transform.setPosition(0, -10, 0); + const leftRectMask = leftViewport.addComponent(RectMask2D); + leftRectMask.alphaClip = true; + + const leftContent = leftViewport.createChild("Content"); + (leftContent.transform).size = new Vector2(200, 600); + const leftScroll = leftContent.addComponent(ScrollScript); + leftScroll.range = 150; + leftScroll.speed = 40; + + const tileColors = [ + new Color(0.91, 0.3, 0.24, 1), + new Color(0.16, 0.5, 0.73, 1), + new Color(0.18, 0.8, 0.44, 1), + new Color(0.95, 0.61, 0.07, 1), + new Color(0.56, 0.27, 0.68, 1), + new Color(0.2, 0.6, 0.86, 1) + ]; + for (let i = 0; i < 6; i++) { + const tile = leftContent.createChild(`Tile${i}`); + (tile.transform).size = new Vector2(180, 70); + tile.transform.setPosition(0, 230 - i * 90, 0); + const tileImg = tile.addComponent(Image); + tileImg.sprite = new Sprite(engine, solidTexture); + tileImg.color = tileColors[i]; + + const label = tile.createChild("Label"); + (label.transform).size = new Vector2(160, 30); + const text = label.addComponent(Text); + text.text = `Item ${i + 1}`; + text.fontSize = 20; + text.color = new Color(1, 1, 1, 1); + } + + const leftTitle = leftFrame.createChild("Title"); + (leftTitle.transform).size = new Vector2(200, 30); + leftTitle.transform.setPosition(0, 125, 0); + const leftTitleText = leftTitle.addComponent(Text); + leftTitleText.text = "Hard Clip"; + leftTitleText.fontSize = 18; + leftTitleText.color = new Color(1, 1, 1, 0.9); + + // ========== Right: Soft clip (softness > 0) ========== + const rightFrame = canvasEntity.createChild("RightFrame"); + (rightFrame.transform).size = new Vector2(240, 300); + rightFrame.transform.setPosition(180, 20, 0); + const rightBg = rightFrame.addComponent(Image); + rightBg.sprite = new Sprite(engine, solidTexture); + rightBg.color = new Color(0.12, 0.14, 0.18, 1); + + const rightViewport = rightFrame.createChild("Viewport"); + (rightViewport.transform).size = new Vector2(200, 220); + rightViewport.transform.setPosition(0, -10, 0); + const rightRectMask = rightViewport.addComponent(RectMask2D); + rightRectMask.softness = new Vector2(30, 30); + + const rightContent = rightViewport.createChild("Content"); + (rightContent.transform).size = new Vector2(300, 300); + const rightImage = rightContent.addComponent(Image); + rightImage.sprite = new Sprite(engine, gradientTexture); + const rightScroll = rightContent.addComponent(ScrollScript); + rightScroll.range = 60; + rightScroll.speed = 30; + + const rightTitle = rightFrame.createChild("Title"); + (rightTitle.transform).size = new Vector2(200, 30); + rightTitle.transform.setPosition(0, 125, 0); + const rightTitleText = rightTitle.addComponent(Text); + rightTitleText.text = "Soft Clip"; + rightTitleText.fontSize = 18; + rightTitleText.color = new Color(1, 1, 1, 0.9); + + engine.run(); + + console.log("UI 03 - RectMask2D 矩形裁剪"); + console.log("- 左: alphaClip=true, 硬裁剪 (discard), 模拟滚动列表"); + console.log("- 右: softness=(30,30), 柔和边缘裁剪, 渐变图片滚动"); + console.log("- RectMask2D 基于层级自动裁剪子节点, 使用 shader 实现"); +}); diff --git a/packages/core/src/2d/assembler/ISpriteAssembler.ts b/packages/core/src/2d/assembler/ISpriteAssembler.ts index 876c6f87f7..6d38637b20 100644 --- a/packages/core/src/2d/assembler/ISpriteAssembler.ts +++ b/packages/core/src/2d/assembler/ISpriteAssembler.ts @@ -1,21 +1,27 @@ -import { Matrix, Vector2 } from "@galacean/engine-math"; -import { ISpriteRenderer } from "./ISpriteRenderer"; +import { BoundingBox, Color, Matrix, Vector2 } from "@galacean/engine-math"; +import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; +import { SpriteTileMode } from "../enums/SpriteTileMode"; +import { SpritePrimitive } from "../sprite/SpritePrimitive"; /** * Interface for sprite assembler. */ export interface ISpriteAssembler { - resetData(renderer: ISpriteRenderer, vertexCount?: number): void; + resetData(primitive: SpritePrimitive, chunkManager: PrimitiveChunkManager, vertexCount?: number): void; updatePositions( - renderer: ISpriteRenderer, + primitive: SpritePrimitive, + chunkManager: PrimitiveChunkManager, worldMatrix: Matrix, width: number, height: number, pivot: Vector2, flipX: boolean, flipY: boolean, - referenceResolutionPerUnit?: number + outBounds: BoundingBox, + referenceResolutionPerUnit?: number, + tileMode?: SpriteTileMode, + tiledAdaptiveThreshold?: number ): void; - updateUVs(renderer: ISpriteRenderer): void; - updateColor(renderer: ISpriteRenderer, alpha: number): void; + updateUVs(primitive: SpritePrimitive): void; + updateColor(primitive: SpritePrimitive, color: Color, alpha: number): void; } diff --git a/packages/core/src/2d/assembler/ISpriteRenderer.ts b/packages/core/src/2d/assembler/ISpriteRenderer.ts deleted file mode 100644 index a72f4e9436..0000000000 --- a/packages/core/src/2d/assembler/ISpriteRenderer.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Color } from "@galacean/engine-math"; -import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; -import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; -import { SpriteTileMode } from "../enums/SpriteTileMode"; -import { Sprite } from "../sprite"; - -/** - * Interface for sprite renderer. - */ -export interface ISpriteRenderer { - sprite: Sprite; - color?: Color; - tileMode?: SpriteTileMode; - tiledAdaptiveThreshold?: number; - _subChunk: SubPrimitiveChunk; - _getChunkManager(): PrimitiveChunkManager; -} diff --git a/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts b/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts index 563f812106..8ffb875a17 100644 --- a/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts +++ b/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts @@ -1,7 +1,8 @@ -import { BoundingBox, Matrix, Vector2 } from "@galacean/engine-math"; +import { BoundingBox, Color, Matrix, Vector2 } from "@galacean/engine-math"; import { StaticInterfaceImplement } from "../../base/StaticInterfaceImplement"; +import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; +import { SpritePrimitive } from "../sprite/SpritePrimitive"; import { ISpriteAssembler } from "./ISpriteAssembler"; -import { ISpriteRenderer } from "./ISpriteRenderer"; /** * Assemble vertex data for the sprite renderer in simple mode. @@ -11,25 +12,26 @@ export class SimpleSpriteAssembler { private static _rectangleTriangles = [0, 1, 2, 2, 1, 3]; private static _matrix = new Matrix(); - static resetData(renderer: ISpriteRenderer): void { - const manager = renderer._getChunkManager(); - const lastSubChunk = renderer._subChunk; - lastSubChunk && manager.freeSubChunk(lastSubChunk); - const subChunk = manager.allocateSubChunk(4); + static resetData(primitive: SpritePrimitive, chunkManager: PrimitiveChunkManager): void { + const lastSubChunk = primitive.subChunk; + lastSubChunk && chunkManager.freeSubChunk(lastSubChunk); + const subChunk = chunkManager.allocateSubChunk(4); subChunk.indices = SimpleSpriteAssembler._rectangleTriangles; - renderer._subChunk = subChunk; + primitive.subChunk = subChunk; } static updatePositions( - renderer: ISpriteRenderer, + primitive: SpritePrimitive, + chunkManager: PrimitiveChunkManager, worldMatrix: Matrix, width: number, height: number, pivot: Vector2, flipX: boolean, - flipY: boolean + flipY: boolean, + outBounds: BoundingBox ): void { - const { sprite } = renderer; + const { sprite } = primitive; const { x: pivotX, y: pivotY } = pivot; // Position to World const modelMatrix = SimpleSpriteAssembler._matrix; @@ -52,7 +54,7 @@ export class SimpleSpriteAssembler { // --------------- // Update positions const spritePositions = sprite._getPositions(); - const subChunk = renderer._subChunk; + const subChunk = primitive.subChunk; const vertices = subChunk.chunk.vertices; for (let i = 0, o = subChunk.vertexArea.start; i < 4; ++i, o += 9) { const { x, y } = spritePositions[i]; @@ -62,14 +64,14 @@ export class SimpleSpriteAssembler { } // @ts-ignore - BoundingBox.transform(sprite._getBounds(), modelMatrix, renderer._bounds); + BoundingBox.transform(sprite._getBounds(), modelMatrix, outBounds); } - static updateUVs(renderer: ISpriteRenderer): void { - const spriteUVs = renderer.sprite._getUVs(); + static updateUVs(primitive: SpritePrimitive): void { + const spriteUVs = primitive.sprite._getUVs(); const { x: left, y: bottom } = spriteUVs[0]; const { x: right, y: top } = spriteUVs[3]; - const subChunk = renderer._subChunk; + const subChunk = primitive.subChunk; const vertices = subChunk.chunk.vertices; const offset = subChunk.vertexArea.start + 3; vertices[offset] = left; @@ -82,9 +84,9 @@ export class SimpleSpriteAssembler { vertices[offset + 28] = top; } - static updateColor(renderer: ISpriteRenderer, alpha: number): void { - const subChunk = renderer._subChunk; - const { r, g, b, a } = renderer.color; + static updateColor(primitive: SpritePrimitive, color: Color, alpha: number): void { + const subChunk = primitive.subChunk; + const { r, g, b, a } = color; const finalAlpha = a * alpha; const vertices = subChunk.chunk.vertices; for (let i = 0, o = subChunk.vertexArea.start + 5; i < 4; ++i, o += 9) { diff --git a/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts b/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts index 31d19d149c..24e07fd36c 100644 --- a/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts +++ b/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts @@ -1,7 +1,8 @@ -import { Matrix, Vector2 } from "@galacean/engine-math"; +import { BoundingBox, Color, Matrix, Vector2 } from "@galacean/engine-math"; import { StaticInterfaceImplement } from "../../base/StaticInterfaceImplement"; +import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; +import { SpritePrimitive } from "../sprite/SpritePrimitive"; import { ISpriteAssembler } from "./ISpriteAssembler"; -import { ISpriteRenderer } from "./ISpriteRenderer"; /** * Assemble vertex data for the sprite renderer in sliced mode. @@ -16,26 +17,27 @@ export class SlicedSpriteAssembler { private static _row = new Array(4); private static _column = new Array(4); - static resetData(renderer: ISpriteRenderer): void { - const manager = renderer._getChunkManager(); - const lastSubChunk = renderer._subChunk; - lastSubChunk && manager.freeSubChunk(lastSubChunk); - const subChunk = manager.allocateSubChunk(16); + static resetData(primitive: SpritePrimitive, chunkManager: PrimitiveChunkManager): void { + const lastSubChunk = primitive.subChunk; + lastSubChunk && chunkManager.freeSubChunk(lastSubChunk); + const subChunk = chunkManager.allocateSubChunk(16); subChunk.indices = SlicedSpriteAssembler._rectangleTriangles; - renderer._subChunk = subChunk; + primitive.subChunk = subChunk; } static updatePositions( - renderer: ISpriteRenderer, + primitive: SpritePrimitive, + chunkManager: PrimitiveChunkManager, worldMatrix: Matrix, width: number, height: number, pivot: Vector2, flipX: boolean, flipY: boolean, + outBounds: BoundingBox, referenceResolutionPerUnit: number = 1 ): void { - const { sprite } = renderer; + const { sprite } = primitive; const { border } = sprite; // Update local positions. const spritePositions = sprite._getPositions(); @@ -106,7 +108,7 @@ export class SlicedSpriteAssembler { // 0 - 4 - 8 - 12 // ------------------------ // Assemble position and uv. - const subChunk = renderer._subChunk; + const subChunk = primitive.subChunk; const vertices = subChunk.chunk.vertices; for (let i = 0, o = subChunk.vertexArea.start; i < 4; i++) { const rowValue = row[i]; @@ -118,17 +120,15 @@ export class SlicedSpriteAssembler { } } - // @ts-ignore - const bounds = renderer._bounds; - bounds.min.set(row[0], column[0], 0); - bounds.max.set(row[3], column[3], 0); - bounds.transform(modelMatrix); + outBounds.min.set(row[0], column[0], 0); + outBounds.max.set(row[3], column[3], 0); + outBounds.transform(modelMatrix); } - static updateUVs(renderer: ISpriteRenderer): void { - const subChunk = renderer._subChunk; + static updateUVs(primitive: SpritePrimitive): void { + const subChunk = primitive.subChunk; const vertices = subChunk.chunk.vertices; - const spriteUVs = renderer.sprite._getUVs(); + const spriteUVs = primitive.sprite._getUVs(); for (let i = 0, o = subChunk.vertexArea.start + 3; i < 4; i++) { const rowU = spriteUVs[i].x; for (let j = 0; j < 4; j++, o += 9) { @@ -138,9 +138,9 @@ export class SlicedSpriteAssembler { } } - static updateColor(renderer: ISpriteRenderer, alpha: number): void { - const subChunk = renderer._subChunk; - const { r, g, b, a } = renderer.color; + static updateColor(primitive: SpritePrimitive, color: Color, alpha: number): void { + const subChunk = primitive.subChunk; + const { r, g, b, a } = color; const finalAlpha = a * alpha; const vertices = subChunk.chunk.vertices; for (let i = 0, o = subChunk.vertexArea.start + 5; i < 16; ++i, o += 9) { diff --git a/packages/core/src/2d/assembler/TiledSpriteAssembler.ts b/packages/core/src/2d/assembler/TiledSpriteAssembler.ts index bc765c45f9..ea35c4ebcb 100644 --- a/packages/core/src/2d/assembler/TiledSpriteAssembler.ts +++ b/packages/core/src/2d/assembler/TiledSpriteAssembler.ts @@ -1,10 +1,12 @@ -import { MathUtil, Matrix, Vector2 } from "@galacean/engine-math"; +import { BoundingBox, Color, MathUtil, Matrix, Vector2 } from "@galacean/engine-math"; import { Logger } from "../../base"; import { StaticInterfaceImplement } from "../../base/StaticInterfaceImplement"; +import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; import { DisorderedArray } from "../../utils/DisorderedArray"; import { SpriteTileMode } from "../enums/SpriteTileMode"; +import { Sprite } from "../sprite/Sprite"; +import { SpritePrimitive } from "../sprite/SpritePrimitive"; import { ISpriteAssembler } from "./ISpriteAssembler"; -import { ISpriteRenderer } from "./ISpriteRenderer"; /** * Assemble vertex data for the sprite renderer in tiled mode. @@ -17,36 +19,52 @@ export class TiledSpriteAssembler { private static _uvRow = new DisorderedArray(); private static _uvColumn = new DisorderedArray(); - static resetData(renderer: ISpriteRenderer, vertexCount: number): void { + static resetData(primitive: SpritePrimitive, chunkManager: PrimitiveChunkManager, vertexCount: number): void { if (vertexCount) { - const manager = renderer._getChunkManager(); - const lastSubChunk = renderer._subChunk; + const lastSubChunk = primitive.subChunk; const sizeChanged = lastSubChunk && lastSubChunk.vertexArea.size !== vertexCount * 9; - sizeChanged && manager.freeSubChunk(lastSubChunk); + sizeChanged && chunkManager.freeSubChunk(lastSubChunk); if (!lastSubChunk || sizeChanged) { - const newSubChunk = manager.allocateSubChunk(vertexCount); + const newSubChunk = chunkManager.allocateSubChunk(vertexCount); newSubChunk.indices = []; - renderer._subChunk = newSubChunk; + primitive.subChunk = newSubChunk; } } } static updatePositions( - renderer: ISpriteRenderer, + primitive: SpritePrimitive, + chunkManager: PrimitiveChunkManager, worldMatrix: Matrix, width: number, height: number, pivot: Vector2, flipX: boolean, flipY: boolean, - referenceResolutionPerUnit: number = 1 + outBounds: BoundingBox, + referenceResolutionPerUnit: number = 1, + tileMode?: SpriteTileMode, + tiledAdaptiveThreshold?: number ): void { // Calculate row and column const { _posRow: rPos, _posColumn: cPos, _uvRow: rUV, _uvColumn: cUV } = TiledSpriteAssembler; TiledSpriteAssembler.resetData( - renderer, - TiledSpriteAssembler._calculateDividing(renderer, width, height, rPos, cPos, rUV, cUV, referenceResolutionPerUnit) + primitive, + chunkManager, + TiledSpriteAssembler._calculateDividing( + primitive.sprite, + tileMode, + tiledAdaptiveThreshold, + chunkManager.maxVertexCount, + width, + height, + rPos, + cPos, + rUV, + cUV, + referenceResolutionPerUnit + ) ); // Update renderer's worldMatrix const { x: pivotX, y: pivotY } = pivot; @@ -71,7 +89,7 @@ export class TiledSpriteAssembler { const rowLength = rPos.length - 1; const columnLength = cPos.length - 1; - const subChunk = renderer._subChunk; + const subChunk = primitive.subChunk; const vertices = subChunk.chunk.vertices; const indices = subChunk.indices; let count = 0; @@ -118,18 +136,16 @@ export class TiledSpriteAssembler { } } - // @ts-ignore - const bounds = renderer._bounds; - bounds.min.set(rPos.get(0), cPos.get(0), 0); - bounds.max.set(rPos.get(rowLength), cPos.get(columnLength), 0); - bounds.transform(modelMatrix); + outBounds.min.set(rPos.get(0), cPos.get(0), 0); + outBounds.max.set(rPos.get(rowLength), cPos.get(columnLength), 0); + outBounds.transform(modelMatrix); } - static updateUVs(renderer: ISpriteRenderer): void { + static updateUVs(primitive: SpritePrimitive): void { const { _posRow: posRow, _posColumn: posColumn, _uvRow: uvRow, _uvColumn: uvColumn } = TiledSpriteAssembler; const rowLength = posRow.length - 1; const columnLength = posColumn.length - 1; - const subChunk = renderer._subChunk; + const subChunk = primitive.subChunk; const vertices = subChunk.chunk.vertices; for (let j = 0, o = subChunk.vertexArea.start + 3; j < columnLength; j++) { const doubleJ = 2 * j; @@ -159,9 +175,9 @@ export class TiledSpriteAssembler { } } - static updateColor(renderer: ISpriteRenderer, alpha: number): void { - const subChunk = renderer._subChunk; - const { r, g, b, a } = renderer.color; + static updateColor(primitive: SpritePrimitive, color: Color, alpha: number): void { + const subChunk = primitive.subChunk; + const { r, g, b, a } = color; const finalAlpha = a * alpha; const vertices = subChunk.chunk.vertices; const vertexArea = subChunk.vertexArea; @@ -174,7 +190,10 @@ export class TiledSpriteAssembler { } private static _calculateDividing( - renderer: ISpriteRenderer, + sprite: Sprite, + tileMode: SpriteTileMode, + threshold: number, + maxVertexCount: number, width: number, height: number, rPos: DisorderedArray, @@ -183,7 +202,6 @@ export class TiledSpriteAssembler { cUV: DisorderedArray, referenceResolutionPerUnit: number ): number { - const { sprite, tiledAdaptiveThreshold: threshold } = renderer; const { border } = sprite; const spritePositions = sprite._getPositions(); const { x: left, y: bottom } = spritePositions[0]; @@ -199,7 +217,7 @@ export class TiledSpriteAssembler { const fixedB = expectHeight * border.y; const fixedTB = fixedT + fixedB; const fixedCH = expectHeight - fixedTB; - const isAdaptive = renderer.tileMode === SpriteTileMode.Adaptive; + const isAdaptive = tileMode === SpriteTileMode.Adaptive; let rType: TiledType, rBlocksCount: number, rTiledCount: number; let cType: TiledType, cBlocksCount: number, cTiledCount: number; if (fixedLR >= width) { @@ -241,7 +259,6 @@ export class TiledSpriteAssembler { rPos.length = cPos.length = rUV.length = cUV.length = 0; const vertexCount = rBlocksCount * cBlocksCount * 4; - const maxVertexCount = renderer._getChunkManager().maxVertexCount; if (vertexCount > maxVertexCount) { rPos.add(width * left), rPos.add(width * right); cPos.add(height * bottom), cPos.add(height * top); diff --git a/packages/core/src/2d/index.ts b/packages/core/src/2d/index.ts index 47be64ccfc..65b033c745 100644 --- a/packages/core/src/2d/index.ts +++ b/packages/core/src/2d/index.ts @@ -1,5 +1,4 @@ export type { ISpriteAssembler } from "./assembler/ISpriteAssembler"; -export type { ISpriteRenderer } from "./assembler/ISpriteRenderer"; export { SimpleSpriteAssembler } from "./assembler/SimpleSpriteAssembler"; export { SlicedSpriteAssembler } from "./assembler/SlicedSpriteAssembler"; export { TiledSpriteAssembler } from "./assembler/TiledSpriteAssembler"; diff --git a/packages/core/src/2d/sprite/SpriteMask.ts b/packages/core/src/2d/sprite/SpriteMask.ts index 141f352d35..1f1d904e6e 100644 --- a/packages/core/src/2d/sprite/SpriteMask.ts +++ b/packages/core/src/2d/sprite/SpriteMask.ts @@ -1,4 +1,4 @@ -import { BoundingBox } from "@galacean/engine-math"; +import { Vector2, Vector3 } from "@galacean/engine-math"; import { Entity } from "../../Entity"; import { RenderQueueFlags } from "../../RenderPipeline/BasicRenderPipeline"; import { BatchUtils } from "../../RenderPipeline/BatchUtils"; @@ -10,16 +10,17 @@ import { SubRenderElement } from "../../RenderPipeline/SubRenderElement"; import { Renderer, RendererUpdateFlags } from "../../Renderer"; import { assignmentClone, ignoreClone } from "../../clone/CloneManager"; import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; +import { Material } from "../../material"; import { ShaderProperty } from "../../shader/ShaderProperty"; -import { ISpriteRenderer } from "../assembler/ISpriteRenderer"; -import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; -import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; -import { Sprite } from "./Sprite"; +import { Texture2D } from "../../texture"; +import { SpriteDrawMode } from "../enums/SpriteDrawMode"; +import { SpriteRenderable, SpriteRenderableFlags } from "./SpriteRenderable"; +import { SpriteMaskUtils } from "./SpriteMaskUtils"; /** * A component for masking Sprites. */ -export class SpriteMask extends Renderer implements ISpriteRenderer { +export class SpriteMask extends SpriteRenderable(Renderer) { /** @internal */ static _textureProperty: ShaderProperty = ShaderProperty.getByName("renderer_MaskTexture"); /** @internal */ @@ -31,76 +32,58 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { /** @internal */ @ignoreClone _renderElement: RenderElement; - - /** @internal */ - @ignoreClone - _subChunk: SubPrimitiveChunk; /** @internal */ @ignoreClone _maskIndex: number = -1; - @ignoreClone - private _sprite: Sprite = null; - - @ignoreClone - private _automaticWidth: number = 0; - @ignoreClone - private _automaticHeight: number = 0; - @assignmentClone - private _customWidth: number = undefined; - @assignmentClone - private _customHeight: number = undefined; - @assignmentClone - private _flipX: boolean = false; - @assignmentClone - private _flipY: boolean = false; - @assignmentClone private _alphaCutoff: number = 0.5; /** - * Render width (in world coordinates). - * - * @remarks - * If width is set, return the set value, - * otherwise return `SpriteMask.sprite.width`. + * The minimum alpha value used by the mask to select the area of influence defined over the mask's sprite. Value between 0 and 1. */ - get width(): number { - if (this._customWidth !== undefined) { - return this._customWidth; - } else { - this._dirtyUpdateFlag & SpriteMaskUpdateFlags.AutomaticSize && this._calDefaultSize(); - return this._automaticWidth; + get alphaCutoff(): number { + return this._alphaCutoff; + } + + set alphaCutoff(value: number) { + if (this._alphaCutoff !== value) { + this._alphaCutoff = value; + this.shaderData.setFloat(SpriteMask._alphaCutoffProperty, value); } } + /** + * Render width. If set, uses custom value; otherwise uses sprite's natural width. + */ + get width(): number { + return this._getWidth(); + } + set width(value: number) { if (this._customWidth !== value) { this._customWidth = value; - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + this._dirtyUpdateFlag |= + this.drawMode === SpriteDrawMode.Tiled + ? SpriteRenderableFlags.WorldVolumeUVAndColor + : RendererUpdateFlags.WorldVolume; } } /** - * Render height (in world coordinates). - * - * @remarks - * If height is set, return the set value, - * otherwise return `SpriteMask.sprite.height`. + * Render height. If set, uses custom value; otherwise uses sprite's natural height. */ get height(): number { - if (this._customHeight !== undefined) { - return this._customHeight; - } else { - this._dirtyUpdateFlag & SpriteMaskUpdateFlags.AutomaticSize && this._calDefaultSize(); - return this._automaticHeight; - } + return this._getHeight(); } set height(value: number) { if (this._customHeight !== value) { this._customHeight = value; - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + this._dirtyUpdateFlag |= + this.drawMode === SpriteDrawMode.Tiled + ? SpriteRenderableFlags.WorldVolumeUVAndColor + : RendererUpdateFlags.WorldVolume; } } @@ -133,86 +116,86 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { } /** - * The Sprite to render. + * @internal */ - get sprite(): Sprite { - return this._sprite; + constructor(entity: Entity) { + super(entity); + this._initSpriteRenderable(SpriteMask._textureProperty); + this.shaderData.setFloat(SpriteMask._alphaCutoffProperty, this._alphaCutoff); + this._renderElement = new RenderElement(); + this._renderElement.addSubRenderElement(new SubRenderElement()); } - set sprite(value: Sprite | null) { - const lastSprite = this._sprite; - if (lastSprite !== value) { - if (lastSprite) { - this._addResourceReferCount(lastSprite, -1); - lastSprite._updateFlagManager.removeListener(this._onSpriteChange); - } - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.All; - if (value) { - this._addResourceReferCount(value, 1); - value._updateFlagManager.addListener(this._onSpriteChange); - this.shaderData.setTexture(SpriteMask._textureProperty, value.texture); - } else { - this.shaderData.setTexture(SpriteMask._textureProperty, null); - } - this._sprite = value; + // ===== SpriteRenderable abstract implementations ===== + + /** @internal */ + override _getWidth(): number { + if (this._customWidth !== undefined) { + return this._customWidth; + } + if (this._autoSizeDirty) { + this._calDefaultSize(); } + return this._automaticWidth; } - /** - * The minimum alpha value used by the mask to select the area of influence defined over the mask's sprite. Value between 0 and 1. - */ - get alphaCutoff(): number { - return this._alphaCutoff; + /** @internal */ + override _getHeight(): number { + if (this._customHeight !== undefined) { + return this._customHeight; + } + if (this._autoSizeDirty) { + this._calDefaultSize(); + } + return this._automaticHeight; } - set alphaCutoff(value: number) { - if (this._alphaCutoff !== value) { - this._alphaCutoff = value; - this.shaderData.setFloat(SpriteMask._alphaCutoffProperty, value); - } + /** @internal */ + override _getAlpha(): number { + return 1; } - /** - * @internal - */ - constructor(entity: Entity) { - super(entity); - SimpleSpriteAssembler.resetData(this); - this.setMaterial(this._engine._basicResources.spriteMaskDefaultMaterial); - this.shaderData.setFloat(SpriteMask._alphaCutoffProperty, this._alphaCutoff); - this._renderElement = new RenderElement(); - this._renderElement.addSubRenderElement(new SubRenderElement()); - this._onSpriteChange = this._onSpriteChange.bind(this); + /** @internal */ + override _getPivot(): Vector2 { + return this.sprite?.pivot; } - /** - * @internal - */ - override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean, batched: boolean): void { - //@todo: Always update world positions to buffer, should opt - super._updateTransformShaderData(context, onlyMVP, true); + /** @internal */ + override _getReferenceResolutionPerUnit(): number | undefined { + return undefined; } - /** - * @internal - */ - override _cloneTo(target: SpriteMask): void { - super._cloneTo(target); - target.sprite = this._sprite; + /** @internal */ + override _getChunkManager(): PrimitiveChunkManager { + return this.engine._batcherManager.primitiveChunkManagerMask; } - /** - * @internal - */ - override _canBatch(elementA: SubRenderElement, elementB: SubRenderElement): boolean { - return BatchUtils.canBatchSpriteMask(elementA, elementB); + /** @internal */ + override _getDefaultSpriteMaterial(): Material { + return this._engine._basicResources.spriteMaskDefaultMaterial; } - /** - * @internal - */ - override _batch(elementA: SubRenderElement, elementB?: SubRenderElement): void { - BatchUtils.batchFor2D(elementA, elementB); + /** @internal */ + override _submitSpriteRenderElement( + context: RenderContext, + material: Material, + subChunk: SubPrimitiveChunk, + texture: Texture2D + ): void { + const renderElement = this._renderElement; + const subRenderElement = renderElement.subRenderElements[0]; + renderElement.set(this.priority, this._distanceForSort); + subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, texture, subChunk); + subRenderElement.shaderPasses = material.shader.subShaders[0].passes; + subRenderElement.renderQueueFlags = RenderQueueFlags.All; + renderElement.addSubRenderElement(subRenderElement); + } + + // ===== Mask-specific overrides ===== + + /** @internal */ + override _canBatch(elementA: SubRenderElement, elementB: SubRenderElement): boolean { + return BatchUtils.canBatchSpriteMask(elementA, elementB); } /** @@ -234,152 +217,24 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { /** * @internal */ - _getChunkManager(): PrimitiveChunkManager { - return this.engine._batcherManager.primitiveChunkManagerMask; + _containsWorldPoint(worldPoint: Vector3): boolean { + const sprite = this.sprite; + if (!sprite) return false; + return SpriteMaskUtils.containsWorldPoint( + worldPoint, + sprite, + this._transformEntity.transform.worldMatrix, + this.width, + this.height, + sprite.pivot, + this.flipX, + this.flipY, + this.alphaCutoff + ); } - protected override _updateBounds(worldBounds: BoundingBox): void { - const sprite = this._sprite; - if (sprite) { - SimpleSpriteAssembler.updatePositions( - this, - this._transformEntity.transform.worldMatrix, - this.width, - this.height, - sprite.pivot, - this._flipX, - this._flipY - ); - } else { - const { worldPosition } = this._transformEntity.transform; - worldBounds.min.copyFrom(worldPosition); - worldBounds.max.copyFrom(worldPosition); - } - } - - /** - * @inheritdoc - */ - protected override _render(context: RenderContext): void { - const { _sprite: sprite } = this; - if (!sprite?.texture || !this.width || !this.height) { - return; - } - - let material = this.getMaterial(); - if (!material) { - return; - } - const { _engine: engine } = this; - // @todo: This question needs to be raised rather than hidden. - if (material.destroyed) { - material = engine._basicResources.spriteMaskDefaultMaterial; - } - - // Update position - if (this._dirtyUpdateFlag & RendererUpdateFlags.WorldVolume) { - SimpleSpriteAssembler.updatePositions( - this, - this._transformEntity.transform.worldMatrix, - this.width, - this.height, - sprite.pivot, - this._flipX, - this._flipY - ); - this._dirtyUpdateFlag &= ~RendererUpdateFlags.WorldVolume; - } - - // Update uv - if (this._dirtyUpdateFlag & SpriteMaskUpdateFlags.UV) { - SimpleSpriteAssembler.updateUVs(this); - this._dirtyUpdateFlag &= ~SpriteMaskUpdateFlags.UV; - } - - const renderElement = this._renderElement; - const subRenderElement = renderElement.subRenderElements[0]; - renderElement.set(this.priority, this._distanceForSort); - - const subChunk = this._subChunk; - subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, this.sprite.texture, subChunk); - subRenderElement.shaderPasses = material.shader.subShaders[0].passes; - subRenderElement.renderQueueFlags = RenderQueueFlags.All; - renderElement.addSubRenderElement(subRenderElement); - } - - /** - * @inheritdoc - */ protected override _onDestroy(): void { - const sprite = this._sprite; - if (sprite) { - this._addResourceReferCount(sprite, -1); - sprite._updateFlagManager.removeListener(this._onSpriteChange); - } - super._onDestroy(); - - this._sprite = null; - if (this._subChunk) { - this._getChunkManager().freeSubChunk(this._subChunk); - this._subChunk = null; - } - this._renderElement = null; } - - private _calDefaultSize(): void { - const sprite = this._sprite; - if (sprite) { - this._automaticWidth = sprite.width; - this._automaticHeight = sprite.height; - } else { - this._automaticWidth = this._automaticHeight = 0; - } - this._dirtyUpdateFlag &= ~SpriteMaskUpdateFlags.AutomaticSize; - } - - @ignoreClone - private _onSpriteChange(type: SpriteModifyFlags): void { - switch (type) { - case SpriteModifyFlags.texture: - this.shaderData.setTexture(SpriteMask._textureProperty, this.sprite.texture); - break; - case SpriteModifyFlags.size: - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.AutomaticSize; - if (this._customWidth === undefined || this._customHeight === undefined) { - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; - } - break; - case SpriteModifyFlags.region: - case SpriteModifyFlags.atlasRegionOffset: - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.WorldVolumeAndUV; - break; - case SpriteModifyFlags.atlasRegion: - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.UV; - break; - case SpriteModifyFlags.pivot: - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; - break; - case SpriteModifyFlags.destroy: - this.sprite = null; - break; - default: - break; - } - } -} - -/** - * @remarks Extends `RendererUpdateFlags`. - */ -enum SpriteMaskUpdateFlags { - /** UV. */ - UV = 0x2, - /** Automatic Size. */ - AutomaticSize = 0x4, - /** WorldVolume and UV. */ - WorldVolumeAndUV = 0x3, - /** All. */ - All = 0x7 -} +} \ No newline at end of file diff --git a/packages/core/src/2d/sprite/SpriteMaskUtils.ts b/packages/core/src/2d/sprite/SpriteMaskUtils.ts new file mode 100644 index 0000000000..b7033ee800 --- /dev/null +++ b/packages/core/src/2d/sprite/SpriteMaskUtils.ts @@ -0,0 +1,136 @@ +import { Matrix, Vector2, Vector3 } from "@galacean/engine-math"; +import { Texture2D, TextureFormat } from "../../texture"; +import { Sprite } from "./Sprite"; + +/** + * Internal helpers for sprite mask hit testing. + * @internal + */ +export class SpriteMaskUtils { + private static _tempMat: Matrix = new Matrix(); + private static _tempVec3: Vector3 = new Vector3(); + private static _u8Buffer1 = new Uint8Array(1); + private static _u8Buffer2 = new Uint8Array(2); + private static _u8Buffer4 = new Uint8Array(4); + private static _u16Buffer1 = new Uint16Array(1); + private static _u16Buffer4 = new Uint16Array(4); + private static _f32Buffer4 = new Float32Array(4); + private static _u32Buffer4 = new Uint32Array(4); + + static containsWorldPoint( + worldPoint: Vector3, + sprite: Sprite | null, + worldMatrix: Matrix, + width: number, + height: number, + pivot: Vector2, + flipX: boolean, + flipY: boolean, + alphaCutoff: number = 0 + ): boolean { + if (!sprite || !width || !height) { + return false; + } + + const worldMatrixInv = SpriteMaskUtils._tempMat; + Matrix.invert(worldMatrix, worldMatrixInv); + const localPosition = SpriteMaskUtils._tempVec3; + Vector3.transformCoordinate(worldPoint, worldMatrixInv, localPosition); + + const sx = flipX ? -width : width; + const sy = flipY ? -height : height; + if (!sx || !sy) { + return false; + } + + const spriteX = localPosition.x / sx + pivot.x; + const spriteY = localPosition.y / sy + pivot.y; + const spritePositions = sprite._getPositions(); + const { x: left, y: bottom } = spritePositions[0]; + const { x: right, y: top } = spritePositions[3]; + if (!(spriteX >= left && spriteX <= right && spriteY >= bottom && spriteY <= top)) { + return false; + } + + if (alphaCutoff <= 0) { + return true; + } + + const texture = sprite.texture; + if (!texture) { + return false; + } + + const spriteUVs = sprite._getUVs(); + const leftU = spriteUVs[0].x; + const bottomV = spriteUVs[0].y; + const rightU = spriteUVs[3].x; + const topV = spriteUVs[3].y; + const positionWidth = right - left; + const positionHeight = top - bottom; + if (!positionWidth || !positionHeight) { + return false; + } + + const tx = (spriteX - left) / positionWidth; + const ty = (spriteY - bottom) / positionHeight; + const u = leftU + (rightU - leftU) * tx; + const v = bottomV + (topV - bottomV) * ty; + const x = Math.min(Math.max(Math.floor(u * texture.width), 0), texture.width - 1); + const y = Math.min(Math.max(Math.floor(v * texture.height), 0), texture.height - 1); + return SpriteMaskUtils._sampleTextureAlpha(texture, x, y) >= alphaCutoff; + } + + private static _sampleTextureAlpha(texture: Texture2D, x: number, y: number): number { + try { + switch (texture.format) { + case TextureFormat.R8G8B8A8: { + const buffer = SpriteMaskUtils._u8Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3] / 255; + } + case TextureFormat.R4G4B4A4: { + const buffer = SpriteMaskUtils._u16Buffer1; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return (buffer[0] & 0xf) / 15; + } + case TextureFormat.R5G5B5A1: { + const buffer = SpriteMaskUtils._u16Buffer1; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[0] & 0x1; + } + case TextureFormat.Alpha8: + case TextureFormat.R8: { + const buffer = SpriteMaskUtils._u8Buffer1; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[0] / 255; + } + case TextureFormat.LuminanceAlpha: + case TextureFormat.R8G8: { + const buffer = SpriteMaskUtils._u8Buffer2; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[1] / 255; + } + case TextureFormat.R16G16B16A16: { + const buffer = SpriteMaskUtils._u16Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3] / 65535; + } + case TextureFormat.R32G32B32A32: { + const buffer = SpriteMaskUtils._f32Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3]; + } + case TextureFormat.R32G32B32A32_UInt: { + const buffer = SpriteMaskUtils._u32Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3] / 4294967295; + } + default: + return 1; + } + } catch { + return 1; + } + } +} diff --git a/packages/core/src/2d/sprite/SpritePrimitive.ts b/packages/core/src/2d/sprite/SpritePrimitive.ts new file mode 100644 index 0000000000..a54e2dd8d2 --- /dev/null +++ b/packages/core/src/2d/sprite/SpritePrimitive.ts @@ -0,0 +1,119 @@ +import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; +import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; +import { ShaderData } from "../../shader/ShaderData"; +import { ShaderProperty } from "../../shader/ShaderProperty"; +import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; +import { Sprite } from "./Sprite"; + +/** + * Minimal host interface required by SpritePrimitive. + * Both Renderer and UIRenderer satisfy this. + */ +export interface ISpritePrimitiveOwner { + shaderData: ShaderData; + _addResourceReferCount(resource: any, count: number): void; +} + +/** + * Manages sprite reference lifecycle: ref counting, change listener registration, + * texture shader property binding, and sub-chunk ownership. + * + * Shared by SpriteRenderable (SpriteRenderer/Image) and future SpriteMaskRenderable (SpriteMask/UIMask). + */ +export class SpritePrimitive { + /** The sub-chunk allocated for this sprite's vertex data. */ + subChunk: SubPrimitiveChunk = null; + + private _sprite: Sprite = null; + private _owner: ISpritePrimitiveOwner; + private _textureProperty: ShaderProperty; + private _onSpriteChanged: (type: SpriteModifyFlags | null) => void; + + /** + * The current sprite. + */ + get sprite(): Sprite | null { + return this._sprite; + } + + set sprite(value: Sprite | null) { + const lastSprite = this._sprite; + if (lastSprite !== value) { + if (lastSprite) { + this._owner._addResourceReferCount(lastSprite, -1); + lastSprite._updateFlagManager.removeListener(this._handleSpritePropertyChange); + } + if (value) { + this._owner._addResourceReferCount(value, 1); + value._updateFlagManager.addListener(this._handleSpritePropertyChange); + this._owner.shaderData.setTexture(this._textureProperty, value.texture); + } else { + this._owner.shaderData.setTexture(this._textureProperty, null); + } + this._sprite = value; + // Notify: sprite instance changed (null = full change, not a specific property) + this._onSpriteChanged(null); + } + } + + /** + * @param owner - The renderer that owns this primitive + * @param textureProperty - Shader property for texture binding + * @param onSpriteChanged - Callback for sprite changes. `null` type = sprite instance replaced; otherwise specific property changed. + * texture and destroy are handled internally and NOT forwarded. + */ + constructor( + owner: ISpritePrimitiveOwner, + textureProperty: ShaderProperty, + onSpriteChanged: (type: SpriteModifyFlags | null) => void + ) { + this._owner = owner; + this._textureProperty = textureProperty; + this._onSpriteChanged = onSpriteChanged; + this._handleSpritePropertyChange = this._handleSpritePropertyChange.bind(this); + } + + /** + * Clone sprite reference to target primitive. Triggers target's setter (ref counting + listener). + */ + cloneTo(target: SpritePrimitive): void { + target.sprite = this._sprite; + } + + /** + * Release sprite reference, listeners, and sub-chunk. Call from host's _onDestroy. + */ + destroy(chunkManager: PrimitiveChunkManager): void { + if (this.subChunk) { + chunkManager.freeSubChunk(this.subChunk); + this.subChunk = null; + } + + const sprite = this._sprite; + if (sprite) { + this._owner._addResourceReferCount(sprite, -1); + sprite._updateFlagManager.removeListener(this._handleSpritePropertyChange); + } + this._sprite = null; + this._owner = null; + this._onSpriteChanged = null; + } + + /** + * Listener for sprite property changes. Handles texture/destroy internally, + * forwards all other changes to the behavior layer via callback. + */ + private _handleSpritePropertyChange(type: SpriteModifyFlags): void { + switch (type) { + case SpriteModifyFlags.texture: + this._owner.shaderData.setTexture(this._textureProperty, this._sprite.texture); + break; + case SpriteModifyFlags.destroy: + this.sprite = null; + break; + default: + this._onSpriteChanged(type); + break; + } + } +} diff --git a/packages/core/src/2d/sprite/SpriteRenderable.ts b/packages/core/src/2d/sprite/SpriteRenderable.ts new file mode 100644 index 0000000000..31cf76e645 --- /dev/null +++ b/packages/core/src/2d/sprite/SpriteRenderable.ts @@ -0,0 +1,484 @@ +import { BoundingBox, Color, MathUtil, Vector2 } from "@galacean/engine-math"; +import { BatchUtils } from "../../RenderPipeline/BatchUtils"; +import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; +import { RenderContext } from "../../RenderPipeline/RenderContext"; +import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; +import { SubRenderElement } from "../../RenderPipeline/SubRenderElement"; +import { Renderer, RendererUpdateFlags } from "../../Renderer"; +import { assignmentClone, ignoreClone } from "../../clone/CloneManager"; +import { Material } from "../../material"; +import { ShaderProperty } from "../../shader/ShaderProperty"; +import { Texture2D } from "../../texture"; +import { ISpriteAssembler } from "../assembler/ISpriteAssembler"; +import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; +import { SlicedSpriteAssembler } from "../assembler/SlicedSpriteAssembler"; +import { TiledSpriteAssembler } from "../assembler/TiledSpriteAssembler"; +import { SpriteDrawMode } from "../enums/SpriteDrawMode"; +import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; +import { SpriteTileMode } from "../enums/SpriteTileMode"; +import { Sprite } from "./Sprite"; +import { SpritePrimitive } from "./SpritePrimitive"; + +/** + * @remarks Extends `RendererUpdateFlags`. + */ +export enum SpriteRenderableFlags { + /** Color. */ + Color = 0x2, + /** UV. */ + UV = 0x4, + + /** WorldVolume and UV. */ + WorldVolumeAndUV = 0x5, + /** WorldVolume, UV and Color. */ + WorldVolumeUVAndColor = 0x7, + /** All. */ + All = 0x7 +} + +type RendererConstructor = abstract new (...args: any[]) => Renderer; + +/** + * Public contract of the SpriteRenderable mixin. + */ +export interface ISpriteRenderable { + sprite: Sprite | null; + drawMode: SpriteDrawMode; + tileMode: SpriteTileMode; + tiledAdaptiveThreshold: number; + _spriteData: SpritePrimitive; + /** @internal */ + _customWidth?: number; + /** @internal */ + _customHeight?: number; + /** @internal */ + _automaticWidth: number; + /** @internal */ + _automaticHeight: number; + /** @internal */ + _autoSizeDirty: boolean; + /** @internal */ + _flipX: boolean; + /** @internal */ + _flipY: boolean; + /** @internal */ + _calDefaultSize(): void; + _getChunkManager(): PrimitiveChunkManager; + _getDefaultSpriteMaterial(): Material; + _getColor(): Color | null; + _getAlpha(): number; + _getWidth(): number; + _getHeight(): number; + _getPivot(): Vector2; + _getFlipX(): boolean; + _getFlipY(): boolean; + _getReferenceResolutionPerUnit(): number | undefined; + _onSpriteSizeChanged(): void; + _onSpritePivotChanged(): void; + _submitSpriteRenderElement( + context: RenderContext, + material: Material, + subChunk: SubPrimitiveChunk, + texture: Texture2D + ): void; + _initSpriteRenderable(textureProperty: ShaderProperty): void; +} + +/** + * Wiring mixin that provides shared sprite rendering logic for both 2D SpriteRenderer and UI Image. + * + * Discipline: this mixin only handles wiring (forwarding, lifecycle hookup, abstract declarations). + * All host-specific behavior is accessed through abstract methods, composition objects, and hooks. + * The mixin NEVER touches host private fields directly. + */ +export function SpriteRenderable( + Base: T +): (abstract new (...args: any[]) => ISpriteRenderable) & T { + abstract class SpriteRenderableHost extends Base { + /** @internal */ + @ignoreClone + _spriteData: SpritePrimitive; + + @ignoreClone + private _drawMode: SpriteDrawMode; + @ignoreClone + private _assembler: ISpriteAssembler; + @assignmentClone + private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; + @assignmentClone + private _tiledAdaptiveThreshold: number = 0.5; + + // ===== Size management (optional, for 2D sprites) ===== + + /** @internal */ + @ignoreClone + protected _customWidth?: number; + /** @internal */ + @ignoreClone + protected _customHeight?: number; + /** @internal */ + @ignoreClone + protected _automaticWidth: number = 0; + /** @internal */ + @ignoreClone + protected _automaticHeight: number = 0; + /** @internal */ + @ignoreClone + protected _autoSizeDirty: boolean = true; + /** @internal */ + @assignmentClone + protected _flipX: boolean = false; + /** @internal */ + @assignmentClone + protected _flipY: boolean = false; + + // ===== Abstract methods: host MUST implement ===== + + /** Which PrimitiveChunkManager to allocate vertex data from. */ + abstract _getChunkManager(): PrimitiveChunkManager; + + /** Default material when material is null or destroyed. */ + abstract _getDefaultSpriteMaterial(): Material; + + /** Push the final render element to the appropriate pipeline. */ + abstract _submitSpriteRenderElement( + context: RenderContext, + material: Material, + subChunk: SubPrimitiveChunk, + texture: Texture2D + ): void; + + // ===== Abstract methods: host MUST implement (avoids MRO shadowing) ===== + + /** Get width for rendering. 2D hosts use custom/automatic size; UI hosts use UITransform.size. */ + abstract _getWidth(): number; + + /** Get height for rendering. 2D hosts use custom/automatic size; UI hosts use UITransform.size. */ + abstract _getHeight(): number; + + /** Final alpha multiplier. 2D hosts return 1; UI hosts return globalAlpha. */ + abstract _getAlpha(): number; + + /** Pivot for rendering. 2D hosts return sprite pivot; UI hosts return UITransform pivot. */ + abstract _getPivot(): Vector2; + + /** Reference resolution per unit. 2D hosts return undefined; UI hosts return canvas value. */ + abstract _getReferenceResolutionPerUnit(): number | undefined; + + // ===== Methods with defaults: host CAN override ===== + + /** Color for vertex coloring. Default: null (no color, for masks). */ + _getColor(): Color | null { + return null; + } + + /** Whether to flip X. Default: returns internal _flipX. */ + _getFlipX(): boolean { + return this._flipX; + } + + /** Whether to flip Y. Default: returns internal _flipY. */ + _getFlipY(): boolean { + return this._flipY; + } + + /** Called when sprite size changes. Default: mark auto size dirty. */ + _onSpriteSizeChanged(): void { + this._autoSizeDirty = true; + if (this._customWidth === undefined || this._customHeight === undefined) { + this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + } + } + + /** Called when sprite pivot changes. Default: mark world volume dirty. */ + _onSpritePivotChanged(): void { + this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + } + + // ===== Internal helpers ===== + + /** + * Calculate default size from sprite. + * @internal + */ + protected _calDefaultSize(): void { + const sprite = this._spriteData.sprite; + if (sprite) { + this._automaticWidth = sprite.width; + this._automaticHeight = sprite.height; + } else { + this._automaticWidth = this._automaticHeight = 0; + } + this._autoSizeDirty = false; + } + + // ===== Public API (forwarding) ===== + + /** + * The Sprite to render. + */ + get sprite(): Sprite | null { + return this._spriteData.sprite; + } + + set sprite(value: Sprite | null) { + this._spriteData.sprite = value; + } + + /** + * The draw mode of the sprite. + */ + get drawMode(): SpriteDrawMode { + return this._drawMode; + } + + set drawMode(value: SpriteDrawMode) { + if (this._drawMode !== value) { + this._drawMode = value; + switch (value) { + case SpriteDrawMode.Simple: + this._assembler = SimpleSpriteAssembler; + break; + case SpriteDrawMode.Sliced: + this._assembler = SlicedSpriteAssembler; + break; + case SpriteDrawMode.Tiled: + this._assembler = TiledSpriteAssembler; + break; + default: + break; + } + this._assembler.resetData(this._spriteData, this._getChunkManager()); + this._dirtyUpdateFlag |= SpriteRenderableFlags.WorldVolumeUVAndColor; + } + } + + /** + * The tiling mode of the sprite. (Only works in tiled mode.) + */ + get tileMode(): SpriteTileMode { + return this._tileMode; + } + + set tileMode(value: SpriteTileMode) { + if (this._tileMode !== value) { + this._tileMode = value; + if (this._drawMode === SpriteDrawMode.Tiled) { + this._dirtyUpdateFlag |= SpriteRenderableFlags.WorldVolumeUVAndColor; + } + } + } + + /** + * Stretch Threshold in Tile Adaptive Mode, specified in normalized. (Only works in tiled adaptive mode.) + */ + get tiledAdaptiveThreshold(): number { + return this._tiledAdaptiveThreshold; + } + + set tiledAdaptiveThreshold(value: number) { + if (value !== this._tiledAdaptiveThreshold) { + value = MathUtil.clamp(value, 0, 1); + this._tiledAdaptiveThreshold = value; + if (this._drawMode === SpriteDrawMode.Tiled) { + this._dirtyUpdateFlag |= SpriteRenderableFlags.WorldVolumeUVAndColor; + } + } + } + + // ===== Wiring: init ===== + + /** + * Initialize sprite renderable state. Must be called from subclass constructor. + * @param textureProperty - The shader property used for sprite texture binding. + * @internal + */ + _initSpriteRenderable(textureProperty: ShaderProperty): void { + this._spriteData = new SpritePrimitive(this as any, textureProperty, this._onSpriteChanged.bind(this)); + this.drawMode = SpriteDrawMode.Simple; + this._dirtyUpdateFlag |= SpriteRenderableFlags.Color; + this.setMaterial(this._getDefaultSpriteMaterial()); + } + + // ===== Wiring: lifecycle ===== + + /** + * @internal + */ + override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean, batched: boolean): void { + //@todo: Always update world positions to buffer, should opt + super._updateTransformShaderData(context, onlyMVP, true); + } + + /** + * @internal + */ + // @ts-ignore + override _cloneTo(target: SpriteRenderableHost): void { + // @ts-ignore + super._cloneTo(target); + this._spriteData.cloneTo(target._spriteData); + target.drawMode = this._drawMode; + } + + /** + * @internal + */ + override _canBatch(elementA: SubRenderElement, elementB: SubRenderElement): boolean { + return BatchUtils.canBatchSprite(elementA, elementB); + } + + /** + * @internal + */ + override _batch(elementA: SubRenderElement, elementB?: SubRenderElement): void { + BatchUtils.batchFor2D(elementA, elementB); + } + + protected override _updateBounds(worldBounds: BoundingBox): void { + if (this._spriteData.sprite) { + this._assembler.updatePositions( + this._spriteData, + this._getChunkManager(), + this._transformEntity.transform.worldMatrix, + this._getWidth(), + this._getHeight(), + this._getPivot(), + this._getFlipX(), + this._getFlipY(), + this._bounds, + this._getReferenceResolutionPerUnit(), + this._tileMode, + this._tiledAdaptiveThreshold + ); + } else { + const { worldPosition } = this._transformEntity.transform; + worldBounds.min.copyFrom(worldPosition); + worldBounds.max.copyFrom(worldPosition); + } + } + + protected override _render(context: RenderContext): void { + const sprite = this._spriteData.sprite; + const width = this._getWidth(); + const height = this._getHeight(); + if (!sprite?.texture || !width || !height) { + return; + } + + let material = this.getMaterial(); + if (!material) { + return; + } + // @todo: This question needs to be raised rather than hidden. + if (material.destroyed) { + material = this._getDefaultSpriteMaterial(); + } + + const color = this._getColor(); + const alpha = this._getAlpha(); + if (color && color.a * alpha <= 0) { + return; + } + + // Update position + if (this._dirtyUpdateFlag & RendererUpdateFlags.WorldVolume) { + this._assembler.updatePositions( + this._spriteData, + this._getChunkManager(), + this._transformEntity.transform.worldMatrix, + this._getWidth(), + this._getHeight(), + this._getPivot(), + this._getFlipX(), + this._getFlipY(), + this._bounds, + this._getReferenceResolutionPerUnit(), + this._tileMode, + this._tiledAdaptiveThreshold + ); + this._dirtyUpdateFlag &= ~RendererUpdateFlags.WorldVolume; + } + + // Update uv + if (this._dirtyUpdateFlag & SpriteRenderableFlags.UV) { + this._assembler.updateUVs(this._spriteData); + this._dirtyUpdateFlag &= ~SpriteRenderableFlags.UV; + } + + // Update color + if (color && this._dirtyUpdateFlag & SpriteRenderableFlags.Color) { + this._assembler.updateColor(this._spriteData, color, alpha); + this._dirtyUpdateFlag &= ~SpriteRenderableFlags.Color; + } + + // Submit + this._submitSpriteRenderElement(context, material, this._spriteData.subChunk, sprite.texture); + } + + protected override _onDestroy(): void { + this._spriteData.destroy(this._getChunkManager()); + + this._assembler = null; + + super._onDestroy(); + } + + // ===== Wiring: sprite change dispatch ===== + + /** + * Callback from SpritePrimitive. + * `type === null` means sprite instance was replaced; otherwise a specific property changed. + */ + private _onSpriteChanged(type: SpriteModifyFlags | null): void { + if (type === null) { + // Sprite instance replaced — mark everything dirty + this._dirtyUpdateFlag |= SpriteRenderableFlags.All; + this._onSpriteSizeChanged(); + return; + } + + switch (type) { + case SpriteModifyFlags.size: + this._onSpriteSizeChanged(); + switch (this._drawMode) { + case SpriteDrawMode.Sliced: + this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + break; + case SpriteDrawMode.Tiled: + this._dirtyUpdateFlag |= SpriteRenderableFlags.WorldVolumeUVAndColor; + break; + default: + break; + } + break; + case SpriteModifyFlags.border: + switch (this._drawMode) { + case SpriteDrawMode.Sliced: + this._dirtyUpdateFlag |= SpriteRenderableFlags.WorldVolumeAndUV; + break; + case SpriteDrawMode.Tiled: + this._dirtyUpdateFlag |= SpriteRenderableFlags.WorldVolumeUVAndColor; + break; + default: + break; + } + break; + case SpriteModifyFlags.region: + case SpriteModifyFlags.atlasRegionOffset: + this._dirtyUpdateFlag |= SpriteRenderableFlags.WorldVolumeAndUV; + break; + case SpriteModifyFlags.atlasRegion: + this._dirtyUpdateFlag |= SpriteRenderableFlags.UV; + break; + case SpriteModifyFlags.pivot: + this._onSpritePivotChanged(); + break; + default: + break; + } + } + } + + return SpriteRenderableHost as unknown as (abstract new (...args: any[]) => ISpriteRenderable) & T; +} diff --git a/packages/core/src/2d/sprite/SpriteRenderer.ts b/packages/core/src/2d/sprite/SpriteRenderer.ts index c1b183ee7a..713393cf5b 100644 --- a/packages/core/src/2d/sprite/SpriteRenderer.ts +++ b/packages/core/src/2d/sprite/SpriteRenderer.ts @@ -1,149 +1,34 @@ -import { BoundingBox, Color, MathUtil } from "@galacean/engine-math"; +import { Color, Vector2 } from "@galacean/engine-math"; import { Entity } from "../../Entity"; -import { BatchUtils } from "../../RenderPipeline/BatchUtils"; import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; import { RenderContext } from "../../RenderPipeline/RenderContext"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; -import { SubRenderElement } from "../../RenderPipeline/SubRenderElement"; import { Renderer, RendererUpdateFlags } from "../../Renderer"; import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; +import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; +import { Material } from "../../material"; import { ShaderProperty } from "../../shader/ShaderProperty"; -import { ISpriteAssembler } from "../assembler/ISpriteAssembler"; -import { ISpriteRenderer } from "../assembler/ISpriteRenderer"; -import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; -import { SlicedSpriteAssembler } from "../assembler/SlicedSpriteAssembler"; -import { TiledSpriteAssembler } from "../assembler/TiledSpriteAssembler"; +import { Texture2D } from "../../texture"; import { SpriteDrawMode } from "../enums/SpriteDrawMode"; import { SpriteMaskInteraction } from "../enums/SpriteMaskInteraction"; -import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; -import { SpriteTileMode } from "../enums/SpriteTileMode"; -import { Sprite } from "./Sprite"; -import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; +import { SpriteRenderable, SpriteRenderableFlags } from "./SpriteRenderable"; /** * Renders a Sprite for 2D graphics. */ -export class SpriteRenderer extends Renderer implements ISpriteRenderer { +export class SpriteRenderer extends SpriteRenderable(Renderer) { /** @internal */ static _textureProperty: ShaderProperty = ShaderProperty.getByName("renderer_SpriteTexture"); /** @internal */ - @ignoreClone - _subChunk: SubPrimitiveChunk; - - @ignoreClone - private _drawMode: SpriteDrawMode; - @ignoreClone - private _assembler: ISpriteAssembler; @assignmentClone - private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; + _maskInteraction: SpriteMaskInteraction = SpriteMaskInteraction.None; + /** @internal */ @assignmentClone - private _tiledAdaptiveThreshold: number = 0.5; + _maskLayer: SpriteMaskLayer = SpriteMaskLayer.Layer0; @deepClone private _color: Color = new Color(1, 1, 1, 1); - @ignoreClone - private _sprite: Sprite = null; - - @ignoreClone - private _automaticWidth: number = 0; - @ignoreClone - private _automaticHeight: number = 0; - @assignmentClone - private _customWidth: number = undefined; - @assignmentClone - private _customHeight: number = undefined; - @assignmentClone - private _flipX: boolean = false; - @assignmentClone - private _flipY: boolean = false; - - /** - * The draw mode of the sprite renderer. - */ - get drawMode(): SpriteDrawMode { - return this._drawMode; - } - - set drawMode(value: SpriteDrawMode) { - if (this._drawMode !== value) { - this._drawMode = value; - switch (value) { - case SpriteDrawMode.Simple: - this._assembler = SimpleSpriteAssembler; - break; - case SpriteDrawMode.Sliced: - this._assembler = SlicedSpriteAssembler; - break; - case SpriteDrawMode.Tiled: - this._assembler = TiledSpriteAssembler; - break; - default: - break; - } - this._assembler.resetData(this); - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeUVAndColor; - } - } - - /** - * The tiling mode of the sprite renderer. (Only works in tiled mode.) - */ - get tileMode(): SpriteTileMode { - return this._tileMode; - } - - set tileMode(value: SpriteTileMode) { - if (this._tileMode !== value) { - this._tileMode = value; - if (this.drawMode === SpriteDrawMode.Tiled) { - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeUVAndColor; - } - } - } - - /** - * Stretch Threshold in Tile Adaptive Mode, specified in normalized. (Only works in tiled adaptive mode.) - */ - get tiledAdaptiveThreshold(): number { - return this._tiledAdaptiveThreshold; - } - - set tiledAdaptiveThreshold(value: number) { - if (value !== this._tiledAdaptiveThreshold) { - value = MathUtil.clamp(value, 0, 1); - this._tiledAdaptiveThreshold = value; - if (this.drawMode === SpriteDrawMode.Tiled) { - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeUVAndColor; - } - } - } - - /** - * The Sprite to render. - */ - get sprite(): Sprite { - return this._sprite; - } - - set sprite(value: Sprite | null) { - const lastSprite = this._sprite; - if (lastSprite !== value) { - if (lastSprite) { - this._addResourceReferCount(lastSprite, -1); - lastSprite._updateFlagManager.removeListener(this._onSpriteChange); - } - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.All; - if (value) { - this._addResourceReferCount(value, 1); - value._updateFlagManager.addListener(this._onSpriteChange); - this.shaderData.setTexture(SpriteRenderer._textureProperty, value.texture); - } else { - this.shaderData.setTexture(SpriteRenderer._textureProperty, null); - } - this._sprite = value; - } - } /** * Rendering color for the Sprite graphic. @@ -159,53 +44,35 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { } /** - * Render width (in world coordinates). - * - * @remarks - * If width is set, return the set value, - * otherwise return `SpriteRenderer.sprite.width`. + * Render width. If set, uses custom value; otherwise uses sprite's natural width. */ get width(): number { - if (this._customWidth !== undefined) { - return this._customWidth; - } else { - this._dirtyUpdateFlag & SpriteRendererUpdateFlags.AutomaticSize && this._calDefaultSize(); - return this._automaticWidth; - } + return this._getWidth(); } set width(value: number) { if (this._customWidth !== value) { this._customWidth = value; this._dirtyUpdateFlag |= - this._drawMode === SpriteDrawMode.Tiled - ? SpriteRendererUpdateFlags.WorldVolumeUVAndColor + this.drawMode === SpriteDrawMode.Tiled + ? SpriteRenderableFlags.WorldVolumeUVAndColor : RendererUpdateFlags.WorldVolume; } } /** - * Render height (in world coordinates). - * - * @remarks - * If height is set, return the set value, - * otherwise return `SpriteRenderer.sprite.height`. + * Render height. If set, uses custom value; otherwise uses sprite's natural height. */ get height(): number { - if (this._customHeight !== undefined) { - return this._customHeight; - } else { - this._dirtyUpdateFlag & SpriteRendererUpdateFlags.AutomaticSize && this._calDefaultSize(); - return this._automaticHeight; - } + return this._getHeight(); } set height(value: number) { if (this._customHeight !== value) { this._customHeight = value; this._dirtyUpdateFlag |= - this._drawMode === SpriteDrawMode.Tiled - ? SpriteRendererUpdateFlags.WorldVolumeUVAndColor + this.drawMode === SpriteDrawMode.Tiled + ? SpriteRenderableFlags.WorldVolumeUVAndColor : RendererUpdateFlags.WorldVolume; } } @@ -267,228 +134,86 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { */ constructor(entity: Entity) { super(entity); - this.drawMode = SpriteDrawMode.Simple; - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.Color; - this.setMaterial(this._engine._basicResources.spriteDefaultMaterial); - this._onSpriteChange = this._onSpriteChange.bind(this); + this._initSpriteRenderable(SpriteRenderer._textureProperty); //@ts-ignore this._color._onValueChanged = this._onColorChanged.bind(this); } - /** - * @internal - */ - override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean, batched: boolean): void { - //@todo: Always update world positions to buffer, should opt - super._updateTransformShaderData(context, onlyMVP, true); - } + // ===== Abstract implementations ===== - /** - * @internal - */ - override _cloneTo(target: SpriteRenderer): void { - super._cloneTo(target); - target.sprite = this._sprite; - target.drawMode = this._drawMode; + /** @internal */ + override _getColor(): Color { + return this._color; } - /** - * @internal - */ - override _canBatch(elementA: SubRenderElement, elementB: SubRenderElement): boolean { - return BatchUtils.canBatchSprite(elementA, elementB); + /** @internal */ + override _getWidth(): number { + if (this._customWidth !== undefined) { + return this._customWidth; + } + if (this._autoSizeDirty) { + this._calDefaultSize(); + } + return this._automaticWidth; } - /** - * @internal - */ - override _batch(elementA: SubRenderElement, elementB?: SubRenderElement): void { - BatchUtils.batchFor2D(elementA, elementB); + /** @internal */ + override _getHeight(): number { + if (this._customHeight !== undefined) { + return this._customHeight; + } + if (this._autoSizeDirty) { + this._calDefaultSize(); + } + return this._automaticHeight; } - /** - * @internal - */ - _getChunkManager(): PrimitiveChunkManager { - return this.engine._batcherManager.primitiveChunkManager2D; + /** @internal */ + override _getAlpha(): number { + return 1; } - protected override _updateBounds(worldBounds: BoundingBox): void { - const sprite = this._sprite; - if (sprite) { - this._assembler.updatePositions( - this, - this._transformEntity.transform.worldMatrix, - this.width, - this.height, - sprite.pivot, - this._flipX, - this._flipY - ); - } else { - const { worldPosition } = this._transformEntity.transform; - worldBounds.min.copyFrom(worldPosition); - worldBounds.max.copyFrom(worldPosition); - } + /** @internal */ + override _getPivot(): Vector2 { + return this.sprite?.pivot; } - protected override _render(context: RenderContext): void { - const { _sprite: sprite } = this; - if (!sprite?.texture || !this.width || !this.height) { - return; - } - - let material = this.getMaterial(); - if (!material) { - return; - } - // @todo: This question needs to be raised rather than hidden. - if (material.destroyed) { - material = this._engine._basicResources.spriteDefaultMaterial; - } - - // Update position - if (this._dirtyUpdateFlag & RendererUpdateFlags.WorldVolume) { - this._assembler.updatePositions( - this, - this._transformEntity.transform.worldMatrix, - this.width, - this.height, - sprite.pivot, - this._flipX, - this._flipY - ); - this._dirtyUpdateFlag &= ~RendererUpdateFlags.WorldVolume; - } + /** @internal */ + override _getReferenceResolutionPerUnit(): number | undefined { + return undefined; + } - // Update uv - if (this._dirtyUpdateFlag & SpriteRendererUpdateFlags.UV) { - this._assembler.updateUVs(this); - this._dirtyUpdateFlag &= ~SpriteRendererUpdateFlags.UV; - } + /** @internal */ + override _getChunkManager(): PrimitiveChunkManager { + return this.engine._batcherManager.primitiveChunkManager2D; + } - // Update color - if (this._dirtyUpdateFlag & SpriteRendererUpdateFlags.Color) { - this._assembler.updateColor(this, 1); - this._dirtyUpdateFlag &= ~SpriteRendererUpdateFlags.Color; - } + /** @internal */ + override _getDefaultSpriteMaterial(): Material { + return this._engine._basicResources.spriteDefaultMaterial; + } - // Push primitive + /** @internal */ + override _submitSpriteRenderElement( + context: RenderContext, + material: Material, + subChunk: SubPrimitiveChunk, + texture: Texture2D + ): void { const camera = context.camera; const engine = camera.engine; const renderElement = engine._renderElementPool.get(); renderElement.set(this.priority, this._distanceForSort); const subRenderElement = engine._subRenderElementPool.get(); - const subChunk = this._subChunk; - subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, this.sprite.texture, subChunk); + subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, texture, subChunk); renderElement.addSubRenderElement(subRenderElement); camera._renderPipeline.pushRenderElement(context, renderElement); } - protected override _onDestroy(): void { - const sprite = this._sprite; - if (sprite) { - this._addResourceReferCount(sprite, -1); - sprite._updateFlagManager.removeListener(this._onSpriteChange); - } - - super._onDestroy(); - - this._sprite = null; - this._assembler = null; - if (this._subChunk) { - this._getChunkManager().freeSubChunk(this._subChunk); - this._subChunk = null; - } - } - - private _calDefaultSize(): void { - const sprite = this._sprite; - if (sprite) { - this._automaticWidth = sprite.width; - this._automaticHeight = sprite.height; - } else { - this._automaticWidth = this._automaticHeight = 0; - } - this._dirtyUpdateFlag &= ~SpriteRendererUpdateFlags.AutomaticSize; - } - - @ignoreClone - private _onSpriteChange(type: SpriteModifyFlags): void { - switch (type) { - case SpriteModifyFlags.texture: - this.shaderData.setTexture(SpriteRenderer._textureProperty, this.sprite.texture); - break; - case SpriteModifyFlags.size: - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.AutomaticSize; - if (this._customWidth === undefined || this._customHeight === undefined) { - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; - } - switch (this._drawMode) { - case SpriteDrawMode.Simple: - // When the width and height of `SpriteRenderer` are `undefined`, - // the `size` of `Sprite` will affect the position of `SpriteRenderer`. - if (this._customWidth === undefined || this._customHeight === undefined) { - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; - } - break; - case SpriteDrawMode.Sliced: - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; - break; - case SpriteDrawMode.Tiled: - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeUVAndColor; - break; - } - break; - case SpriteModifyFlags.border: - switch (this._drawMode) { - case SpriteDrawMode.Sliced: - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; - break; - case SpriteDrawMode.Tiled: - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeUVAndColor; - break; - default: - break; - } - break; - case SpriteModifyFlags.region: - case SpriteModifyFlags.atlasRegionOffset: - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; - break; - case SpriteModifyFlags.atlasRegion: - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.UV; - break; - case SpriteModifyFlags.pivot: - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; - break; - case SpriteModifyFlags.destroy: - this.sprite = null; - break; - } - } + // ===== Private ===== @ignoreClone private _onColorChanged(): void { - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.Color; + this._dirtyUpdateFlag |= SpriteRenderableFlags.Color; } -} - -/** - * @remarks Extends `RendererUpdateFlags`. - */ -enum SpriteRendererUpdateFlags { - /** UV. */ - UV = 0x2, - /** Color. */ - Color = 0x4, - /** Automatic Size. */ - AutomaticSize = 0x8, - - /** WorldVolume and UV. */ - WorldVolumeAndUV = 0x3, - /** WorldVolume, UV and Color. */ - WorldVolumeUVAndColor = 0x7, - /** All. */ - All = 0xf -} +} \ No newline at end of file diff --git a/packages/core/src/2d/sprite/index.ts b/packages/core/src/2d/sprite/index.ts index 162d016472..c3265f07f1 100644 --- a/packages/core/src/2d/sprite/index.ts +++ b/packages/core/src/2d/sprite/index.ts @@ -1,3 +1,8 @@ export { Sprite } from "./Sprite"; +export { SpritePrimitive } from "./SpritePrimitive"; +export type { ISpritePrimitiveOwner } from "./SpritePrimitive"; +export type { ISpriteRenderable } from "./SpriteRenderable"; +export { SpriteRenderable, SpriteRenderableFlags } from "./SpriteRenderable"; export { SpriteMask } from "./SpriteMask"; +export { SpriteMaskUtils } from "./SpriteMaskUtils"; export { SpriteRenderer } from "./SpriteRenderer"; diff --git a/packages/core/src/2d/text/TextRenderable.ts b/packages/core/src/2d/text/TextRenderable.ts new file mode 100644 index 0000000000..bf95e99e0b --- /dev/null +++ b/packages/core/src/2d/text/TextRenderable.ts @@ -0,0 +1,714 @@ +import { BoundingBox, Color, Vector3 } from "@galacean/engine-math"; +import { Engine } from "../../Engine"; +import { BatchUtils } from "../../RenderPipeline/BatchUtils"; +import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; +import { RenderContext } from "../../RenderPipeline/RenderContext"; +import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; +import { SubRenderElement } from "../../RenderPipeline/SubRenderElement"; +import { Renderer, RendererUpdateFlags } from "../../Renderer"; +import { assignmentClone, ignoreClone } from "../../clone/CloneManager"; +import { Material } from "../../material"; +import { ShaderProperty } from "../../shader"; +import { Texture2D } from "../../texture"; +import { FontStyle } from "../enums/FontStyle"; +import { TextHorizontalAlignment, TextVerticalAlignment } from "../enums/TextAlignment"; +import { OverflowMode } from "../enums/TextOverflow"; +import { CharRenderInfo } from "./CharRenderInfo"; +import { Font } from "./Font"; +import { ITextRenderer } from "./ITextRenderer"; +import { SubFont } from "./SubFont"; +import { TextUtils } from "./TextUtils"; + +/** + * @remarks Extends `RendererUpdateFlags`. + */ +export enum TextRenderableFlags { + /** Color. */ + Color = 0x2, + /** SubFont needs reset. */ + SubFont = 0x4, + /** Local positions and bounds need recalculation. */ + LocalPositionBounds = 0x8, + /** World positions need update. */ + WorldPosition = 0x10, + + /** Position = WorldVolume | LocalPositionBounds | WorldPosition. */ + Position = 0x19, + /** Font = SubFont | Position. */ + Font = 0x1d, + /** All. */ + All = 0x1f +} + +type RendererConstructor = abstract new (...args: any[]) => Renderer; + +/** + * Public contract of the TextRenderable mixin. + */ +export interface ITextRenderable { + text: string; + font: Font; + fontSize: number; + fontStyle: FontStyle; + lineSpacing: number; + characterSpacing: number; + horizontalAlignment: TextHorizontalAlignment; + verticalAlignment: TextVerticalAlignment; + enableWrapping: boolean; + overflowMode: OverflowMode; + _subFont: SubFont; + _getChunkManager(): PrimitiveChunkManager; + _getSubFont(): SubFont; + _getWidth(): number; + _getHeight(): number; + _getPivotX(): number; + _getPivotY(): number; + _getReferenceResolutionPerUnit(): number | undefined; + _getAlpha(): number; + _submitText(context: RenderContext, material: Material): void; + _isTextHostInvisible(): boolean; + _isContainDirtyFlag(type: number): boolean; + _setDirtyFlagTrue(type: number): void; + _setDirtyFlagFalse(type: number): void; + _getTextChunks(): TextChunk[]; + _getTextTextureProperty(): ShaderProperty; + _initTextRenderable(): void; +} + +/** + * Wiring mixin that provides shared text rendering logic for both 2D TextRenderer and UI Text. + */ +export function TextRenderable( + Base: T +): (abstract new (...args: any[]) => ITextRenderable) & T { + abstract class TextRenderableHost extends Base implements ITextRenderer { + private static _textureProperty = ShaderProperty.getByName("renderElement_TextTexture"); + private static _tempVec30 = new Vector3(); + private static _tempVec31 = new Vector3(); + private static _worldPositions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; + private static _charRenderInfos: CharRenderInfo[] = []; + + @ignoreClone + private _textChunks = Array(); + /** @internal */ + @assignmentClone + _subFont: SubFont = null; + @ignoreClone + private _localBounds = new BoundingBox(); + @assignmentClone + private _text = ""; + @assignmentClone + private _font: Font = null; + @assignmentClone + private _fontSize = 24; + @assignmentClone + private _fontStyle = FontStyle.None; + @assignmentClone + private _lineSpacing = 0; + @assignmentClone + private _characterSpacing = 0; + @assignmentClone + private _horizontalAlignment = TextHorizontalAlignment.Center; + @assignmentClone + private _verticalAlignment = TextVerticalAlignment.Center; + @assignmentClone + private _enableWrapping = false; + @assignmentClone + private _overflowMode = OverflowMode.Overflow; + + // ===== Abstract methods ===== + + abstract get color(): Color; + abstract _getChunkManager(): PrimitiveChunkManager; + abstract _submitText(context: RenderContext, material: Material): void; + + /** The text layout width. */ + abstract _getWidth(): number; + + /** The text layout height. */ + abstract _getHeight(): number; + + // ===== Abstract methods: host MUST implement (avoids MRO shadowing) ===== + + /** Final alpha multiplier. 2D hosts return 1; UI hosts return globalAlpha. */ + abstract _getAlpha(): number; + + /** Text pivot X. 2D hosts return 0.5; UI hosts return UITransform pivot. */ + abstract _getPivotX(): number; + + /** Text pivot Y. 2D hosts return 0.5; UI hosts return UITransform pivot. */ + abstract _getPivotY(): number; + + /** Reference resolution per unit. 2D hosts return undefined; UI hosts return canvas value. */ + abstract _getReferenceResolutionPerUnit(): number | undefined; + + // ===== Methods with defaults ===== + + _isTextHostInvisible(): boolean { + return false; + } + + // ===== Text properties ===== + + get text(): string { + return this._text; + } + + set text(value: string) { + value = value || ""; + if (this._text !== value) { + this._text = value; + this._setDirtyFlagTrue(TextRenderableFlags.Position); + } + } + + get font(): Font { + return this._font; + } + + set font(value: Font) { + const lastFont = this._font; + if (lastFont !== value) { + lastFont && this._addResourceReferCount(lastFont, -1); + value && this._addResourceReferCount(value, 1); + this._font = value; + this._setDirtyFlagTrue(TextRenderableFlags.Font); + } + } + + get fontSize(): number { + return this._fontSize; + } + + set fontSize(value: number) { + if (this._fontSize !== value) { + this._fontSize = value; + this._setDirtyFlagTrue(TextRenderableFlags.Font); + } + } + + get fontStyle(): FontStyle { + return this._fontStyle; + } + + set fontStyle(value: FontStyle) { + if (this._fontStyle !== value) { + this._fontStyle = value; + this._setDirtyFlagTrue(TextRenderableFlags.Font); + } + } + + get lineSpacing(): number { + return this._lineSpacing; + } + + set lineSpacing(value: number) { + if (this._lineSpacing !== value) { + this._lineSpacing = value; + this._setDirtyFlagTrue(TextRenderableFlags.Position); + } + } + + get characterSpacing(): number { + return this._characterSpacing; + } + + set characterSpacing(value: number) { + if (this._characterSpacing !== value) { + this._characterSpacing = value; + this._setDirtyFlagTrue(TextRenderableFlags.Position); + } + } + + get horizontalAlignment(): TextHorizontalAlignment { + return this._horizontalAlignment; + } + + set horizontalAlignment(value: TextHorizontalAlignment) { + if (this._horizontalAlignment !== value) { + this._horizontalAlignment = value; + this._setDirtyFlagTrue(TextRenderableFlags.Position); + } + } + + get verticalAlignment(): TextVerticalAlignment { + return this._verticalAlignment; + } + + set verticalAlignment(value: TextVerticalAlignment) { + if (this._verticalAlignment !== value) { + this._verticalAlignment = value; + this._setDirtyFlagTrue(TextRenderableFlags.Position); + } + } + + get enableWrapping(): boolean { + return this._enableWrapping; + } + + set enableWrapping(value: boolean) { + if (this._enableWrapping !== value) { + this._enableWrapping = value; + this._setDirtyFlagTrue(TextRenderableFlags.Position); + } + } + + get overflowMode(): OverflowMode { + return this._overflowMode; + } + + set overflowMode(value: OverflowMode) { + if (this._overflowMode !== value) { + this._overflowMode = value; + this._setDirtyFlagTrue(TextRenderableFlags.Position); + } + } + + // ===== Bounds ===== + + override get bounds(): BoundingBox { + if (this._isTextNoVisible()) { + if (this._isContainDirtyFlag(RendererUpdateFlags.WorldVolume)) { + this._localBounds.min.set(0, 0, 0); + this._localBounds.max.set(0, 0, 0); + this._updateBounds(this._bounds); + this._setDirtyFlagFalse(RendererUpdateFlags.WorldVolume); + } + return this._bounds; + } + this._isContainDirtyFlag(TextRenderableFlags.SubFont) && this._resetSubFont(); + this._isContainDirtyFlag(TextRenderableFlags.LocalPositionBounds) && this._updateLocalData(); + this._isContainDirtyFlag(TextRenderableFlags.WorldPosition) && this._updatePosition(); + this._isContainDirtyFlag(RendererUpdateFlags.WorldVolume) && this._updateBounds(this._bounds); + this._setDirtyFlagFalse(TextRenderableFlags.Font); + + return this._bounds; + } + + // ===== Init ===== + + _initTextRenderable(): void { + this.font = this._engine._textDefaultFont; + this.setMaterial(this._engine._basicResources.textDefaultMaterial); + } + + // ===== Lifecycle ===== + + override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean, batched: boolean): void { + super._updateTransformShaderData(context, onlyMVP, true); + } + + override _canBatch(elementA: SubRenderElement, elementB: SubRenderElement): boolean { + return BatchUtils.canBatchSprite(elementA, elementB); + } + + override _batch(elementA: SubRenderElement, elementB?: SubRenderElement): void { + BatchUtils.batchFor2D(elementA, elementB); + } + + // @ts-ignore + override _cloneTo(target: TextRenderableHost): void { + // @ts-ignore + super._cloneTo(target); + target.font = this._font; + target._subFont = this._subFont; + } + + protected override _updateBounds(worldBounds: BoundingBox): void { + BoundingBox.transform(this._localBounds, this._transformEntity.transform.worldMatrix, worldBounds); + } + + protected override _render(context: RenderContext): void { + if (this._isTextNoVisible()) { + return; + } + + if (this._isContainDirtyFlag(TextRenderableFlags.SubFont)) { + this._resetSubFont(); + this._setDirtyFlagFalse(TextRenderableFlags.SubFont); + } + + if (this._isContainDirtyFlag(TextRenderableFlags.LocalPositionBounds)) { + this._updateLocalData(); + this._setDirtyFlagFalse(TextRenderableFlags.LocalPositionBounds); + } + + if (this._isContainDirtyFlag(TextRenderableFlags.WorldPosition)) { + this._updatePosition(); + this._setDirtyFlagFalse(TextRenderableFlags.WorldPosition); + } + + if (this._isContainDirtyFlag(TextRenderableFlags.Color)) { + this._updateColor(); + this._setDirtyFlagFalse(TextRenderableFlags.Color); + } + + const material = this.getMaterial(); + if (!material) { + return; + } + + this._submitText(context, material); + } + + protected override _onDestroy(): void { + if (this._font) { + this._addResourceReferCount(this._font, -1); + this._font = null; + } + + super._onDestroy(); + + this._freeTextChunks(); + this._textChunks = null; + this._subFont && (this._subFont = null); + } + + @ignoreClone + protected override _onTransformChanged(type: number): void { + super._onTransformChanged(type); + this._setDirtyFlagTrue(TextRenderableFlags.WorldPosition); + } + + // ===== Shared text methods ===== + + _isContainDirtyFlag(type: number): boolean { + return (this._dirtyUpdateFlag & type) != 0; + } + + _setDirtyFlagTrue(type: number): void { + this._dirtyUpdateFlag |= type; + } + + _setDirtyFlagFalse(type: number): void { + this._dirtyUpdateFlag &= ~type; + } + + _getSubFont(): SubFont { + if (!this._subFont) { + this._resetSubFont(); + } + return this._subFont; + } + + /** @internal Accessible by hosts for submit loop. */ + _getTextChunks(): TextChunk[] { + return this._textChunks; + } + + /** @internal Texture property for sub-render element shader data. */ + _getTextTextureProperty(): ShaderProperty { + return TextRenderableHost._textureProperty; + } + + // ===== Private ===== + + private _isTextNoVisible(): boolean { + const textWidth = this._getWidth(); + const textHeight = this._getHeight(); + return ( + !this._font || + this._text === "" || + this._fontSize === 0 || + (this._enableWrapping && textWidth <= 0) || + (this._overflowMode === OverflowMode.Truncate && textHeight <= 0) || + this._isTextHostInvisible() + ); + } + + private _resetSubFont(): void { + const font = this._font; + this._subFont = font._getSubFont(this.fontSize, this.fontStyle); + this._subFont.nativeFontString = TextUtils.getNativeFontString(font.name, this.fontSize, this.fontStyle); + } + + private _updatePosition(): void { + const e = this._transformEntity.transform.worldMatrix.elements; + + // prettier-ignore + const e0 = e[0], e1 = e[1], e2 = e[2], + e4 = e[4], e5 = e[5], e6 = e[6], + e12 = e[12], e13 = e[13], e14 = e[14]; + + const up = TextRenderableHost._tempVec31.set(e4, e5, e6); + const right = TextRenderableHost._tempVec30.set(e0, e1, e2); + + const worldPositions = TextRenderableHost._worldPositions; + const worldPosition0 = worldPositions[0]; + const worldPosition1 = worldPositions[1]; + const worldPosition2 = worldPositions[2]; + const worldPosition3 = worldPositions[3]; + + const textChunks = this._textChunks; + for (let i = 0, n = textChunks.length; i < n; ++i) { + const { subChunk, charRenderInfos } = textChunks[i]; + for (let j = 0, m = charRenderInfos.length; j < m; ++j) { + const charRenderInfo = charRenderInfos[j]; + const { localPositions } = charRenderInfo; + const { x: topLeftX, y: topLeftY } = localPositions; + + // Top-Left + worldPosition0.set( + topLeftX * e0 + topLeftY * e4 + e12, + topLeftX * e1 + topLeftY * e5 + e13, + topLeftX * e2 + topLeftY * e6 + e14 + ); + + // Right offset + Vector3.scale(right, localPositions.z - topLeftX, worldPosition1); + // Top-Right + Vector3.add(worldPosition0, worldPosition1, worldPosition1); + // Up offset + Vector3.scale(up, localPositions.w - topLeftY, worldPosition2); + // Bottom-Left + Vector3.add(worldPosition0, worldPosition2, worldPosition3); + // Bottom-Right + Vector3.add(worldPosition1, worldPosition2, worldPosition2); + + const vertices = subChunk.chunk.vertices; + for (let k = 0, o = subChunk.vertexArea.start + charRenderInfo.indexInChunk * 36; k < 4; ++k, o += 9) { + worldPositions[k].copyToArray(vertices, o); + } + } + } + } + + private _updateColor(): void { + const { r, g, b, a } = this.color; + const finalAlpha = a * this._getAlpha(); + const textChunks = this._textChunks; + for (let i = 0, n = textChunks.length; i < n; ++i) { + const subChunk = textChunks[i].subChunk; + const vertexArea = subChunk.vertexArea; + const vertexCount = vertexArea.size / 9; + const vertices = subChunk.chunk.vertices; + for (let j = 0, o = vertexArea.start + 5; j < vertexCount; ++j, o += 9) { + vertices[o] = r; + vertices[o + 1] = g; + vertices[o + 2] = b; + vertices[o + 3] = finalAlpha; + } + } + } + + private _updateLocalData(): void { + let rendererWidth = this._getWidth(); + let rendererHeight = this._getHeight(); + const pivotX = this._getPivotX(); + const pivotY = this._getPivotY(); + const resPerUnit = this._getReferenceResolutionPerUnit(); + const pixelsPerUnit = resPerUnit ? Engine._pixelsPerUnit / resPerUnit : Engine._pixelsPerUnit; + const offsetWidth = rendererWidth * (0.5 - pivotX); + const offsetHeight = rendererHeight * (0.5 - pivotY); + + const { min, max } = this._localBounds; + const charRenderInfos = TextRenderableHost._charRenderInfos; + const charFont = this._getSubFont(); + const characterSpacing = this._characterSpacing * this._fontSize; + const textMetrics = this._enableWrapping + ? TextUtils.measureTextWithWrap( + this, + rendererWidth * pixelsPerUnit, + rendererHeight * pixelsPerUnit, + this._lineSpacing * this._fontSize, + characterSpacing + ) + : TextUtils.measureTextWithoutWrap( + this, + rendererHeight * pixelsPerUnit, + this._lineSpacing * this._fontSize, + characterSpacing + ); + const { height, lines, lineWidths, lineHeight, lineMaxSizes } = textMetrics; + const charRenderInfoPool = this.engine._charRenderInfoPool; + const linesLen = lines.length; + let renderElementCount = 0; + + if (linesLen > 0) { + const { horizontalAlignment } = this; + const pixelsPerUnitReciprocal = 1.0 / pixelsPerUnit; + rendererWidth *= pixelsPerUnit; + rendererHeight *= pixelsPerUnit; + const halfRendererWidth = rendererWidth * 0.5; + const halfLineHeight = lineHeight * 0.5; + + let startY = 0; + const topDiff = lineHeight * 0.5 - lineMaxSizes[0].ascent; + const bottomDiff = lineHeight * 0.5 - lineMaxSizes[linesLen - 1].descent - 1; + switch (this.verticalAlignment) { + case TextVerticalAlignment.Top: + startY = rendererHeight * 0.5 - halfLineHeight + topDiff; + break; + case TextVerticalAlignment.Center: + startY = height * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5; + break; + case TextVerticalAlignment.Bottom: + startY = height - rendererHeight * 0.5 - halfLineHeight - bottomDiff; + break; + } + + let firstLine = -1; + let minX = Number.MAX_SAFE_INTEGER; + let minY = Number.MAX_SAFE_INTEGER; + let maxX = Number.MIN_SAFE_INTEGER; + let maxY = Number.MIN_SAFE_INTEGER; + for (let i = 0; i < linesLen; ++i) { + const lineWidth = lineWidths[i]; + if (lineWidth > 0) { + const line = lines[i]; + let startX = 0; + let firstRow = -1; + if (firstLine < 0) { + firstLine = i; + } + switch (horizontalAlignment) { + case TextHorizontalAlignment.Left: + startX = -halfRendererWidth; + break; + case TextHorizontalAlignment.Center: + startX = -lineWidth * 0.5; + break; + case TextHorizontalAlignment.Right: + startX = halfRendererWidth - lineWidth; + break; + } + for (let j = 0, n = line.length; j < n; ++j) { + const char = line[j]; + const charInfo = charFont._getCharInfo(char); + if (charInfo.h > 0) { + firstRow < 0 && (firstRow = j); + const charRenderInfo = (charRenderInfos[renderElementCount++] = charRenderInfoPool.get()); + const { localPositions } = charRenderInfo; + charRenderInfo.texture = charFont._getTextureByIndex(charInfo.index); + charRenderInfo.uvs = charInfo.uvs; + const { w, ascent, descent } = charInfo; + const left = (startX + offsetWidth) * pixelsPerUnitReciprocal; + const right = (startX + w + offsetWidth) * pixelsPerUnitReciprocal; + const top = (startY + ascent + offsetHeight) * pixelsPerUnitReciprocal; + const bottom = (startY - descent + offsetHeight) * pixelsPerUnitReciprocal; + localPositions.set(left, top, right, bottom); + i === firstLine && (maxY = Math.max(maxY, top)); + minY = Math.min(minY, bottom); + j === firstRow && (minX = Math.min(minX, left)); + maxX = Math.max(maxX, right); + } + startX += charInfo.xAdvance + characterSpacing; + } + } + startY -= lineHeight; + } + if (firstLine < 0) { + min.set(0, 0, 0); + max.set(0, 0, 0); + } else { + min.set(minX, minY, 0); + max.set(maxX, maxY, 0); + } + } else { + min.set(0, 0, 0); + max.set(0, 0, 0); + } + + charFont._getLastIndex() > 0 && + charRenderInfos.sort((a, b) => { + return a.texture.instanceId - b.texture.instanceId; + }); + + this._freeTextChunks(); + + if (renderElementCount === 0) { + return; + } + + const textChunks = this._textChunks; + let curTextChunk = new TextChunk(); + textChunks.push(curTextChunk); + + const chunkMaxVertexCount = this._getChunkManager().maxVertexCount; + const curCharRenderInfo = charRenderInfos[0]; + let curTexture = curCharRenderInfo.texture; + curTextChunk.texture = curTexture; + let curCharInfos = curTextChunk.charRenderInfos; + curCharInfos.push(curCharRenderInfo); + + for (let i = 1; i < renderElementCount; ++i) { + const charRenderInfo = charRenderInfos[i]; + const texture = charRenderInfo.texture; + if (curTexture !== texture || curCharInfos.length * 4 + 4 > chunkMaxVertexCount) { + this._buildChunk(curTextChunk, curCharInfos.length); + + curTextChunk = new TextChunk(); + textChunks.push(curTextChunk); + curTexture = texture; + curTextChunk.texture = texture; + curCharInfos = curTextChunk.charRenderInfos; + } + curCharInfos.push(charRenderInfo); + } + const charLength = curCharInfos.length; + if (charLength > 0) { + this._buildChunk(curTextChunk, charLength); + } + charRenderInfos.length = 0; + } + + private _buildChunk(textChunk: TextChunk, count: number): SubPrimitiveChunk { + const { r, g, b, a } = this.color; + const finalAlpha = a * this._getAlpha(); + const tempIndices = CharRenderInfo.triangles; + const tempIndicesLength = tempIndices.length; + const subChunk = (textChunk.subChunk = this._getChunkManager().allocateSubChunk(count * 4)); + const vertices = subChunk.chunk.vertices; + const indices = (subChunk.indices = []); + const charRenderInfos = textChunk.charRenderInfos; + for (let i = 0, ii = 0, io = 0, vo = subChunk.vertexArea.start + 3; i < count; ++i, io += 4) { + const charRenderInfo = charRenderInfos[i]; + charRenderInfo.indexInChunk = i; + + // Set indices + for (let j = 0; j < tempIndicesLength; ++j) { + indices[ii++] = tempIndices[j] + io; + } + + // Set uv and color for vertices + for (let j = 0; j < 4; ++j, vo += 9) { + const uv = charRenderInfo.uvs[j]; + uv.copyToArray(vertices, vo); + vertices[vo + 2] = r; + vertices[vo + 3] = g; + vertices[vo + 4] = b; + vertices[vo + 5] = finalAlpha; + } + } + + return subChunk; + } + + private _freeTextChunks(): void { + const textChunks = this._textChunks; + const charRenderInfoPool = this.engine._charRenderInfoPool; + const manager = this._getChunkManager(); + for (let i = 0, n = textChunks.length; i < n; ++i) { + const textChunk = textChunks[i]; + const { charRenderInfos } = textChunk; + for (let j = 0, m = charRenderInfos.length; j < m; ++j) { + charRenderInfoPool.return(charRenderInfos[j]); + } + charRenderInfos.length = 0; + manager.freeSubChunk(textChunk.subChunk); + textChunk.subChunk = null; + textChunk.texture = null; + } + textChunks.length = 0; + } + } + + return TextRenderableHost as unknown as (abstract new (...args: any[]) => ITextRenderable) & T; +} + +/** @internal */ +export class TextChunk { + charRenderInfos = new Array(); + subChunk: SubPrimitiveChunk; + texture: Texture2D; +} diff --git a/packages/core/src/2d/text/TextRenderer.ts b/packages/core/src/2d/text/TextRenderer.ts index 143e46a7da..d918b4c9d7 100644 --- a/packages/core/src/2d/text/TextRenderer.ts +++ b/packages/core/src/2d/text/TextRenderer.ts @@ -1,74 +1,32 @@ -import { BoundingBox, Color, Vector3 } from "@galacean/engine-math"; -import { Engine } from "../../Engine"; +import { Color } from "@galacean/engine-math"; import { Entity } from "../../Entity"; -import { BatchUtils } from "../../RenderPipeline/BatchUtils"; import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; import { RenderContext } from "../../RenderPipeline/RenderContext"; -import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; -import { SubRenderElement } from "../../RenderPipeline/SubRenderElement"; import { Renderer } from "../../Renderer"; -import { TransformModifyFlags } from "../../Transform"; import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; -import { ShaderData, ShaderProperty } from "../../shader"; +import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; +import { ShaderData } from "../../shader"; import { ShaderDataGroup } from "../../shader/enums/ShaderDataGroup"; -import { Texture2D } from "../../texture"; -import { FontStyle } from "../enums/FontStyle"; import { SpriteMaskInteraction } from "../enums/SpriteMaskInteraction"; -import { TextHorizontalAlignment, TextVerticalAlignment } from "../enums/TextAlignment"; -import { OverflowMode } from "../enums/TextOverflow"; -import { CharRenderInfo } from "./CharRenderInfo"; -import { Font } from "./Font"; -import { ITextRenderer } from "./ITextRenderer"; -import { SubFont } from "./SubFont"; -import { TextUtils } from "./TextUtils"; -import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; +import { Material } from "../../material"; +import { TextChunk, TextRenderable, TextRenderableFlags } from "./TextRenderable"; /** * Renders a text for 2D graphics. */ -export class TextRenderer extends Renderer implements ITextRenderer { - private static _textureProperty = ShaderProperty.getByName("renderElement_TextTexture"); - private static _tempVec30 = new Vector3(); - private static _tempVec31 = new Vector3(); - private static _worldPositions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; - private static _charRenderInfos: CharRenderInfo[] = []; - - @ignoreClone - private _textChunks = Array(); +export class TextRenderer extends TextRenderable(Renderer) { /** @internal */ @assignmentClone - _subFont: SubFont = null; + _maskInteraction: SpriteMaskInteraction = SpriteMaskInteraction.None; /** @internal */ - @ignoreClone - _dirtyFlag = DirtyFlag.Font; + @assignmentClone + _maskLayer: SpriteMaskLayer = SpriteMaskLayer.Layer0; + @deepClone private _color = new Color(1, 1, 1, 1); - @assignmentClone - private _text = ""; - @assignmentClone - private _width = 0; - @assignmentClone - private _height = 0; - @ignoreClone - private _localBounds = new BoundingBox(); - @assignmentClone - private _font: Font = null; - @assignmentClone - private _fontSize = 24; - @assignmentClone - private _fontStyle = FontStyle.None; - @assignmentClone - private _lineSpacing = 0; - @assignmentClone - private _characterSpacing = 0; - @assignmentClone - private _horizontalAlignment = TextHorizontalAlignment.Center; - @assignmentClone - private _verticalAlignment = TextVerticalAlignment.Center; - @assignmentClone - private _enableWrapping = false; - @assignmentClone - private _overflowMode = OverflowMode.Overflow; + + private _width: number = 0; + private _height: number = 0; /** * Rendering color for the Text. @@ -83,21 +41,6 @@ export class TextRenderer extends Renderer implements ITextRenderer { } } - /** - * Rendering string for the Text. - */ - get text(): string { - return this._text; - } - - set text(value: string) { - value = value || ""; - if (this._text !== value) { - this._text = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - /** * The width of the TextRenderer (in 3D world coordinates). */ @@ -108,7 +51,7 @@ export class TextRenderer extends Renderer implements ITextRenderer { set width(value: number) { if (this._width !== value) { this._width = value; - this._setDirtyFlagTrue(DirtyFlag.Position); + this._setDirtyFlagTrue(TextRenderableFlags.Position); } } @@ -122,137 +65,19 @@ export class TextRenderer extends Renderer implements ITextRenderer { set height(value: number) { if (this._height !== value) { this._height = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * The font of the Text. - */ - get font(): Font { - return this._font; - } - - set font(value: Font) { - const lastFont = this._font; - if (lastFont !== value) { - lastFont && this._addResourceReferCount(lastFont, -1); - value && this._addResourceReferCount(value, 1); - this._font = value; - this._setDirtyFlagTrue(DirtyFlag.Font); - } - } - - /** - * The font size of the Text. - */ - get fontSize(): number { - return this._fontSize; - } - - set fontSize(value: number) { - if (this._fontSize !== value) { - this._fontSize = value; - this._setDirtyFlagTrue(DirtyFlag.Font); - } - } - - /** - * The style of the font. - */ - get fontStyle(): FontStyle { - return this._fontStyle; - } - - set fontStyle(value: FontStyle) { - if (this.fontStyle !== value) { - this._fontStyle = value; - this._setDirtyFlagTrue(DirtyFlag.Font); - } - } - - /** - * The space between two lines, in em (ratio of fontSize). - */ - get lineSpacing(): number { - return this._lineSpacing; - } - - set lineSpacing(value: number) { - if (this._lineSpacing !== value) { - this._lineSpacing = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * The space between two characters, in em (ratio of fontSize). - */ - get characterSpacing(): number { - return this._characterSpacing; - } - - set characterSpacing(value: number) { - if (this._characterSpacing !== value) { - this._characterSpacing = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * The horizontal alignment. - */ - get horizontalAlignment(): TextHorizontalAlignment { - return this._horizontalAlignment; - } - - set horizontalAlignment(value: TextHorizontalAlignment) { - if (this._horizontalAlignment !== value) { - this._horizontalAlignment = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * The vertical alignment. - */ - get verticalAlignment(): TextVerticalAlignment { - return this._verticalAlignment; - } - - set verticalAlignment(value: TextVerticalAlignment) { - if (this._verticalAlignment !== value) { - this._verticalAlignment = value; - this._setDirtyFlagTrue(DirtyFlag.Position); + this._setDirtyFlagTrue(TextRenderableFlags.Position); } } /** - * Whether wrap text to next line when exceeds the width of the container. + * The mask layer the text renderer belongs to. */ - get enableWrapping(): boolean { - return this._enableWrapping; - } - - set enableWrapping(value: boolean) { - if (this._enableWrapping !== value) { - this._enableWrapping = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * The overflow mode. - */ - get overflowMode(): OverflowMode { - return this._overflowMode; + get maskLayer(): SpriteMaskLayer { + return this._maskLayer; } - set overflowMode(value: OverflowMode) { - if (this._overflowMode !== value) { - this._overflowMode = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } + set maskLayer(value: SpriteMaskLayer) { + this._maskLayer = value; } /** @@ -268,496 +93,67 @@ export class TextRenderer extends Renderer implements ITextRenderer { } } - /** - * The mask layer the sprite renderer belongs to. - */ - get maskLayer(): SpriteMaskLayer { - return this._maskLayer; - } - - set maskLayer(value: SpriteMaskLayer) { - this._maskLayer = value; - } - - /** - * The bounding volume of the TextRenderer. - */ - override get bounds(): BoundingBox { - if (this._isTextNoVisible()) { - if (this._isContainDirtyFlag(DirtyFlag.WorldBounds)) { - const localBounds = this._localBounds; - localBounds.min.set(0, 0, 0); - localBounds.max.set(0, 0, 0); - this._updateBounds(this._bounds); - this._setDirtyFlagFalse(DirtyFlag.WorldBounds); - } - return this._bounds; - } - this._isContainDirtyFlag(DirtyFlag.SubFont) && this._resetSubFont(); - this._isContainDirtyFlag(DirtyFlag.LocalPositionBounds) && this._updateLocalData(); - this._isContainDirtyFlag(DirtyFlag.WorldPosition) && this._updatePosition(); - this._isContainDirtyFlag(DirtyFlag.WorldBounds) && this._updateBounds(this._bounds); - this._setDirtyFlagFalse(DirtyFlag.Font); - - return this._bounds; - } - constructor(entity: Entity) { super(entity); - - const { engine } = this; - this._font = engine._textDefaultFont; - this._addResourceReferCount(this._font, 1); - this.setMaterial(engine._basicResources.textDefaultMaterial); + this._initTextRenderable(); + this._dirtyUpdateFlag |= TextRenderableFlags.Font; //@ts-ignore this._color._onValueChanged = this._onColorChanged.bind(this); } - /** - * @internal - */ - protected override _onDestroy(): void { - if (this._font) { - this._addResourceReferCount(this._font, -1); - this._font = null; - } - - super._onDestroy(); - - this._freeTextChunks(); - this._textChunks = null; - - this._subFont && (this._subFont = null); - } - - /** - * @internal - */ - override _cloneTo(target: TextRenderer): void { - super._cloneTo(target); - target.font = this._font; - target._subFont = this._subFont; - } - - /** - * @internal - */ - _isContainDirtyFlag(type: number): boolean { - return (this._dirtyFlag & type) != 0; - } + // ===== Abstract implementations ===== - /** - * @internal - */ - _setDirtyFlagTrue(type: number): void { - this._dirtyFlag |= type; - } - - /** - * @internal - */ - _setDirtyFlagFalse(type: number): void { - this._dirtyFlag &= ~type; + override _getChunkManager(): PrimitiveChunkManager { + return this.engine._batcherManager.primitiveChunkManager2D; } - /** - * @internal - */ - _getSubFont(): SubFont { - if (!this._subFont) { - this._resetSubFont(); - } - return this._subFont; + override _getWidth(): number { + return this._width; } - /** - * @internal - */ - override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean, batched: boolean): void { - //@todo: Always update world positions to buffer, should opt - super._updateTransformShaderData(context, onlyMVP, true); + override _getHeight(): number { + return this._height; } - /** - * @internal - */ - override _canBatch(elementA: SubRenderElement, elementB: SubRenderElement): boolean { - return BatchUtils.canBatchSprite(elementA, elementB); + override _getAlpha(): number { + return 1; } - /** - * @internal - */ - override _batch(elementA: SubRenderElement, elementB?: SubRenderElement): void { - BatchUtils.batchFor2D(elementA, elementB); + override _getPivotX(): number { + return 0.5; } - /** - * @internal - */ - _getChunkManager(): PrimitiveChunkManager { - return this.engine._batcherManager.primitiveChunkManager2D; + override _getPivotY(): number { + return 0.5; } - protected override _updateBounds(worldBounds: BoundingBox): void { - BoundingBox.transform(this._localBounds, this._entity.transform.worldMatrix, worldBounds); + override _getReferenceResolutionPerUnit(): number | undefined { + return undefined; } - protected override _render(context: RenderContext): void { - if (this._isTextNoVisible()) { - return; - } - - if (this._isContainDirtyFlag(DirtyFlag.SubFont)) { - this._resetSubFont(); - this._setDirtyFlagFalse(DirtyFlag.SubFont); - } - - if (this._isContainDirtyFlag(DirtyFlag.LocalPositionBounds)) { - this._updateLocalData(); - this._setDirtyFlagFalse(DirtyFlag.LocalPositionBounds); - } - - if (this._isContainDirtyFlag(DirtyFlag.WorldPosition)) { - this._updatePosition(); - this._setDirtyFlagFalse(DirtyFlag.WorldPosition); - } - - if (this._isContainDirtyFlag(DirtyFlag.Color)) { - this._updateColor(); - this._setDirtyFlagFalse(DirtyFlag.Color); - } - + override _submitText(context: RenderContext, material: Material): void { const camera = context.camera; const engine = camera.engine; const textSubRenderElementPool = engine._textSubRenderElementPool; - const material = this.getMaterial(); const renderElement = engine._renderElementPool.get(); renderElement.set(this.priority, this._distanceForSort); - const textChunks = this._textChunks; + const textChunks = this._getTextChunks(); + const textureProperty = this._getTextTextureProperty(); for (let i = 0, n = textChunks.length; i < n; ++i) { const { subChunk, texture } = textChunks[i]; const subRenderElement = textSubRenderElementPool.get(); subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, texture, subChunk); subRenderElement.shaderData ||= new ShaderData(ShaderDataGroup.RenderElement); - subRenderElement.shaderData.setTexture(TextRenderer._textureProperty, texture); + subRenderElement.shaderData.setTexture(textureProperty, texture); renderElement.addSubRenderElement(subRenderElement); } camera._renderPipeline.pushRenderElement(context, renderElement); } - private _resetSubFont(): void { - const font = this._font; - this._subFont = font._getSubFont(this.fontSize, this.fontStyle); - this._subFont.nativeFontString = TextUtils.getNativeFontString(font.name, this.fontSize, this.fontStyle); - } - - private _updatePosition(): void { - const { transform } = this.entity; - const e = transform.worldMatrix.elements; - - // prettier-ignore - const e0 = e[0], e1 = e[1], e2 = e[2], - e4 = e[4], e5 = e[5], e6 = e[6], - e12 = e[12], e13 = e[13], e14 = e[14]; - - const up = TextRenderer._tempVec31.set(e4, e5, e6); - const right = TextRenderer._tempVec30.set(e0, e1, e2); - - const worldPositions = TextRenderer._worldPositions; - const worldPosition0 = worldPositions[0]; - const worldPosition1 = worldPositions[1]; - const worldPosition2 = worldPositions[2]; - const worldPosition3 = worldPositions[3]; - - const textChunks = this._textChunks; - for (let i = 0, n = textChunks.length; i < n; ++i) { - const { subChunk, charRenderInfos } = textChunks[i]; - for (let j = 0, m = charRenderInfos.length; j < m; ++j) { - const charRenderInfo = charRenderInfos[j]; - const { localPositions } = charRenderInfo; - const { x: topLeftX, y: topLeftY } = localPositions; - - // Top-Left - worldPosition0.set( - topLeftX * e0 + topLeftY * e4 + e12, - topLeftX * e1 + topLeftY * e5 + e13, - topLeftX * e2 + topLeftY * e6 + e14 - ); - - // Right offset - Vector3.scale(right, localPositions.z - topLeftX, worldPosition1); - // Top-Right - Vector3.add(worldPosition0, worldPosition1, worldPosition1); - // Up offset - Vector3.scale(up, localPositions.w - topLeftY, worldPosition2); - // Bottom-Left - Vector3.add(worldPosition0, worldPosition2, worldPosition3); - // Bottom-Right - Vector3.add(worldPosition1, worldPosition2, worldPosition2); - - const vertices = subChunk.chunk.vertices; - for (let k = 0, o = subChunk.vertexArea.start + charRenderInfo.indexInChunk * 36; k < 4; ++k, o += 9) { - worldPositions[k].copyToArray(vertices, o); - } - } - } - } - - private _updateColor(): void { - const { r, g, b, a } = this._color; - const textChunks = this._textChunks; - for (let i = 0, n = textChunks.length; i < n; ++i) { - const subChunk = textChunks[i].subChunk; - const vertexArea = subChunk.vertexArea; - const vertexCount = vertexArea.size / 9; - const vertices = subChunk.chunk.vertices; - for (let j = 0, o = vertexArea.start + 5; j < vertexCount; ++j, o += 9) { - vertices[o] = r; - vertices[o + 1] = g; - vertices[o + 2] = b; - vertices[o + 3] = a; - } - } - } - - private _updateLocalData(): void { - const { _pixelsPerUnit } = Engine; - const { min, max } = this._localBounds; - const charRenderInfos = TextRenderer._charRenderInfos; - const charFont = this._getSubFont(); - const characterSpacing = this._characterSpacing * this._fontSize; - const textMetrics = this.enableWrapping - ? TextUtils.measureTextWithWrap( - this, - this.width * _pixelsPerUnit, - this.height * _pixelsPerUnit, - this._lineSpacing * this._fontSize, - characterSpacing - ) - : TextUtils.measureTextWithoutWrap( - this, - this.height * _pixelsPerUnit, - this._lineSpacing * this._fontSize, - characterSpacing - ); - const { height, lines, lineWidths, lineHeight, lineMaxSizes } = textMetrics; - const charRenderInfoPool = this.engine._charRenderInfoPool; - const linesLen = lines.length; - let renderElementCount = 0; - - if (linesLen > 0) { - const { horizontalAlignment } = this; - const pixelsPerUnitReciprocal = 1.0 / _pixelsPerUnit; - const rendererWidth = this._width * _pixelsPerUnit; - const halfRendererWidth = rendererWidth * 0.5; - const rendererHeight = this._height * _pixelsPerUnit; - const halfLineHeight = lineHeight * 0.5; - - let startY = 0; - const topDiff = lineHeight * 0.5 - lineMaxSizes[0].ascent; - const bottomDiff = lineHeight * 0.5 - lineMaxSizes[linesLen - 1].descent - 1; - switch (this.verticalAlignment) { - case TextVerticalAlignment.Top: - startY = rendererHeight * 0.5 - halfLineHeight + topDiff; - break; - case TextVerticalAlignment.Center: - startY = height * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5; - break; - case TextVerticalAlignment.Bottom: - startY = height - rendererHeight * 0.5 - halfLineHeight - bottomDiff; - break; - } - - let firstLine = -1; - let minX = Number.MAX_SAFE_INTEGER; - let minY = Number.MAX_SAFE_INTEGER; - let maxX = Number.MIN_SAFE_INTEGER; - let maxY = Number.MIN_SAFE_INTEGER; - for (let i = 0; i < linesLen; ++i) { - const lineWidth = lineWidths[i]; - if (lineWidth > 0) { - const line = lines[i]; - let startX = 0; - let firstRow = -1; - if (firstLine < 0) { - firstLine = i; - } - switch (horizontalAlignment) { - case TextHorizontalAlignment.Left: - startX = -halfRendererWidth; - break; - case TextHorizontalAlignment.Center: - startX = -lineWidth * 0.5; - break; - case TextHorizontalAlignment.Right: - startX = halfRendererWidth - lineWidth; - break; - } - for (let j = 0, n = line.length; j < n; ++j) { - const char = line[j]; - const charInfo = charFont._getCharInfo(char); - if (charInfo.h > 0) { - firstRow < 0 && (firstRow = j); - const charRenderInfo = (charRenderInfos[renderElementCount++] = charRenderInfoPool.get()); - const { localPositions } = charRenderInfo; - charRenderInfo.texture = charFont._getTextureByIndex(charInfo.index); - charRenderInfo.uvs = charInfo.uvs; - const { w, ascent, descent } = charInfo; - const left = startX * pixelsPerUnitReciprocal; - const right = (startX + w) * pixelsPerUnitReciprocal; - const top = (startY + ascent) * pixelsPerUnitReciprocal; - const bottom = (startY - descent) * pixelsPerUnitReciprocal; - localPositions.set(left, top, right, bottom); - i === firstLine && (maxY = Math.max(maxY, top)); - minY = Math.min(minY, bottom); - j === firstRow && (minX = Math.min(minX, left)); - maxX = Math.max(maxX, right); - } - startX += charInfo.xAdvance + characterSpacing; - } - } - startY -= lineHeight; - } - if (firstLine < 0) { - min.set(0, 0, 0); - max.set(0, 0, 0); - } else { - min.set(minX, minY, 0); - max.set(maxX, maxY, 0); - } - } else { - min.set(0, 0, 0); - max.set(0, 0, 0); - } - - charFont._getLastIndex() > 0 && - charRenderInfos.sort((a, b) => { - return a.texture.instanceId - b.texture.instanceId; - }); - - this._freeTextChunks(); - - if (renderElementCount === 0) { - return; - } - - const textChunks = this._textChunks; - let curTextChunk = new TextChunk(); - textChunks.push(curTextChunk); - - const chunkMaxVertexCount = this._getChunkManager().maxVertexCount; - const curCharRenderInfo = charRenderInfos[0]; - let curTexture = curCharRenderInfo.texture; - curTextChunk.texture = curTexture; - let curCharInfos = curTextChunk.charRenderInfos; - curCharInfos.push(curCharRenderInfo); - - for (let i = 1; i < renderElementCount; ++i) { - const charRenderInfo = charRenderInfos[i]; - const texture = charRenderInfo.texture; - if (curTexture !== texture || curCharInfos.length * 4 + 4 > chunkMaxVertexCount) { - this._buildChunk(curTextChunk, curCharInfos.length); - - curTextChunk = new TextChunk(); - textChunks.push(curTextChunk); - curTexture = texture; - curTextChunk.texture = texture; - curCharInfos = curTextChunk.charRenderInfos; - } - curCharInfos.push(charRenderInfo); - } - const charLength = curCharInfos.length; - if (charLength > 0) { - this._buildChunk(curTextChunk, charLength); - } - charRenderInfos.length = 0; - } - - @ignoreClone - protected override _onTransformChanged(bit: TransformModifyFlags): void { - super._onTransformChanged(bit); - this._setDirtyFlagTrue(DirtyFlag.WorldPosition | DirtyFlag.WorldBounds); - } - - private _isTextNoVisible(): boolean { - return ( - !this._font || - this._text === "" || - this._fontSize === 0 || - (this.enableWrapping && this.width <= 0) || - (this.overflowMode === OverflowMode.Truncate && this.height <= 0) - ); - } - - private _buildChunk(textChunk: TextChunk, count: number): SubPrimitiveChunk { - const { r, g, b, a } = this.color; - const tempIndices = CharRenderInfo.triangles; - const tempIndicesLength = tempIndices.length; - const subChunk = (textChunk.subChunk = this._getChunkManager().allocateSubChunk(count * 4)); - const vertices = subChunk.chunk.vertices; - const indices = (subChunk.indices = []); - const charRenderInfos = textChunk.charRenderInfos; - for (let i = 0, ii = 0, io = 0, vo = subChunk.vertexArea.start + 3; i < count; ++i, io += 4) { - const charRenderInfo = charRenderInfos[i]; - charRenderInfo.indexInChunk = i; - - // Set indices - for (let j = 0; j < tempIndicesLength; ++j) { - indices[ii++] = tempIndices[j] + io; - } - - // Set uv and color for vertices - for (let j = 0; j < 4; ++j, vo += 9) { - const uv = charRenderInfo.uvs[j]; - uv.copyToArray(vertices, vo); - vertices[vo + 2] = r; - vertices[vo + 3] = g; - vertices[vo + 4] = b; - vertices[vo + 5] = a; - } - } - - return subChunk; - } - - private _freeTextChunks(): void { - const textChunks = this._textChunks; - const charRenderInfoPool = this.engine._charRenderInfoPool; - const manager = this._getChunkManager(); - for (let i = 0, n = textChunks.length; i < n; ++i) { - const textChunk = textChunks[i]; - const { charRenderInfos } = textChunk; - for (let j = 0, m = charRenderInfos.length; j < m; ++j) { - charRenderInfoPool.return(charRenderInfos[j]); - } - charRenderInfos.length = 0; - manager.freeSubChunk(textChunk.subChunk); - textChunk.subChunk = null; - textChunk.texture = null; - } - textChunks.length = 0; - } + // ===== Private ===== @ignoreClone private _onColorChanged(): void { - this._setDirtyFlagTrue(DirtyFlag.Color); + this._setDirtyFlagTrue(TextRenderableFlags.Color); } } - -class TextChunk { - charRenderInfos = new Array(); - subChunk: SubPrimitiveChunk; - texture: Texture2D; -} - -enum DirtyFlag { - SubFont = 0x1, - LocalPositionBounds = 0x2, - WorldPosition = 0x4, - WorldBounds = 0x8, - Color = 0x10, - - Position = LocalPositionBounds | WorldPosition | WorldBounds, - Font = SubFont | Position -} diff --git a/packages/core/src/2d/text/index.ts b/packages/core/src/2d/text/index.ts index 91a084757d..8b412e006b 100644 --- a/packages/core/src/2d/text/index.ts +++ b/packages/core/src/2d/text/index.ts @@ -1,4 +1,6 @@ export { Font } from "./Font"; +export type { ITextRenderable } from "./TextRenderable"; +export { TextChunk, TextRenderable, TextRenderableFlags } from "./TextRenderable"; export { TextRenderer } from "./TextRenderer"; // For set TextUtils._extendHeight used to extend the height of canvas, because in miniprogram performance is different from h5 export { CharRenderInfo } from "./CharRenderInfo"; diff --git a/packages/core/src/BasicResources.ts b/packages/core/src/BasicResources.ts index 264cf2ed95..dae8bd2a70 100644 --- a/packages/core/src/BasicResources.ts +++ b/packages/core/src/BasicResources.ts @@ -33,6 +33,8 @@ export class BasicResources { private static _maskReadOutsideRenderStates: RenderStateElementMap = null; private static _maskWriteIncrementRenderStates: RenderStateElementMap = null; private static _maskWriteDecrementRenderStates: RenderStateElementMap = null; + private static _uiStencilWriteStates: RenderStateElementMap = null; + private static _uiStencilTestStatesCache: Map = new Map(); static getMaskInteractionRenderStates(maskInteraction: SpriteMaskInteraction): RenderStateElementMap { const visibleInsideMask = maskInteraction === SpriteMaskInteraction.VisibleInsideMask; @@ -102,6 +104,49 @@ export class BasicResources { return renderStates; } + /** + * Get stencil write states for UI hierarchy-based mask (increment, no color write). + */ + static getUIStencilWriteStates(): RenderStateElementMap { + let renderStates = BasicResources._uiStencilWriteStates; + if (renderStates) { + return renderStates; + } + BasicResources._uiStencilWriteStates = renderStates = {}; + renderStates[RenderStateElementKey.StencilStateEnabled] = true; + renderStates[RenderStateElementKey.StencilStatePassOperationFront] = StencilOperation.IncrementSaturate; + renderStates[RenderStateElementKey.StencilStatePassOperationBack] = StencilOperation.IncrementSaturate; + renderStates[RenderStateElementKey.StencilStateCompareFunctionFront] = CompareFunction.Always; + renderStates[RenderStateElementKey.StencilStateCompareFunctionBack] = CompareFunction.Always; + renderStates[RenderStateElementKey.StencilStateFailOperationFront] = StencilOperation.Keep; + renderStates[RenderStateElementKey.StencilStateFailOperationBack] = StencilOperation.Keep; + renderStates[RenderStateElementKey.StencilStateZFailOperationFront] = StencilOperation.Keep; + renderStates[RenderStateElementKey.StencilStateZFailOperationBack] = StencilOperation.Keep; + renderStates[RenderStateElementKey.BlendStateColorWriteMask0] = ColorWriteMask.None; + renderStates[RenderStateElementKey.DepthStateEnabled] = false; + renderStates[RenderStateElementKey.RasterStateCullMode] = CullMode.Off; + return renderStates; + } + + /** + * Get stencil test states for UI hierarchy-based mask (read stencil, ref = depth, LessEqual). + */ + static getUIStencilTestStates(depth: number): RenderStateElementMap { + const cache = BasicResources._uiStencilTestStatesCache; + let renderStates = cache.get(depth); + if (renderStates) { + return renderStates; + } + renderStates = {}; + renderStates[RenderStateElementKey.StencilStateEnabled] = true; + renderStates[RenderStateElementKey.StencilStateWriteMask] = 0x00; + renderStates[RenderStateElementKey.StencilStateReferenceValue] = depth; + renderStates[RenderStateElementKey.StencilStateCompareFunctionFront] = CompareFunction.LessEqual; + renderStates[RenderStateElementKey.StencilStateCompareFunctionBack] = CompareFunction.LessEqual; + cache.set(depth, renderStates); + return renderStates; + } + /** * Use triangle to blit texture, ref: https://michaldrobot.com/2014/04/01/gcn-execution-patterns-in-full-screen-passes/ . */ diff --git a/packages/core/src/RenderPipeline/BatchUtils.ts b/packages/core/src/RenderPipeline/BatchUtils.ts index 387c951f93..1b250d407f 100644 --- a/packages/core/src/RenderPipeline/BatchUtils.ts +++ b/packages/core/src/RenderPipeline/BatchUtils.ts @@ -16,9 +16,43 @@ export class BatchUtils { return false; } + // UI stencil depth must match for batching + if (elementA.uiStencilDepth !== elementB.uiStencilDepth || elementA.uiStencilOp !== elementB.uiStencilOp) { + return false; + } + const rendererA = elementA.component; const rendererB = elementB.component; const maskInteractionA = rendererA.maskInteraction; + const rendererAAny = rendererA as any; + const rendererBAny = rendererB as any; + const rectMaskEnabledA = rendererAAny._rectMaskEnabled; + if (rectMaskEnabledA !== rendererBAny._rectMaskEnabled) { + return false; + } + if (rectMaskEnabledA) { + const rectMaskRectA = rendererAAny._rectMaskRect; + const rectMaskRectB = rendererBAny._rectMaskRect; + const rectMaskSoftnessA = rendererAAny._rectMaskSoftness; + const rectMaskSoftnessB = rendererBAny._rectMaskSoftness; + if ( + !rectMaskRectA || + !rectMaskRectB || + !rectMaskSoftnessA || + !rectMaskSoftnessB || + rectMaskRectA.x !== rectMaskRectB.x || + rectMaskRectA.y !== rectMaskRectB.y || + rectMaskRectA.z !== rectMaskRectB.z || + rectMaskRectA.w !== rectMaskRectB.w || + rectMaskSoftnessA.x !== rectMaskSoftnessB.x || + rectMaskSoftnessA.y !== rectMaskSoftnessB.y || + rectMaskSoftnessA.z !== rectMaskSoftnessB.z || + rectMaskSoftnessA.w !== rectMaskSoftnessB.w || + rendererAAny._rectMaskHardClip !== rendererBAny._rectMaskHardClip + ) { + return false; + } + } // Compare mask, texture and material return ( diff --git a/packages/core/src/RenderPipeline/MaskManager.ts b/packages/core/src/RenderPipeline/MaskManager.ts index 8454e1d584..5a55481572 100644 --- a/packages/core/src/RenderPipeline/MaskManager.ts +++ b/packages/core/src/RenderPipeline/MaskManager.ts @@ -1,4 +1,6 @@ +import { Vector3 } from "@galacean/engine-math"; import { SpriteMask } from "../2d"; +import { SpriteMaskInteraction } from "../2d/enums/SpriteMaskInteraction"; import { CameraClearFlags } from "../enums/CameraClearFlags"; import { SpriteMaskLayer } from "../enums/SpriteMaskLayer"; import { Material } from "../material"; @@ -29,16 +31,55 @@ export class MaskManager { private _preMaskLayer = SpriteMaskLayer.Nothing; private _allSpriteMasks = new DisorderedArray(); + private _filteredMasksByLayer = new Map(); + private _isFilteredMasksDirty = true; addSpriteMask(mask: SpriteMask): void { mask._maskIndex = this._allSpriteMasks.length; this._allSpriteMasks.add(mask); + this._isFilteredMasksDirty = true; } removeSpriteMask(mask: SpriteMask): void { const replaced = this._allSpriteMasks.deleteByIndex(mask._maskIndex); replaced && (replaced._maskIndex = mask._maskIndex); mask._maskIndex = -1; + this._isFilteredMasksDirty = true; + } + + /** + * Called when a mask's influenceLayers changes. + */ + onMaskInfluenceLayersChange(): void { + this._isFilteredMasksDirty = true; + } + + /** + * Check if a world point is visible given the mask interaction and layer. + */ + isVisibleByMask( + maskInteraction: SpriteMaskInteraction, + maskLayer: SpriteMaskLayer, + worldPoint: Vector3 + ): boolean { + if (maskInteraction === SpriteMaskInteraction.None) { + return true; + } + + const masks = this._getMasksByLayer(maskLayer); + if (!masks || masks.length === 0) { + return maskInteraction === SpriteMaskInteraction.VisibleOutsideMask; + } + + let insideAny = false; + for (let i = 0, n = masks.length; i < n; i++) { + if (masks[i]._containsWorldPoint(worldPoint)) { + insideAny = true; + break; + } + } + + return maskInteraction === SpriteMaskInteraction.VisibleInsideMask ? insideAny : !insideAny; } drawMask(context: RenderContext, pipelineStageTagValue: string, maskLayer: SpriteMaskLayer): void { @@ -118,6 +159,28 @@ export class MaskManager { const allSpriteMasks = this._allSpriteMasks; allSpriteMasks.length = 0; allSpriteMasks.garbageCollection(); + this._filteredMasksByLayer.clear(); + } + + private _getMasksByLayer(maskLayer: SpriteMaskLayer): SpriteMask[] | undefined { + if (this._isFilteredMasksDirty) { + this._filteredMasksByLayer.clear(); + this._isFilteredMasksDirty = false; + } + + let filtered = this._filteredMasksByLayer.get(maskLayer); + if (filtered === undefined) { + filtered = []; + const masks = this._allSpriteMasks; + for (let i = 0, n = masks.length; i < n; i++) { + const mask = masks.get(i); + if (mask.influenceLayers & maskLayer) { + filtered.push(mask); + } + } + this._filteredMasksByLayer.set(maskLayer, filtered); + } + return filtered; } private _buildMaskRenderElement( diff --git a/packages/core/src/RenderPipeline/RenderQueue.ts b/packages/core/src/RenderPipeline/RenderQueue.ts index 3558679996..30dc08ab76 100644 --- a/packages/core/src/RenderPipeline/RenderQueue.ts +++ b/packages/core/src/RenderPipeline/RenderQueue.ts @@ -75,16 +75,26 @@ export class RenderQueue { renderer._updateTransformShaderData(context, true, batched); } - const maskInteraction = renderer._maskInteraction; + const maskInteraction = (renderer as any)._maskInteraction ?? SpriteMaskInteraction.None; const needMaskInteraction = maskInteraction !== SpriteMaskInteraction.None; const needMaskType = maskType !== RenderQueueMaskType.No; let customStates: RenderStateElementMap = null; - if (needMaskType) { + // UI hierarchy-based stencil mask + const uiStencilDepth = subElement.uiStencilDepth; + if (uiStencilDepth > 0) { + if (subElement.uiStencilOp === 1) { + // Mask shape: write stencil (increment) + customStates = BasicResources.getUIStencilWriteStates(); + } else { + // Masked content: test stencil + customStates = BasicResources.getUIStencilTestStates(uiStencilDepth); + } + } else if (needMaskType) { customStates = BasicResources.getMaskTypeRenderStates(maskType); } else { if (needMaskInteraction) { - maskManager.drawMask(context, pipelineStageTagValue, subElement.component._maskLayer); + maskManager.drawMask(context, pipelineStageTagValue, (renderer as any)._maskLayer); customStates = BasicResources.getMaskInteractionRenderStates(maskInteraction); } else { maskManager.isReadStencil(material) && maskManager.clearMask(context, pipelineStageTagValue); @@ -108,8 +118,8 @@ export class RenderQueue { } let renderState = shaderPass._renderState; - if (needMaskType) { - // Mask don't care render queue type + if (needMaskType || uiStencilDepth > 0) { + // Mask and UI stencil elements don't care about render queue type if (!renderState) { renderState = renderStates[j]; } diff --git a/packages/core/src/RenderPipeline/SubRenderElement.ts b/packages/core/src/RenderPipeline/SubRenderElement.ts index f8c86c114d..aea7b4ea8f 100644 --- a/packages/core/src/RenderPipeline/SubRenderElement.ts +++ b/packages/core/src/RenderPipeline/SubRenderElement.ts @@ -17,6 +17,11 @@ export class SubRenderElement implements IPoolElement { batched: boolean; renderQueueFlags: RenderQueueFlags; + /** UI stencil depth. 0 = no stencil, >0 = stencil test/write at this depth. */ + uiStencilDepth: number = 0; + /** UI stencil operation. 0 = test (read stencil), 1 = increment (write mask), -1 = decrement (exit mask). */ + uiStencilOp: number = 0; + // @todo: maybe should remove later texture?: Texture2D; subChunk?: SubPrimitiveChunk; @@ -35,6 +40,8 @@ export class SubRenderElement implements IPoolElement { this.subPrimitive = subPrimitive; this.texture = texture; this.subChunk = subChunk; + this.uiStencilDepth = 0; + this.uiStencilOp = 0; } dispose(): void { diff --git a/packages/core/src/RenderPipeline/index.ts b/packages/core/src/RenderPipeline/index.ts index 7161b57757..8e32233f03 100644 --- a/packages/core/src/RenderPipeline/index.ts +++ b/packages/core/src/RenderPipeline/index.ts @@ -1,5 +1,8 @@ export { BasicRenderPipeline, RenderQueueFlags } from "./BasicRenderPipeline"; export { BatchUtils } from "./BatchUtils"; export { Blitter } from "./Blitter"; +export { PrimitiveChunkManager } from "./PrimitiveChunkManager"; +export { RenderContext } from "./RenderContext"; export { RenderQueue } from "./RenderQueue"; +export { SubPrimitiveChunk } from "./SubPrimitiveChunk"; export { PipelineStage } from "./enums/PipelineStage"; diff --git a/packages/core/src/Renderer.ts b/packages/core/src/Renderer.ts index ab9f743c0c..d193db9f77 100644 --- a/packages/core/src/Renderer.ts +++ b/packages/core/src/Renderer.ts @@ -1,6 +1,5 @@ // @ts-ignore import { BoundingBox, Matrix, Vector3, Vector4 } from "@galacean/engine-math"; -import { SpriteMaskInteraction } from "./2d/enums/SpriteMaskInteraction"; import { Component } from "./Component"; import { DependentMode, dependentComponents } from "./ComponentsDependencies"; import { Entity } from "./Entity"; @@ -8,7 +7,6 @@ import { RenderContext } from "./RenderPipeline/RenderContext"; import { SubRenderElement } from "./RenderPipeline/SubRenderElement"; import { Transform, TransformModifyFlags } from "./Transform"; import { assignmentClone, deepClone, ignoreClone } from "./clone/CloneManager"; -import { SpriteMaskLayer } from "./enums/SpriteMaskLayer"; import { Material } from "./material"; import { ShaderMacro, ShaderProperty } from "./shader"; import { ShaderData } from "./shader/ShaderData"; @@ -47,13 +45,8 @@ export class Renderer extends Component { @ignoreClone _renderFrameCount: number; /** @internal */ - @assignmentClone - _maskInteraction: SpriteMaskInteraction = SpriteMaskInteraction.None; - /** @internal */ @ignoreClone _batchedTransformShaderData: boolean = false; - @assignmentClone - _maskLayer: SpriteMaskLayer = SpriteMaskLayer.Layer0; @ignoreClone protected _overrideUpdate: boolean = false; diff --git a/packages/core/src/shaderlib/extra/text.fs.glsl b/packages/core/src/shaderlib/extra/text.fs.glsl index 8fe1125d69..019d419ba4 100644 --- a/packages/core/src/shaderlib/extra/text.fs.glsl +++ b/packages/core/src/shaderlib/extra/text.fs.glsl @@ -1,15 +1,46 @@ uniform sampler2D renderElement_TextTexture; +uniform vec4 renderer_UIRectClipRect; +uniform float renderer_UIRectClipEnabled; +uniform vec4 renderer_UIRectClipSoftness; +uniform float renderer_UIRectClipHardClip; varying vec2 v_uv; varying vec4 v_color; +varying vec2 v_worldPosition; + +float getUIRectClipAlpha() +{ + vec4 edgeDistance = vec4( + v_worldPosition.x - renderer_UIRectClipRect.x, + v_worldPosition.y - renderer_UIRectClipRect.y, + renderer_UIRectClipRect.z - v_worldPosition.x, + renderer_UIRectClipRect.w - v_worldPosition.y + ); + vec4 hardClipFactor = step(vec4(0.0), edgeDistance); + vec4 softness = max(renderer_UIRectClipSoftness, vec4(1e-5)); + vec4 softClipFactor = clamp(edgeDistance / softness, 0.0, 1.0); + vec4 useSoftness = step(vec4(1e-5), renderer_UIRectClipSoftness); + vec4 clipFactor = mix(hardClipFactor, softClipFactor, useSoftness); + return clipFactor.x * clipFactor.y * clipFactor.z * clipFactor.w; +} void main() { + float rectClipAlpha = 1.0; + if (renderer_UIRectClipEnabled > 0.5) { + rectClipAlpha = getUIRectClipAlpha(); + } + vec4 texColor = texture2D(renderElement_TextTexture, v_uv); #ifdef GRAPHICS_API_WEBGL2 float coverage = texColor.r; #else float coverage = texColor.a; #endif - gl_FragColor = vec4(v_color.rgb, v_color.a * coverage); + vec4 finalColor = vec4(v_color.rgb, v_color.a * coverage); + finalColor.a *= rectClipAlpha; + if (renderer_UIRectClipEnabled > 0.5 && renderer_UIRectClipHardClip > 0.5 && finalColor.a < 0.001) { + discard; + } + gl_FragColor = finalColor; } diff --git a/packages/core/src/shaderlib/extra/text.vs.glsl b/packages/core/src/shaderlib/extra/text.vs.glsl index 37a6b2d333..c3971d0172 100644 --- a/packages/core/src/shaderlib/extra/text.vs.glsl +++ b/packages/core/src/shaderlib/extra/text.vs.glsl @@ -1,4 +1,5 @@ uniform mat4 renderer_MVPMat; +uniform mat4 renderer_ModelMat; attribute vec3 POSITION; attribute vec2 TEXCOORD_0; @@ -6,6 +7,7 @@ attribute vec4 COLOR_0; varying vec2 v_uv; varying vec4 v_color; +varying vec2 v_worldPosition; void main() { @@ -13,4 +15,5 @@ void main() v_uv = TEXCOORD_0; v_color = COLOR_0; + v_worldPosition = POSITION.xy; } diff --git a/packages/core/src/ui/UIUtils.ts b/packages/core/src/ui/UIUtils.ts index c57c8bdf1f..ad2a8e517a 100644 --- a/packages/core/src/ui/UIUtils.ts +++ b/packages/core/src/ui/UIUtils.ts @@ -1,14 +1,21 @@ -import { Matrix, Vector4 } from "@galacean/engine-math"; +import { Color, Matrix, Vector4 } from "@galacean/engine-math"; import { Camera } from "../Camera"; import { Engine } from "../Engine"; import { Layer } from "../Layer"; +import { Blitter } from "../RenderPipeline/Blitter"; import { RenderQueue } from "../RenderPipeline"; import { ContextRendererUpdateFlag } from "../RenderPipeline/RenderContext"; import { Scene } from "../Scene"; import { VirtualCamera } from "../VirtualCamera"; import { EngineObject } from "../base"; -import { RenderQueueType, ShaderData, ShaderDataGroup, ShaderMacro } from "../shader"; +import { CameraClearFlags } from "../enums/CameraClearFlags"; +import { Material } from "../material"; +import { RenderQueueType, Shader, ShaderData, ShaderDataGroup, ShaderMacro } from "../shader"; +import { BlendFactor } from "../shader/enums/BlendFactor"; import { ShaderMacroCollection } from "../shader/ShaderMacroCollection"; +import { RenderTarget } from "../texture/RenderTarget"; +import { Texture2D } from "../texture/Texture2D"; +import { TextureFormat } from "../texture/enums/TextureFormat"; import { DisorderedArray } from "../utils/DisorderedArray"; import { IUICanvas } from "./IUICanvas"; @@ -19,6 +26,9 @@ export class UIUtils { private static _virtualCamera: VirtualCamera; private static _viewport: Vector4; private static _overlayCamera: OverlayCamera; + private static _overlayRT: RenderTarget; + private static _overlayBlitMaterial: Material; + private static _clearColor = new Color(0, 0, 0, 0); static renderOverlay(engine: Engine, scene: Scene, uiCanvases: DisorderedArray): void { engine._macroCollection.enable(UIUtils._shouldSRGBCorrect); @@ -31,17 +41,24 @@ export class UIUtils { camera.engine = engine; camera.scene = scene; renderContext.camera = camera as unknown as Camera; + + const { width, height } = canvas; const { elements: projectE } = virtualCamera.projectionMatrix; const { elements: viewE } = virtualCamera.viewMatrix; - (projectE[0] = 2 / canvas.width), (projectE[5] = 2 / canvas.height), (projectE[10] = 0); - renderContext.setRenderTarget(null, viewport, 0); + (projectE[0] = 2 / width), (projectE[5] = 2 / height), (projectE[10] = 0); + + // Use an intermediate RT with stencil so that UI Mask (stencil-based) works + const overlayRT = UIUtils._getOverlayRT(engine, width, height); + renderContext.setRenderTarget(overlayRT, viewport, 0); + rhi.clearRenderTarget(engine, CameraClearFlags.All, UIUtils._clearColor); + for (let i = 0, n = uiCanvases.length; i < n; i++) { const uiCanvas = uiCanvases.get(i); if (uiCanvas) { const { position } = uiCanvas.entity.transform; (viewE[12] = -position.x), (viewE[13] = -position.y); Matrix.multiply(virtualCamera.projectionMatrix, virtualCamera.viewMatrix, virtualCamera.viewProjectionMatrix); - renderContext.applyVirtualCamera(virtualCamera, false); + renderContext.applyVirtualCamera(virtualCamera, true); uiRenderQueue.rendererUpdateFlag |= ContextRendererUpdateFlag.ProjectionMatrix; uiCanvas._prepareRender(renderContext); uiRenderQueue.pushRenderElement(uiCanvas._renderElement); @@ -52,9 +69,55 @@ export class UIUtils { engine._renderCount++; } } + + // Blit overlay RT to default framebuffer with premultiplied alpha blending + Blitter.blitTexture( + engine, + overlayRT.getColorTexture(0) as Texture2D, + null, + 0, + viewport, + UIUtils._getOverlayBlitMaterial(engine) + ); + renderContext.camera = null; engine._macroCollection.disable(UIUtils._shouldSRGBCorrect); } + + private static _getOverlayRT(engine: Engine, width: number, height: number): RenderTarget { + let rt = UIUtils._overlayRT; + if (!rt || rt.width !== width || rt.height !== height) { + if (rt) { + rt.getColorTexture(0).destroy(); + rt.destroy(); + } + const colorTexture = new Texture2D(engine, width, height, TextureFormat.R8G8B8A8, false); + colorTexture.isGCIgnored = true; + rt = new RenderTarget(engine, width, height, colorTexture, TextureFormat.Depth24Stencil8); + rt.isGCIgnored = true; + UIUtils._overlayRT = rt; + } + return rt; + } + + private static _getOverlayBlitMaterial(engine: Engine): Material { + let material = UIUtils._overlayBlitMaterial; + if (!material) { + material = new Material(engine, Shader.find("blit")); + material.isGCIgnored = true; + const renderState = material.renderState; + renderState.depthState.enabled = false; + renderState.depthState.writeEnabled = false; + const target = renderState.blendState.targetBlendState; + target.enabled = true; + target.sourceColorBlendFactor = BlendFactor.One; + target.destinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha; + target.sourceAlphaBlendFactor = BlendFactor.One; + target.destinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha; + UIUtils._overlayBlitMaterial = material; + } + return material; + } } class OverlayCamera { diff --git a/packages/ui/src/component/UICanvas.ts b/packages/ui/src/component/UICanvas.ts index 9e5b33c405..c2500f0034 100644 --- a/packages/ui/src/component/UICanvas.ts +++ b/packages/ui/src/component/UICanvas.ts @@ -25,6 +25,8 @@ import { ResolutionAdaptationMode } from "../enums/ResolutionAdaptationMode"; import { UIHitResult } from "../input/UIHitResult"; import { IElement } from "../interface/IElement"; import { IGroupAble } from "../interface/IGroupAble"; +import { Mask } from "./advanced/Mask"; +import { RectMask2D } from "./advanced/RectMask2D"; import { UIGroup } from "./UIGroup"; import { UIRenderer } from "./UIRenderer"; import { UITransform } from "./UITransform"; @@ -39,6 +41,7 @@ export class UICanvas extends Component implements IElement { /** @internal */ static _hierarchyCounter: number = 1; private static _tempGroupAbleList: IGroupAble[] = []; + private static _tempRectMaskList: RectMask2D[] = []; private static _tempVec3: Vector3 = new Vector3(); private static _tempMat: Matrix = new Matrix(); @@ -418,7 +421,8 @@ export class UICanvas extends Component implements IElement { const { _orderedRenderers: renderers, entity } = this; const uiHierarchyVersion = entity._uiHierarchyVersion; if (this._hierarchyVersion !== uiHierarchyVersion) { - renderers.length = this._walk(this.entity, renderers); + UICanvas._tempRectMaskList.length = 0; + renderers.length = this._walk(this.entity, renderers, 0, null, 0); UICanvas._tempGroupAbleList.length = 0; this._hierarchyVersion = uiHierarchyVersion; ++UICanvas._hierarchyCounter; @@ -500,26 +504,51 @@ export class UICanvas extends Component implements IElement { transform.size.set(curWidth / expectX, curHeight / expectY); } - private _walk(entity: Entity, renderers: UIRenderer[], depth = 0, group: UIGroup = null): number { + private _walk( + entity: Entity, + renderers: UIRenderer[], + depth = 0, + group: UIGroup = null, + rectMaskCount: number = 0, + stencilDepth: number = 0 + ): number { // @ts-ignore const components: Component[] = entity._components; const tempGroupAbleList = UICanvas._tempGroupAbleList; + const tempRectMaskList = UICanvas._tempRectMaskList; + let rectMask: RectMask2D = null; + let hasMask = false; let groupAbleCount = 0; for (let i = 0, n = components.length; i < n; i++) { const component = components[i]; if (!component.enabled) continue; - if (component instanceof UIRenderer) { + if (component instanceof Mask) { + // Mask is a UIRenderer — process it as such, but also flag the mask + hasMask = true; renderers[depth] = component; ++depth; component._isRootCanvasDirty && Utils.setRootCanvas(component, this); if (component._isGroupDirty) { tempGroupAbleList[groupAbleCount++] = component; } + component._setRectMasks(tempRectMaskList, rectMaskCount); + component._uiStencilDepth = stencilDepth + 1; + } else if (component instanceof UIRenderer) { + renderers[depth] = component; + ++depth; + component._isRootCanvasDirty && Utils.setRootCanvas(component, this); + if (component._isGroupDirty) { + tempGroupAbleList[groupAbleCount++] = component; + } + component._setRectMasks(tempRectMaskList, rectMaskCount); + component._uiStencilDepth = stencilDepth; } else if (component instanceof UIInteractive) { component._isRootCanvasDirty && Utils.setRootCanvas(component, this); if (component._isGroupDirty) { tempGroupAbleList[groupAbleCount++] = component; } + } else if (component instanceof RectMask2D) { + rectMask = component; } else if (component instanceof UIGroup) { component._isRootCanvasDirty && Utils.setRootCanvas(component, this); component._isGroupDirty && Utils.setGroup(component, group); @@ -529,10 +558,17 @@ export class UICanvas extends Component implements IElement { for (let i = 0; i < groupAbleCount; i++) { Utils.setGroup(tempGroupAbleList[i], group); } + if (rectMask) { + tempRectMaskList[rectMaskCount++] = rectMask; + } + // If this entity has a Mask, increment stencil depth for children + if (hasMask) { + stencilDepth++; + } const children = entity.children; for (let i = 0, n = children.length; i < n; i++) { const child = children[i]; - child.isActive && (depth = this._walk(child, renderers, depth, group)); + child.isActive && (depth = this._walk(child, renderers, depth, group, rectMaskCount, stencilDepth)); } return depth; } diff --git a/packages/ui/src/component/UIRenderer.ts b/packages/ui/src/component/UIRenderer.ts index 59a2fc434c..b84ad7333b 100644 --- a/packages/ui/src/component/UIRenderer.ts +++ b/packages/ui/src/component/UIRenderer.ts @@ -4,13 +4,20 @@ import { DependentMode, Entity, EntityModifyFlags, + Material, Matrix, Plane, Ray, + RenderContext, + RenderQueueFlags, Renderer, RendererUpdateFlags, ShaderMacroCollection, ShaderProperty, + SpriteMaskInteraction, + SubPrimitiveChunk, + Texture2D, + Vector2, Vector3, Vector4, assignmentClone, @@ -19,8 +26,10 @@ import { ignoreClone } from "@galacean/engine"; import { Utils } from "../Utils"; +import { CanvasRenderMode } from "../enums/CanvasRenderMode"; import { UIHitResult } from "../input/UIHitResult"; import { IGraphics } from "../interface/IGraphics"; +import { RectMask2D } from "./advanced/RectMask2D"; import { EntityUIModifyFlags, UICanvas } from "./UICanvas"; import { GroupModifyFlags, UIGroup } from "./UIGroup"; import { UITransform } from "./UITransform"; @@ -37,6 +46,16 @@ export class UIRenderer extends Renderer implements IGraphics { static _tempPlane: Plane = new Plane(); /** @internal */ static _textureProperty: ShaderProperty = ShaderProperty.getByName("renderer_UITexture"); + /** @internal */ + static _rectClipRectProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipRect"); + /** @internal */ + static _rectClipEnabledProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipEnabled"); + /** @internal */ + static _rectClipSoftnessProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipSoftness"); + /** @internal */ + static _rectClipHardClipProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipHardClip"); + /** @internal */ + static _tempRect: Vector4 = new Vector4(); /** * Custom boundary for raycast detection. @@ -69,6 +88,24 @@ export class UIRenderer extends Renderer implements IGraphics { /** @internal */ @ignoreClone _subChunk; + /** @internal */ + @ignoreClone + _rectMasks: RectMask2D[] = []; + /** @internal */ + @ignoreClone + _rectMaskRect: Vector4 = new Vector4(); + /** @internal */ + @ignoreClone + _rectMaskEnabled: boolean = false; + /** @internal */ + @ignoreClone + _rectMaskSoftness: Vector4 = new Vector4(); + /** @internal */ + @ignoreClone + _rectMaskHardClip: boolean = false; + /** @internal - stencil depth set by UICanvas._walk for hierarchy-based Mask */ + @ignoreClone + _uiStencilDepth: number = 0; @assignmentClone private _raycastEnabled: boolean = true; @@ -110,6 +147,9 @@ export class UIRenderer extends Renderer implements IGraphics { this._color._onValueChanged = this._onColorChanged; this._groupListener = this._groupListener.bind(this); this._rootCanvasListener = this._rootCanvasListener.bind(this); + this.shaderData.setFloat(UIRenderer._rectClipEnabledProperty, 0); + this.shaderData.setVector4(UIRenderer._rectClipSoftnessProperty, this._rectMaskSoftness); + this.shaderData.setFloat(UIRenderer._rectClipHardClipProperty, 0); } // @ts-ignore @@ -135,6 +175,7 @@ export class UIRenderer extends Renderer implements IGraphics { this._update(context); } + this._updateRectMaskClipState(); this._render(context); // union camera global macro and renderer macro. @@ -237,6 +278,105 @@ export class UIRenderer extends Renderer implements IGraphics { return this.engine._batcherManager.primitiveChunkManagerUI; } + // ===== Layout methods: default implementations for UI ===== + + /** + * Get width from UITransform. + * @internal + */ + _getWidth(): number { + return (this._transformEntity.transform).size.x; + } + + /** + * Get height from UITransform. + * @internal + */ + _getHeight(): number { + return (this._transformEntity.transform).size.y; + } + + /** + * Get pivot from UITransform. + * @internal + */ + _getPivot(): Vector2 { + return (this._transformEntity.transform).pivot; + } + + /** + * Get pivot X from UITransform. + * @internal + */ + _getPivotX(): number { + return (this._transformEntity.transform).pivot.x; + } + + /** + * Get pivot Y from UITransform. + * @internal + */ + _getPivotY(): number { + return (this._transformEntity.transform).pivot.y; + } + + /** + * Get alpha from UIGroup. + * @internal + */ + _getAlpha(): number { + return this._getGlobalAlpha(); + } + + /** + * Get reference resolution per unit from UICanvas. + * @internal + */ + _getReferenceResolutionPerUnit(): number | undefined { + return this._getRootCanvas()?.referenceResolutionPerUnit; + } + + /** + * Submit render element to canvas for UI rendering. + * @param context - The render context + * @param material - The material to use + * @param subChunk - The sub primitive chunk + * @param texture - The texture to use + * @param stencilOp - Stencil operation: 0 = test (read), 1 = increment (write). Default is 0. + * @param forceAllRenderQueue - Whether to force render in all render queues. Default is false. + * @internal + */ + _submitToCanvas( + context: RenderContext, + material: Material, + subChunk: SubPrimitiveChunk, + texture: Texture2D, + stencilOp: number = 0, + forceAllRenderQueue: boolean = false + ): void { + const canvas = this._getRootCanvas(); + if (!canvas) return; + + const engine = context.camera.engine; + const subRenderElement = engine._subRenderElementPool.get(); + subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, texture, subChunk); + + // Set shader passes and render queue flags for overlay mode or forced all queues + if (forceAllRenderQueue || canvas._realRenderMode === CanvasRenderMode.ScreenSpaceOverlay) { + subRenderElement.shaderPasses = material.shader.subShaders[0].passes; + subRenderElement.renderQueueFlags = RenderQueueFlags.All; + } + + // Set stencil for hierarchy-based masking + const stencilDepth = this._uiStencilDepth; + if (stencilDepth > 0 || stencilOp !== 0) { + subRenderElement.uiStencilDepth = stencilDepth; + subRenderElement.uiStencilOp = stencilOp; + } + + canvas._renderElement.addSubRenderElement(subRenderElement); + } + /** * @internal */ @@ -252,7 +392,11 @@ export class UIRenderer extends Renderer implements IGraphics { Matrix.invert(transform.worldMatrix, worldMatrixInv); const localPosition = UIRenderer._tempVec31; Vector3.transformCoordinate(hitPointWorld, worldMatrixInv, localPosition); - if (this._hitTest(localPosition)) { + if ( + this._hitTest(localPosition) && + this._isRaycastVisibleByRectMask(hitPointWorld) && + this._isRaycastVisibleByMask(hitPointWorld) + ) { out.component = this; out.distance = curDistance; out.entity = this.entity; @@ -278,6 +422,153 @@ export class UIRenderer extends Renderer implements IGraphics { ); } + /** + * @internal + */ + _setRectMasks(rectMasks: RectMask2D[], count: number): void { + const targetMasks = this._rectMasks; + targetMasks.length = count; + for (let i = 0; i < count; i++) { + targetMasks[i] = rectMasks[i]; + } + } + + private _isRaycastVisibleByMask(hitPointWorld: Vector3): boolean { + const maskInteraction = (this as any)._maskInteraction ?? SpriteMaskInteraction.None; + if (maskInteraction === SpriteMaskInteraction.None) { + return true; + } + // @ts-ignore + return this.scene._maskManager.isVisibleByMask(maskInteraction, (this as any)._maskLayer, hitPointWorld); + } + + private _isRaycastVisibleByRectMask(hitPointWorld: Vector3): boolean { + const rectMasks = this._rectMasks; + for (let i = 0, n = rectMasks.length; i < n; i++) { + const rectMask = rectMasks[i]; + if (!rectMask.enabled || !rectMask.entity.isActiveInHierarchy) { + continue; + } + if (!rectMask._containsWorldPoint(hitPointWorld)) { + return false; + } + } + return true; + } + + private _updateRectMaskClipState(): void { + const rectMasks = this._rectMasks; + const count = rectMasks.length; + if (count <= 0) { + if (this._rectMaskEnabled) { + this._rectMaskEnabled = false; + this.shaderData.setFloat(UIRenderer._rectClipEnabledProperty, 0); + } + const rectMaskSoftness = this._rectMaskSoftness; + if (rectMaskSoftness.x !== 0 || rectMaskSoftness.y !== 0 || rectMaskSoftness.z !== 0 || rectMaskSoftness.w !== 0) { + rectMaskSoftness.set(0, 0, 0, 0); + this.shaderData.setVector4(UIRenderer._rectClipSoftnessProperty, rectMaskSoftness); + } + if (this._rectMaskHardClip) { + this._rectMaskHardClip = false; + this.shaderData.setFloat(UIRenderer._rectClipHardClipProperty, 0); + } + return; + } + + let minX = Number.NEGATIVE_INFINITY; + let minY = Number.NEGATIVE_INFINITY; + let maxX = Number.POSITIVE_INFINITY; + let maxY = Number.POSITIVE_INFINITY; + let clipSoftnessLeft = 0; + let clipSoftnessBottom = 0; + let clipSoftnessRight = 0; + let clipSoftnessTop = 0; + let clipHardClip = false; + let hasActiveMask = false; + const tempRect = UIRenderer._tempRect; + for (let i = 0; i < count; i++) { + const rectMask = rectMasks[i]; + if (!rectMask.enabled || !rectMask.entity.isActiveInHierarchy) { + continue; + } + hasActiveMask = true; + const softness = rectMask.softness; + if (!clipHardClip && rectMask.alphaClip) { + clipHardClip = true; + } + if (!rectMask._getWorldRect(tempRect)) { + minX = 1; + minY = 1; + maxX = 0; + maxY = 0; + break; + } + if (tempRect.x > minX) { + minX = tempRect.x; + clipSoftnessLeft = softness.x; + } + if (tempRect.y > minY) { + minY = tempRect.y; + clipSoftnessBottom = softness.y; + } + if (tempRect.z < maxX) { + maxX = tempRect.z; + clipSoftnessRight = softness.x; + } + if (tempRect.w < maxY) { + maxY = tempRect.w; + clipSoftnessTop = softness.y; + } + } + + if (!hasActiveMask) { + if (this._rectMaskEnabled) { + this._rectMaskEnabled = false; + this.shaderData.setFloat(UIRenderer._rectClipEnabledProperty, 0); + } + return; + } + + if (minX >= maxX || minY >= maxY) { + minX = 1; + minY = 1; + maxX = 0; + maxY = 0; + clipSoftnessLeft = 0; + clipSoftnessBottom = 0; + clipSoftnessRight = 0; + clipSoftnessTop = 0; + } + + const rectMaskRect = this._rectMaskRect; + if (rectMaskRect.x !== minX || rectMaskRect.y !== minY || rectMaskRect.z !== maxX || rectMaskRect.w !== maxY) { + rectMaskRect.set(minX, minY, maxX, maxY); + this.shaderData.setVector4(UIRenderer._rectClipRectProperty, rectMaskRect); + } + + const rectMaskSoftness = this._rectMaskSoftness; + if ( + rectMaskSoftness.x !== clipSoftnessLeft || + rectMaskSoftness.y !== clipSoftnessBottom || + rectMaskSoftness.z !== clipSoftnessRight || + rectMaskSoftness.w !== clipSoftnessTop + ) { + rectMaskSoftness.set(clipSoftnessLeft, clipSoftnessBottom, clipSoftnessRight, clipSoftnessTop); + this.shaderData.setVector4(UIRenderer._rectClipSoftnessProperty, rectMaskSoftness); + } + + if (this._rectMaskHardClip !== clipHardClip) { + this._rectMaskHardClip = clipHardClip; + this.shaderData.setFloat(UIRenderer._rectClipHardClipProperty, clipHardClip ? 1 : 0); + } + + if (!this._rectMaskEnabled) { + this._rectMaskEnabled = true; + this.shaderData.setFloat(UIRenderer._rectClipEnabledProperty, 1); + } + } + protected override _onDestroy(): void { if (this._subChunk) { this._getChunkManager().freeSubChunk(this._subChunk); @@ -287,6 +578,8 @@ export class UIRenderer extends Renderer implements IGraphics { //@ts-ignore this._color._onValueChanged = null; this._color = null; + this._rectMasks = null; + this._rectMaskSoftness = null; } } diff --git a/packages/ui/src/component/advanced/Image.ts b/packages/ui/src/component/advanced/Image.ts index b8ca6ffe55..1ebc223e05 100644 --- a/packages/ui/src/component/advanced/Image.ts +++ b/packages/ui/src/component/advanced/Image.ts @@ -1,149 +1,65 @@ import { BoundingBox, Entity, - ISpriteAssembler, - ISpriteRenderer, - MathUtil, - RenderQueueFlags, + Material, + RenderContext, RendererUpdateFlags, - SimpleSpriteAssembler, - SlicedSpriteAssembler, - Sprite, SpriteDrawMode, - SpriteModifyFlags, - SpriteTileMode, - TiledSpriteAssembler, - assignmentClone, + SpriteRenderable, + SpriteRenderableFlags, + SubPrimitiveChunk, + Texture2D, ignoreClone } from "@galacean/engine"; -import { CanvasRenderMode } from "../../enums/CanvasRenderMode"; import { RootCanvasModifyFlags } from "../UICanvas"; -import { UIRenderer, UIRendererUpdateFlags } from "../UIRenderer"; +import { UIRenderer } from "../UIRenderer"; import { UITransform, UITransformModifyFlags } from "../UITransform"; /** * UI element that renders an image. */ -export class Image extends UIRenderer implements ISpriteRenderer { - @ignoreClone - private _sprite: Sprite = null; - @ignoreClone - private _drawMode: SpriteDrawMode; - @ignoreClone - private _assembler: ISpriteAssembler; - @assignmentClone - private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; - @assignmentClone - private _tiledAdaptiveThreshold: number = 0.5; - - /** - * The draw mode of the image. - */ - get drawMode(): SpriteDrawMode { - return this._drawMode; - } - - set drawMode(value: SpriteDrawMode) { - if (this._drawMode !== value) { - this._drawMode = value; - switch (value) { - case SpriteDrawMode.Simple: - this._assembler = SimpleSpriteAssembler; - break; - case SpriteDrawMode.Sliced: - this._assembler = SlicedSpriteAssembler; - break; - case SpriteDrawMode.Tiled: - this._assembler = TiledSpriteAssembler; - break; - default: - break; - } - this._assembler.resetData(this); - this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; - } - } - +export class Image extends SpriteRenderable(UIRenderer) { /** - * The tiling mode of the image. (Only works in tiled mode.) + * @internal */ - get tileMode(): SpriteTileMode { - return this._tileMode; + constructor(entity: Entity) { + super(entity); + this._initSpriteRenderable(UIRenderer._textureProperty); } - set tileMode(value: SpriteTileMode) { - if (this._tileMode !== value) { - this._tileMode = value; - if (this.drawMode === SpriteDrawMode.Tiled) { - this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; - } - } - } + // ===== Abstract implementations ===== - /** - * Stretch Threshold in Tile Adaptive Mode, specified in normalized. (Only works in tiled adaptive mode.) - */ - get tiledAdaptiveThreshold(): number { - return this._tiledAdaptiveThreshold; + /** @internal */ + override _getColor() { + return this._color; } - set tiledAdaptiveThreshold(value: number) { - if (value !== this._tiledAdaptiveThreshold) { - value = MathUtil.clamp(value, 0, 1); - this._tiledAdaptiveThreshold = value; - if (this.drawMode === SpriteDrawMode.Tiled) { - this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; - } - } + /** @internal */ + override _getDefaultSpriteMaterial(): Material { + // @ts-ignore + return this._engine._getUIDefaultMaterial(); } - /** - * The Sprite to render. - */ - get sprite(): Sprite { - return this._sprite; + /** @internal */ + override _submitSpriteRenderElement( + context: RenderContext, + material: Material, + subChunk: SubPrimitiveChunk, + texture: Texture2D + ): void { + this._submitToCanvas(context, material, subChunk, texture); } - set sprite(value: Sprite | null) { - const lastSprite = this._sprite; - if (lastSprite !== value) { - if (lastSprite) { - this._addResourceReferCount(lastSprite, -1); - // @ts-ignore - lastSprite._updateFlagManager.removeListener(this._onSpriteChange); - } - this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; - if (value) { - this._addResourceReferCount(value, 1); - // @ts-ignore - value._updateFlagManager.addListener(this._onSpriteChange); - this.shaderData.setTexture(UIRenderer._textureProperty, value.texture); - } else { - this.shaderData.setTexture(UIRenderer._textureProperty, null); - } - this._sprite = value; - } - } - - /** - * @internal - */ - constructor(entity: Entity) { - super(entity); - this.drawMode = SpriteDrawMode.Simple; - // @ts-ignore - this.setMaterial(this._engine._getUIDefaultMaterial()); - this._onSpriteChange = this._onSpriteChange.bind(this); - } + // ===== Image-specific ===== /** * @internal */ _onRootCanvasModify(flag: RootCanvasModifyFlags): void { if (flag & RootCanvasModifyFlags.ReferenceResolutionPerUnit) { - const drawMode = this._drawMode; + const drawMode = this.drawMode; if (drawMode === SpriteDrawMode.Tiled) { - this._dirtyUpdateFlag |= ImageUpdateFlags.All; + this._dirtyUpdateFlag |= SpriteRenderableFlags.All; } else if (drawMode === SpriteDrawMode.Sliced) { this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; } @@ -153,29 +69,16 @@ export class Image extends UIRenderer implements ISpriteRenderer { /** * @internal */ + // @ts-ignore _cloneTo(target: Image): void { // @ts-ignore super._cloneTo(target); - target.sprite = this._sprite; - target.drawMode = this._drawMode; } protected override _updateBounds(worldBounds: BoundingBox): void { - const sprite = this._sprite; const rootCanvas = this._getRootCanvas(); - if (sprite && rootCanvas) { - const transform = this._transformEntity.transform; - const { size } = transform; - this._assembler.updatePositions( - this, - transform.worldMatrix, - size.x, - size.y, - transform.pivot, - false, - false, - rootCanvas.referenceResolutionPerUnit - ); + if (this.sprite && rootCanvas) { + super._updateBounds(worldBounds); } else { const { worldPosition } = this._transformEntity.transform; worldBounds.min.copyFrom(worldPosition); @@ -183,146 +86,11 @@ export class Image extends UIRenderer implements ISpriteRenderer { } } - protected override _render(context): void { - const { _sprite: sprite } = this; - const transform = this._transformEntity.transform; - const { x: width, y: height } = transform.size; - if (!sprite?.texture || !width || !height) { - return; - } - - let material = this.getMaterial(); - if (!material) { - return; - } - // @todo: This question needs to be raised rather than hidden. - if (material.destroyed) { - // @ts-ignore - material = this._engine._getUIDefaultMaterial(); - } - - const alpha = this._getGlobalAlpha(); - if (this._color.a * alpha <= 0) { - return; - } - - let { _dirtyUpdateFlag: dirtyUpdateFlag } = this; - const canvas = this._getRootCanvas(); - // Update position - if (dirtyUpdateFlag & RendererUpdateFlags.WorldVolume) { - this._assembler.updatePositions( - this, - transform.worldMatrix, - width, - height, - transform.pivot, - false, - false, - canvas.referenceResolutionPerUnit - ); - dirtyUpdateFlag &= ~RendererUpdateFlags.WorldVolume; - } - - // Update uv - if (dirtyUpdateFlag & ImageUpdateFlags.UV) { - this._assembler.updateUVs(this); - dirtyUpdateFlag &= ~ImageUpdateFlags.UV; - } - - // Update color - if (dirtyUpdateFlag & UIRendererUpdateFlags.Color) { - this._assembler.updateColor(this, alpha); - dirtyUpdateFlag &= ~UIRendererUpdateFlags.Color; - } - - this._dirtyUpdateFlag = dirtyUpdateFlag; - // Init sub render element. - const { engine } = context.camera; - const subRenderElement = engine._subRenderElementPool.get(); - const subChunk = this._subChunk; - subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, this.sprite.texture, subChunk); - if (canvas._realRenderMode === CanvasRenderMode.ScreenSpaceOverlay) { - subRenderElement.shaderPasses = material.shader.subShaders[0].passes; - subRenderElement.renderQueueFlags = RenderQueueFlags.All; - } - canvas._renderElement.addSubRenderElement(subRenderElement); - } - @ignoreClone protected override _onTransformChanged(type: number): void { - if (type & UITransformModifyFlags.Size && this._drawMode === SpriteDrawMode.Tiled) { - this._dirtyUpdateFlag |= ImageUpdateFlags.All; + if (type & UITransformModifyFlags.Size && this.drawMode === SpriteDrawMode.Tiled) { + this._dirtyUpdateFlag |= SpriteRenderableFlags.All; } this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; } - - protected override _onDestroy(): void { - const sprite = this._sprite; - if (sprite) { - this._addResourceReferCount(sprite, -1); - // @ts-ignore - sprite._updateFlagManager.removeListener(this._onSpriteChange); - this._sprite = null; - } - super._onDestroy(); - } - - @ignoreClone - private _onSpriteChange(type: SpriteModifyFlags): void { - switch (type) { - case SpriteModifyFlags.texture: - this.shaderData.setTexture(UIRenderer._textureProperty, this.sprite.texture); - break; - case SpriteModifyFlags.size: - switch (this._drawMode) { - case SpriteDrawMode.Sliced: - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; - break; - case SpriteDrawMode.Tiled: - this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; - break; - default: - break; - } - break; - case SpriteModifyFlags.border: - switch (this._drawMode) { - case SpriteDrawMode.Sliced: - this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; - break; - case SpriteDrawMode.Tiled: - this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; - break; - default: - break; - } - break; - case SpriteModifyFlags.region: - case SpriteModifyFlags.atlasRegionOffset: - this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; - break; - case SpriteModifyFlags.atlasRegion: - this._dirtyUpdateFlag |= ImageUpdateFlags.UV; - break; - case SpriteModifyFlags.destroy: - this.sprite = null; - break; - } - } -} - -/** - * @remarks Extends `UIRendererUpdateFlags`. - */ -enum ImageUpdateFlags { - /** UV. */ - UV = 0x4, - /** Automatic Size. */ - AutomaticSize = 0x8, - /** WorldVolume and UV. */ - WorldVolumeAndUV = 0x5, - /** WorldVolume, UV and Color. */ - WorldVolumeUVAndColor = 0x7, - /** All. */ - All = 0xf -} +} \ No newline at end of file diff --git a/packages/ui/src/component/advanced/Mask.ts b/packages/ui/src/component/advanced/Mask.ts new file mode 100644 index 0000000000..8f797e8e78 --- /dev/null +++ b/packages/ui/src/component/advanced/Mask.ts @@ -0,0 +1,98 @@ +import { + BoundingBox, + Entity, + Material, + PrimitiveChunkManager, + RenderContext, + RendererUpdateFlags, + ShaderProperty, + SubPrimitiveChunk, + Texture2D, + SpriteRenderable, + assignmentClone, + ignoreClone +} from "@galacean/engine"; +import { UIRenderer } from "../UIRenderer"; + +/** + * UI component that masks descendant UI elements using a sprite shape. + * + * @remarks + * Uses stencil buffer. All UIRenderers that are descendants of the Mask's entity + * are automatically clipped to the mask shape — no manual maskInteraction setup needed. + */ +export class Mask extends SpriteRenderable(UIRenderer) { + /** @internal */ + static _maskTextureProperty: ShaderProperty = ShaderProperty.getByName("renderer_MaskTexture"); + /** @internal */ + static _alphaCutoffProperty: ShaderProperty = ShaderProperty.getByName("renderer_MaskAlphaCutoff"); + + @assignmentClone + private _alphaCutoff: number = 0.5; + + /** + * The minimum alpha value used by the mask to select the area of influence. + * Value between 0 and 1. + */ + get alphaCutoff(): number { + return this._alphaCutoff; + } + + set alphaCutoff(value: number) { + if (this._alphaCutoff !== value) { + this._alphaCutoff = value; + this.shaderData.setFloat(Mask._alphaCutoffProperty, value); + } + } + + /** + * @internal + */ + constructor(entity: Entity) { + super(entity); + this._initSpriteRenderable(Mask._maskTextureProperty); + this.shaderData.setFloat(Mask._alphaCutoffProperty, this._alphaCutoff); + this.raycastEnabled = false; + } + + // ===== SpriteRenderable abstract implementations ===== + + /** @internal */ + override _getChunkManager(): PrimitiveChunkManager { + // @ts-ignore + return this.engine._batcherManager.primitiveChunkManagerMask; + } + + /** @internal */ + override _getDefaultSpriteMaterial(): Material { + // @ts-ignore + return this._engine._basicResources.spriteMaskDefaultMaterial; + } + + /** @internal */ + override _submitSpriteRenderElement( + context: RenderContext, + material: Material, + subChunk: SubPrimitiveChunk, + texture: Texture2D + ): void { + // stencilOp = 1 (increment), forceAllRenderQueue = true + this._submitToCanvas(context, material, subChunk, texture, 1, true); + } + + protected override _updateBounds(worldBounds: BoundingBox): void { + const rootCanvas = this._getRootCanvas(); + if (this.sprite && rootCanvas) { + super._updateBounds(worldBounds); + } else { + const { worldPosition } = this._transformEntity.transform; + worldBounds.min.copyFrom(worldPosition); + worldBounds.max.copyFrom(worldPosition); + } + } + + @ignoreClone + protected override _onTransformChanged(type: number): void { + this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + } +} \ No newline at end of file diff --git a/packages/ui/src/component/advanced/RectMask2D.ts b/packages/ui/src/component/advanced/RectMask2D.ts new file mode 100644 index 0000000000..7fd90c26a7 --- /dev/null +++ b/packages/ui/src/component/advanced/RectMask2D.ts @@ -0,0 +1,157 @@ +import { + Component, + DependentMode, + Entity, + Vector2, + Vector3, + Vector4, + assignmentClone, + deepClone, + dependentComponents +} from "@galacean/engine"; +import { UICanvas } from "../UICanvas"; +import { UITransform } from "../UITransform"; + +/** + * UI component that clips descendant graphics by an axis-aligned rectangle. + */ +@dependentComponents(UITransform, DependentMode.AutoAdd) +export class RectMask2D extends Component { + private static _tempRect: Vector4 = new Vector4(); + private static _tempCorner0: Vector3 = new Vector3(); + private static _tempCorner1: Vector3 = new Vector3(); + private static _tempCorner2: Vector3 = new Vector3(); + private static _tempCorner3: Vector3 = new Vector3(); + + @deepClone + private _softness: Vector2 = new Vector2(0, 0); + @assignmentClone + private _alphaClip: boolean = false; + + /** + * Soft clipping width on X/Y axis in world space. + */ + get softness(): Vector2 { + return this._softness; + } + + set softness(value: Vector2) { + const softness = this._softness; + if (softness === value) { + return; + } + if (softness.x !== value.x || softness.y !== value.y) { + softness.copyFrom(value); + this._clampSoftness(); + } + } + + /** + * Whether to enable hard clip (discard) when outside the rect. + */ + get alphaClip(): boolean { + return this._alphaClip; + } + + set alphaClip(value: boolean) { + this._alphaClip = value; + } + + /** + * @internal + */ + _getWorldRect(out: Vector4): boolean { + const transform = this.entity.transform; + const { x: width, y: height } = transform.size; + if (!width || !height) { + return false; + } + + const { x: pivotX, y: pivotY } = transform.pivot; + const left = -width * pivotX; + const right = width * (1 - pivotX); + const bottom = -height * pivotY; + const top = height * (1 - pivotY); + + const worldMatrix = transform.worldMatrix; + const corner0 = RectMask2D._tempCorner0; + const corner1 = RectMask2D._tempCorner1; + const corner2 = RectMask2D._tempCorner2; + const corner3 = RectMask2D._tempCorner3; + Vector3.transformCoordinate(corner0.set(left, bottom, 0), worldMatrix, corner0); + Vector3.transformCoordinate(corner1.set(left, top, 0), worldMatrix, corner1); + Vector3.transformCoordinate(corner2.set(right, bottom, 0), worldMatrix, corner2); + Vector3.transformCoordinate(corner3.set(right, top, 0), worldMatrix, corner3); + + const minX = Math.min(corner0.x, corner1.x, corner2.x, corner3.x); + const minY = Math.min(corner0.y, corner1.y, corner2.y, corner3.y); + const maxX = Math.max(corner0.x, corner1.x, corner2.x, corner3.x); + const maxY = Math.max(corner0.y, corner1.y, corner2.y, corner3.y); + out.set(minX, minY, maxX, maxY); + return true; + } + + /** + * @internal + */ + _containsWorldPoint(worldPoint: Vector3): boolean { + const worldRect = RectMask2D._tempRect; + if (!this._getWorldRect(worldRect)) { + return false; + } + const { x, y } = worldPoint; + return x >= worldRect.x && x <= worldRect.z && y >= worldRect.y && y <= worldRect.w; + } + + constructor(entity: Entity) { + super(entity); + this._onSoftnessChanged = this._onSoftnessChanged.bind(this); + // @ts-ignore + this._softness._onValueChanged = this._onSoftnessChanged; + } + + // @ts-ignore + override _onEnableInScene(): void { + this.entity._updateUIHierarchyVersion(UICanvas._hierarchyCounter); + } + + // @ts-ignore + override _onDisableInScene(): void { + this.entity._updateUIHierarchyVersion(UICanvas._hierarchyCounter); + } + + // @ts-ignore + override _cloneTo(target: RectMask2D, srcRoot: Entity, targetRoot: Entity): void { + // @ts-ignore + super._cloneTo(target, srcRoot, targetRoot); + + const targetSoftness = target._softness; + // @ts-ignore + targetSoftness._onValueChanged = null; + targetSoftness.copyFrom(this._softness); + target._clampSoftness(); + // @ts-ignore + targetSoftness._onValueChanged = target._onSoftnessChanged; + } + + protected override _onDestroy(): void { + // @ts-ignore + this._softness._onValueChanged = null; + this._softness = null; + super._onDestroy(); + } + + private _onSoftnessChanged(): void { + this._clampSoftness(); + } + + private _clampSoftness(): void { + const softness = this._softness; + if (softness.x < 0) { + softness.x = 0; + } + if (softness.y < 0) { + softness.y = 0; + } + } +} diff --git a/packages/ui/src/component/advanced/Text.ts b/packages/ui/src/component/advanced/Text.ts index 0eb5edeb93..c3436a4b41 100644 --- a/packages/ui/src/component/advanced/Text.ts +++ b/packages/ui/src/component/advanced/Text.ts @@ -1,359 +1,41 @@ import { BoundingBox, - CharRenderInfo, - Engine, Entity, - Font, - FontStyle, - ITextRenderer, - OverflowMode, + Material, + RenderContext, RenderQueueFlags, - RendererUpdateFlags, ShaderData, ShaderDataGroup, - ShaderProperty, - SubFont, - TextHorizontalAlignment, - TextUtils, - TextVerticalAlignment, - Texture2D, - Vector3, - assignmentClone, + TextRenderable, + TextRenderableFlags, ignoreClone } from "@galacean/engine"; import { CanvasRenderMode } from "../../enums/CanvasRenderMode"; import { RootCanvasModifyFlags } from "../UICanvas"; -import { UIRenderer, UIRendererUpdateFlags } from "../UIRenderer"; +import { UIRenderer } from "../UIRenderer"; import { UITransform, UITransformModifyFlags } from "../UITransform"; /** * UI component used to render text. */ -export class Text extends UIRenderer implements ITextRenderer { - private static _textTextureProperty = ShaderProperty.getByName("renderElement_TextTexture"); - private static _worldPositions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; - private static _charRenderInfos: CharRenderInfo[] = []; - - @ignoreClone - private _textChunks = Array(); - @ignoreClone - private _subFont: SubFont = null; - @assignmentClone - private _text: string = ""; - @ignoreClone - private _localBounds: BoundingBox = new BoundingBox(); - @assignmentClone - private _font: Font = null; - @assignmentClone - private _fontSize: number = 24; - @assignmentClone - private _fontStyle: FontStyle = FontStyle.None; - @assignmentClone - private _lineSpacing: number = 0; - @assignmentClone - private _characterSpacing: number = 0; - @assignmentClone - private _horizontalAlignment: TextHorizontalAlignment = TextHorizontalAlignment.Center; - @assignmentClone - private _verticalAlignment: TextVerticalAlignment = TextVerticalAlignment.Center; - @assignmentClone - private _enableWrapping: boolean = false; - @assignmentClone - private _overflowMode: OverflowMode = OverflowMode.Overflow; - - /** - * Rendering string for the Text. - */ - get text(): string { - return this._text; - } - - set text(value: string) { - value = value || ""; - if (this._text !== value) { - this._text = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * The font of the Text. - */ - get font(): Font { - return this._font; - } - - set font(value: Font) { - const lastFont = this._font; - if (lastFont !== value) { - lastFont && this._addResourceReferCount(lastFont, -1); - value && this._addResourceReferCount(value, 1); - this._font = value; - this._setDirtyFlagTrue(DirtyFlag.Font); - } - } - - /** - * The font size of the Text. - */ - get fontSize(): number { - return this._fontSize; - } - - set fontSize(value: number) { - if (this._fontSize !== value) { - this._fontSize = value; - this._setDirtyFlagTrue(DirtyFlag.Font); - } - } - - /** - * The style of the font. - */ - get fontStyle(): FontStyle { - return this._fontStyle; - } - - set fontStyle(value: FontStyle) { - if (this.fontStyle !== value) { - this._fontStyle = value; - this._setDirtyFlagTrue(DirtyFlag.Font); - } - } - - /** - * The space between two lines, in em (ratio of fontSize). - */ - get lineSpacing(): number { - return this._lineSpacing; - } - - set lineSpacing(value: number) { - if (this._lineSpacing !== value) { - this._lineSpacing = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * The space between two characters, in em (ratio of fontSize). - */ - get characterSpacing(): number { - return this._characterSpacing; - } - - set characterSpacing(value: number) { - if (this._characterSpacing !== value) { - this._characterSpacing = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * The horizontal alignment. - */ - get horizontalAlignment(): TextHorizontalAlignment { - return this._horizontalAlignment; - } - - set horizontalAlignment(value: TextHorizontalAlignment) { - if (this._horizontalAlignment !== value) { - this._horizontalAlignment = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * The vertical alignment. - */ - get verticalAlignment(): TextVerticalAlignment { - return this._verticalAlignment; - } - - set verticalAlignment(value: TextVerticalAlignment) { - if (this._verticalAlignment !== value) { - this._verticalAlignment = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * Whether wrap text to next line when exceeds the width of the container. - */ - get enableWrapping(): boolean { - return this._enableWrapping; - } - - set enableWrapping(value: boolean) { - if (this._enableWrapping !== value) { - this._enableWrapping = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * The overflow mode. - */ - get overflowMode(): OverflowMode { - return this._overflowMode; - } - - set overflowMode(value: OverflowMode) { - if (this._overflowMode !== value) { - this._overflowMode = value; - this._setDirtyFlagTrue(DirtyFlag.Position); - } - } - - /** - * The mask layer the sprite renderer belongs to. - */ - get maskLayer(): number { - return this._maskLayer; - } - - set maskLayer(value: number) { - this._maskLayer = value; - } - - /** - * The bounding volume of the TextRenderer. - */ - override get bounds(): BoundingBox { - if (this._isTextNoVisible()) { - if (this._isContainDirtyFlag(RendererUpdateFlags.WorldVolume)) { - const localBounds = this._localBounds; - localBounds.min.set(0, 0, 0); - localBounds.max.set(0, 0, 0); - this._updateBounds(this._bounds); - this._setDirtyFlagFalse(RendererUpdateFlags.WorldVolume); - } - return this._bounds; - } - this._isContainDirtyFlag(DirtyFlag.SubFont) && this._resetSubFont(); - this._isContainDirtyFlag(DirtyFlag.LocalPositionBounds) && this._updateLocalData(); - this._isContainDirtyFlag(DirtyFlag.WorldPosition) && this._updatePosition(); - this._isContainDirtyFlag(RendererUpdateFlags.WorldVolume) && this._updateBounds(this._bounds); - this._setDirtyFlagFalse(DirtyFlag.Font); - - return this._bounds; - } - +export class Text extends TextRenderable(UIRenderer) { constructor(entity: Entity) { super(entity); - const { engine } = this; - // @ts-ignore - this.font = engine._textDefaultFont; + this._initTextRenderable(); this.raycastEnabled = false; - // @ts-ignore - this.setMaterial(engine._basicResources.textDefaultMaterial); - } - - /** - * @internal - */ - protected override _onDestroy(): void { - if (this._font) { - this._addResourceReferCount(this._font, -1); - this._font = null; - } - - super._onDestroy(); - - this._freeTextChunks(); - this._textChunks = null; - - this._subFont && (this._subFont = null); - } - - // @ts-ignore - override _cloneTo(target: Text): void { - // @ts-ignore - super._cloneTo(target); - target.font = this._font; - target._subFont = this._subFont; - } - - /** - * @internal - */ - _isContainDirtyFlag(type: number): boolean { - return (this._dirtyUpdateFlag & type) != 0; - } - - /** - * @internal - */ - _setDirtyFlagTrue(type: number): void { - this._dirtyUpdateFlag |= type; - } - - /** - * @internal - */ - _setDirtyFlagFalse(type: number): void { - this._dirtyUpdateFlag &= ~type; - } - - /** - * @internal - */ - _getSubFont(): SubFont { - if (!this._subFont) { - this._resetSubFont(); - } - return this._subFont; - } - - /** - * @internal - */ - _onRootCanvasModify(flag: RootCanvasModifyFlags): void { - if (flag === RootCanvasModifyFlags.ReferenceResolutionPerUnit) { - this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds); - } - } - - protected override _updateBounds(worldBounds: BoundingBox): void { - const transform = this._transformEntity.transform; - const { x: width, y: height } = transform.size; - const { x: pivotX, y: pivotY } = transform.pivot; - worldBounds.min.set(-width * pivotX, -height * pivotY, 0); - worldBounds.max.set(width * (1 - pivotX), height * (1 - pivotY), 0); - BoundingBox.transform(worldBounds, this._transformEntity.transform.worldMatrix, worldBounds); } - protected override _render(context): void { - if (this._isTextNoVisible()) { - return; - } - - if (this._isContainDirtyFlag(DirtyFlag.SubFont)) { - this._resetSubFont(); - this._setDirtyFlagFalse(DirtyFlag.SubFont); - } + // ===== Abstract implementations ===== + override _submitText(context: RenderContext, material: Material): void { const canvas = this._getRootCanvas(); - if (this._isContainDirtyFlag(DirtyFlag.LocalPositionBounds)) { - this._updateLocalData(); - this._setDirtyFlagFalse(DirtyFlag.LocalPositionBounds); - } - - if (this._isContainDirtyFlag(DirtyFlag.WorldPosition)) { - this._updatePosition(); - this._setDirtyFlagFalse(DirtyFlag.WorldPosition); - } - - if (this._isContainDirtyFlag(UIRendererUpdateFlags.Color)) { - this._updateColor(); - this._setDirtyFlagFalse(UIRendererUpdateFlags.Color); - } + if (!canvas) return; const engine = context.camera.engine; const textSubRenderElementPool = engine._textSubRenderElementPool; - const material = this.getMaterial(); const renderElement = canvas._renderElement; - const textChunks = this._textChunks; + const textChunks = this._getTextChunks(); + const textureProperty = this._getTextTextureProperty(); const isOverlay = canvas._realRenderMode === CanvasRenderMode.ScreenSpaceOverlay; for (let i = 0, n = textChunks.length; i < n; ++i) { const { subChunk, texture } = textChunks[i]; @@ -361,335 +43,61 @@ export class Text extends UIRenderer implements ITextRenderer { subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, texture, subChunk); // @ts-ignore subRenderElement.shaderData ||= new ShaderData(ShaderDataGroup.RenderElement); - subRenderElement.shaderData.setTexture(Text._textTextureProperty, texture); + subRenderElement.shaderData.setTexture(textureProperty, texture); if (isOverlay) { subRenderElement.shaderPasses = material.shader.subShaders[0].passes; subRenderElement.renderQueueFlags = RenderQueueFlags.All; } + // Set UI stencil depth for hierarchy-based masking + const stencilDepth = this._uiStencilDepth; + if (stencilDepth > 0) { + subRenderElement.uiStencilDepth = stencilDepth; + subRenderElement.uiStencilOp = 0; // test (read stencil) + } renderElement.addSubRenderElement(subRenderElement); } } - private _resetSubFont(): void { - const font = this._font; - // @ts-ignore - this._subFont = font._getSubFont(this.fontSize, this.fontStyle); - this._subFont.nativeFontString = TextUtils.getNativeFontString(font.name, this.fontSize, this.fontStyle); - } - - private _updatePosition(): void { - const e = this._transformEntity.transform.worldMatrix.elements; - - // prettier-ignore - const e0 = e[0], e1 = e[1], e2 = e[2], - e4 = e[4], e5 = e[5], e6 = e[6], - e12 = e[12], e13 = e[13], e14 = e[14]; - - const up = UIRenderer._tempVec31.set(e4, e5, e6); - const right = UIRenderer._tempVec30.set(e0, e1, e2); - - const worldPositions = Text._worldPositions; - const [worldPosition0, worldPosition1, worldPosition2, worldPosition3] = worldPositions; - const textChunks = this._textChunks; - for (let i = 0, n = textChunks.length; i < n; ++i) { - const { subChunk, charRenderInfos } = textChunks[i]; - for (let j = 0, m = charRenderInfos.length; j < m; ++j) { - const charRenderInfo = charRenderInfos[j]; - const { localPositions } = charRenderInfo; - const { x: topLeftX, y: topLeftY } = localPositions; - - // Top-Left - worldPosition0.set( - topLeftX * e0 + topLeftY * e4 + e12, - topLeftX * e1 + topLeftY * e5 + e13, - topLeftX * e2 + topLeftY * e6 + e14 - ); - - // Right offset - Vector3.scale(right, localPositions.z - topLeftX, worldPosition1); - // Top-Right - Vector3.add(worldPosition0, worldPosition1, worldPosition1); - // Up offset - Vector3.scale(up, localPositions.w - topLeftY, worldPosition2); - // Bottom-Left - Vector3.add(worldPosition0, worldPosition2, worldPosition3); - // Bottom-Right - Vector3.add(worldPosition1, worldPosition2, worldPosition2); + // ===== Override defaults ===== - const vertices = subChunk.chunk.vertices; - for (let k = 0, o = subChunk.vertexArea.start + charRenderInfo.indexInChunk * 36; k < 4; ++k, o += 9) { - worldPositions[k].copyToArray(vertices, o); - } - } - } + override _isTextHostInvisible(): boolean { + return !this._getRootCanvas(); } - private _updateColor(): void { - const { r, g, b, a } = this._color; - const finalAlpha = a * this._getGlobalAlpha(); - const textChunks = this._textChunks; - for (let i = 0, n = textChunks.length; i < n; ++i) { - const subChunk = textChunks[i].subChunk; - const vertexArea = subChunk.vertexArea; - const vertexCount = vertexArea.size / 9; - const vertices = subChunk.chunk.vertices; - for (let j = 0, o = vertexArea.start + 5; j < vertexCount; ++j, o += 9) { - vertices[o] = r; - vertices[o + 1] = g; - vertices[o + 2] = b; - vertices[o + 3] = finalAlpha; - } + // ===== Text-specific ===== + + /** + * @internal + */ + _onRootCanvasModify(flag: RootCanvasModifyFlags): void { + if (flag === RootCanvasModifyFlags.ReferenceResolutionPerUnit) { + this._setDirtyFlagTrue(TextRenderableFlags.LocalPositionBounds); } } - private _updateLocalData(): void { - // @ts-ignore - const pixelsPerResolution = Engine._pixelsPerUnit / this._getRootCanvas().referenceResolutionPerUnit; - const { min, max } = this._localBounds; - const charRenderInfos = Text._charRenderInfos; - const charFont = this._getSubFont(); - const { size, pivot } = this._transformEntity.transform; - let rendererWidth = size.x; - let rendererHeight = size.y; - const offsetWidth = rendererWidth * (0.5 - pivot.x); - const offsetHeight = rendererHeight * (0.5 - pivot.y); - const characterSpacing = this._characterSpacing * this._fontSize; - const textMetrics = this.enableWrapping - ? TextUtils.measureTextWithWrap( - this, - rendererWidth * pixelsPerResolution, - rendererHeight * pixelsPerResolution, - this._lineSpacing * this._fontSize, - characterSpacing - ) - : TextUtils.measureTextWithoutWrap( - this, - rendererHeight * pixelsPerResolution, - this._lineSpacing * this._fontSize, - characterSpacing - ); - const { height, lines, lineWidths, lineHeight, lineMaxSizes } = textMetrics; + /** + * @internal + */ + // @ts-ignore + override _cloneTo(target: Text): void { // @ts-ignore - const charRenderInfoPool = this.engine._charRenderInfoPool; - const linesLen = lines.length; - let renderElementCount = 0; - - if (linesLen > 0) { - const { horizontalAlignment } = this; - const pixelsPerUnitReciprocal = 1.0 / pixelsPerResolution; - rendererWidth *= pixelsPerResolution; - rendererHeight *= pixelsPerResolution; - const halfRendererWidth = rendererWidth * 0.5; - const halfLineHeight = lineHeight * 0.5; - - let startY = 0; - const topDiff = lineHeight * 0.5 - lineMaxSizes[0].ascent; - const bottomDiff = lineHeight * 0.5 - lineMaxSizes[linesLen - 1].descent - 1; - switch (this.verticalAlignment) { - case TextVerticalAlignment.Top: - startY = rendererHeight * 0.5 - halfLineHeight + topDiff; - break; - case TextVerticalAlignment.Center: - startY = height * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5; - break; - case TextVerticalAlignment.Bottom: - startY = height - rendererHeight * 0.5 - halfLineHeight - bottomDiff; - break; - } - - let firstLine = -1; - let minX = Number.MAX_SAFE_INTEGER; - let minY = Number.MAX_SAFE_INTEGER; - let maxX = Number.MIN_SAFE_INTEGER; - let maxY = Number.MIN_SAFE_INTEGER; - for (let i = 0; i < linesLen; ++i) { - const lineWidth = lineWidths[i]; - if (lineWidth > 0) { - const line = lines[i]; - let startX = 0; - let firstRow = -1; - if (firstLine < 0) { - firstLine = i; - } - switch (horizontalAlignment) { - case TextHorizontalAlignment.Left: - startX = -halfRendererWidth; - break; - case TextHorizontalAlignment.Center: - startX = -lineWidth * 0.5; - break; - case TextHorizontalAlignment.Right: - startX = halfRendererWidth - lineWidth; - break; - } - for (let j = 0, n = line.length; j < n; ++j) { - const char = line[j]; - const charInfo = charFont._getCharInfo(char); - if (charInfo.h > 0) { - firstRow < 0 && (firstRow = j); - const charRenderInfo = (charRenderInfos[renderElementCount++] = charRenderInfoPool.get()); - const { localPositions } = charRenderInfo; - charRenderInfo.texture = charFont._getTextureByIndex(charInfo.index); - charRenderInfo.uvs = charInfo.uvs; - const { w, ascent, descent } = charInfo; - const left = (startX + offsetWidth) * pixelsPerUnitReciprocal; - const right = (startX + w + offsetWidth) * pixelsPerUnitReciprocal; - const top = (startY + ascent + offsetHeight) * pixelsPerUnitReciprocal; - const bottom = (startY - descent + offsetHeight) * pixelsPerUnitReciprocal; - localPositions.set(left, top, right, bottom); - i === firstLine && (maxY = Math.max(maxY, top)); - minY = Math.min(minY, bottom); - j === firstRow && (minX = Math.min(minX, left)); - maxX = Math.max(maxX, right); - } - startX += charInfo.xAdvance + characterSpacing; - } - } - startY -= lineHeight; - } - if (firstLine < 0) { - min.set(0, 0, 0); - max.set(0, 0, 0); - } else { - min.set(minX, minY, 0); - max.set(maxX, maxY, 0); - } - } else { - min.set(0, 0, 0); - max.set(0, 0, 0); - } - - charFont._getLastIndex() > 0 && - charRenderInfos.sort((a, b) => { - return a.texture.instanceId - b.texture.instanceId; - }); - - this._freeTextChunks(); - - if (renderElementCount === 0) { - return; - } - - const textChunks = this._textChunks; - let curTextChunk = new TextChunk(); - textChunks.push(curTextChunk); - - const chunkMaxVertexCount = this._getChunkManager().maxVertexCount; - const curCharRenderInfo = charRenderInfos[0]; - let curTexture = curCharRenderInfo.texture; - curTextChunk.texture = curTexture; - let curCharInfos = curTextChunk.charRenderInfos; - curCharInfos.push(curCharRenderInfo); - - for (let i = 1; i < renderElementCount; ++i) { - const charRenderInfo = charRenderInfos[i]; - const texture = charRenderInfo.texture; - if (curTexture !== texture || curCharInfos.length * 4 + 4 > chunkMaxVertexCount) { - this._buildChunk(curTextChunk, curCharInfos.length); + super._cloneTo(target); + } - curTextChunk = new TextChunk(); - textChunks.push(curTextChunk); - curTexture = texture; - curTextChunk.texture = texture; - curCharInfos = curTextChunk.charRenderInfos; - } - curCharInfos.push(charRenderInfo); - } - const charLength = curCharInfos.length; - if (charLength > 0) { - this._buildChunk(curTextChunk, charLength); - } - charRenderInfos.length = 0; + protected override _updateBounds(worldBounds: BoundingBox): void { + const transform = this._transformEntity.transform; + const { x: width, y: height } = transform.size; + const { x: pivotX, y: pivotY } = transform.pivot; + worldBounds.min.set(-width * pivotX, -height * pivotY, 0); + worldBounds.max.set(width * (1 - pivotX), height * (1 - pivotY), 0); + BoundingBox.transform(worldBounds, this._transformEntity.transform.worldMatrix, worldBounds); } @ignoreClone protected override _onTransformChanged(type: number): void { if (type & UITransformModifyFlags.Size || type & UITransformModifyFlags.Pivot) { - this._dirtyUpdateFlag |= DirtyFlag.LocalPositionBounds; + this._setDirtyFlagTrue(TextRenderableFlags.LocalPositionBounds); } super._onTransformChanged(type); - this._setDirtyFlagTrue(DirtyFlag.WorldPosition); - } - - private _isTextNoVisible(): boolean { - const size = (this._transformEntity.transform).size; - return ( - !this._font || - this._text === "" || - this._fontSize === 0 || - (this.enableWrapping && size.x <= 0) || - (this.overflowMode === OverflowMode.Truncate && size.y <= 0) || - !this._getRootCanvas() - ); - } - - private _buildChunk(textChunk: TextChunk, count: number) { - const { r, g, b, a } = this.color; - const finalAlpha = a * this._getGlobalAlpha(); - const tempIndices = CharRenderInfo.triangles; - const tempIndicesLength = tempIndices.length; - const subChunk = (textChunk.subChunk = this._getChunkManager().allocateSubChunk(count * 4)); - const vertices = subChunk.chunk.vertices; - const indices = (subChunk.indices = []); - const charRenderInfos = textChunk.charRenderInfos; - for (let i = 0, ii = 0, io = 0, vo = subChunk.vertexArea.start + 3; i < count; ++i, io += 4) { - const charRenderInfo = charRenderInfos[i]; - charRenderInfo.indexInChunk = i; - - // Set indices - for (let j = 0; j < tempIndicesLength; ++j) { - indices[ii++] = tempIndices[j] + io; - } - - // Set uv and color for vertices - for (let j = 0; j < 4; ++j, vo += 9) { - const uv = charRenderInfo.uvs[j]; - uv.copyToArray(vertices, vo); - vertices[vo + 2] = r; - vertices[vo + 3] = g; - vertices[vo + 4] = b; - vertices[vo + 5] = finalAlpha; - } - } - - return subChunk; - } - - private _freeTextChunks(): void { - const textChunks = this._textChunks; - // @ts-ignore - const charRenderInfoPool = this.engine._charRenderInfoPool; - const manager = this._getChunkManager(); - for (let i = 0, n = textChunks.length; i < n; ++i) { - const textChunk = textChunks[i]; - const { charRenderInfos } = textChunk; - for (let j = 0, m = charRenderInfos.length; j < m; ++j) { - charRenderInfoPool.return(charRenderInfos[j]); - } - charRenderInfos.length = 0; - manager.freeSubChunk(textChunk.subChunk); - textChunk.subChunk = null; - textChunk.texture = null; - } - textChunks.length = 0; } -} - -class TextChunk { - charRenderInfos = new Array(); - texture: Texture2D; - subChunk; -} - -/** - * @remarks Extends `UIRendererUpdateFlags`. - */ -enum DirtyFlag { - SubFont = 0x4, - LocalPositionBounds = 0x8, - WorldPosition = 0x10, - - // LocalPositionBounds | WorldPosition | WorldVolume - Position = 0x19, - Font = SubFont | Position -} +} \ No newline at end of file diff --git a/packages/ui/src/component/index.ts b/packages/ui/src/component/index.ts index 1f89431265..807e96c4e1 100644 --- a/packages/ui/src/component/index.ts +++ b/packages/ui/src/component/index.ts @@ -4,6 +4,8 @@ export { UIRenderer } from "./UIRenderer"; export { UITransform } from "./UITransform"; export { Button } from "./advanced/Button"; export { Image } from "./advanced/Image"; +export { Mask } from "./advanced/Mask"; +export { RectMask2D } from "./advanced/RectMask2D"; export { Text } from "./advanced/Text"; export { ColorTransition } from "./interactive/transition/ColorTransition"; export { ScaleTransition } from "./interactive/transition/ScaleTransition"; diff --git a/packages/ui/src/shader/uiDefault.fs.glsl b/packages/ui/src/shader/uiDefault.fs.glsl index e4028405de..31d8465895 100644 --- a/packages/ui/src/shader/uiDefault.fs.glsl +++ b/packages/ui/src/shader/uiDefault.fs.glsl @@ -1,12 +1,42 @@ #include uniform sampler2D renderer_UITexture; +uniform vec4 renderer_UIRectClipRect; +uniform float renderer_UIRectClipEnabled; +uniform vec4 renderer_UIRectClipSoftness; +uniform float renderer_UIRectClipHardClip; varying vec2 v_uv; varying vec4 v_color; +varying vec2 v_worldPosition; + +float getUIRectClipAlpha() +{ + vec4 edgeDistance = vec4( + v_worldPosition.x - renderer_UIRectClipRect.x, + v_worldPosition.y - renderer_UIRectClipRect.y, + renderer_UIRectClipRect.z - v_worldPosition.x, + renderer_UIRectClipRect.w - v_worldPosition.y + ); + vec4 hardClipFactor = step(vec4(0.0), edgeDistance); + vec4 softness = max(renderer_UIRectClipSoftness, vec4(1e-5)); + vec4 softClipFactor = clamp(edgeDistance / softness, 0.0, 1.0); + vec4 useSoftness = step(vec4(1e-5), renderer_UIRectClipSoftness); + vec4 clipFactor = mix(hardClipFactor, softClipFactor, useSoftness); + return clipFactor.x * clipFactor.y * clipFactor.z * clipFactor.w; +} void main() { + float rectClipAlpha = 1.0; + if (renderer_UIRectClipEnabled > 0.5) { + rectClipAlpha = getUIRectClipAlpha(); + } + vec4 baseColor = texture2DSRGB(renderer_UITexture, v_uv); vec4 finalColor = baseColor * v_color; + finalColor.a *= rectClipAlpha; + if (renderer_UIRectClipEnabled > 0.5 && renderer_UIRectClipHardClip > 0.5 && finalColor.a < 0.001) { + discard; + } #ifdef ENGINE_SHOULD_SRGB_CORRECT finalColor = outputSRGBCorrection(finalColor); #endif diff --git a/packages/ui/src/shader/uiDefault.vs.glsl b/packages/ui/src/shader/uiDefault.vs.glsl index 2a6b45be4e..52345d9abf 100644 --- a/packages/ui/src/shader/uiDefault.vs.glsl +++ b/packages/ui/src/shader/uiDefault.vs.glsl @@ -1,4 +1,5 @@ uniform mat4 renderer_MVPMat; +uniform mat4 renderer_ModelMat; attribute vec3 POSITION; attribute vec2 TEXCOORD_0; @@ -6,10 +7,12 @@ attribute vec4 COLOR_0; varying vec2 v_uv; varying vec4 v_color; +varying vec2 v_worldPosition; void main() { gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); v_uv = TEXCOORD_0; v_color = COLOR_0; + v_worldPosition = POSITION.xy; } diff --git a/tests/src/core/SpriteMask.test.ts b/tests/src/core/SpriteMask.test.ts index 0e671e6869..7b9d31229f 100644 --- a/tests/src/core/SpriteMask.test.ts +++ b/tests/src/core/SpriteMask.test.ts @@ -1,4 +1,11 @@ -import { RendererUpdateFlags, Sprite, SpriteMask, SpriteMaskLayer, Texture2D } from "@galacean/engine-core"; +import { + RendererUpdateFlags, + Sprite, + SpriteMask, + SpriteMaskLayer, + SpriteRenderableFlags, + Texture2D +} from "@galacean/engine-core"; import { Rect, Vector2, Vector3, Vector4 } from "@galacean/engine-math"; import { WebGLEngine } from "@galacean/engine-rhi-webgl"; import { beforeEach, describe, expect, it } from "vitest"; @@ -124,28 +131,28 @@ describe("SpriteMask", async () => { expect(!!(spriteMask._dirtyUpdateFlag & RendererUpdateFlags.WorldVolume)).to.eq(true); // @ts-ignore - spriteMask._dirtyUpdateFlag &= ~SpriteMaskUpdateFlags.WorldVolumeAndUV; + spriteMask._dirtyUpdateFlag &= ~SpriteRenderableFlags.WorldVolumeAndUV; sprite.region = new Rect(); // @ts-ignore - expect(!!(spriteMask._dirtyUpdateFlag & SpriteMaskUpdateFlags.WorldVolumeAndUV)).to.eq(true); + expect(!!(spriteMask._dirtyUpdateFlag & SpriteRenderableFlags.WorldVolumeAndUV)).to.eq(true); // @ts-ignore - spriteMask._dirtyUpdateFlag &= ~SpriteMaskUpdateFlags.WorldVolumeAndUV; + spriteMask._dirtyUpdateFlag &= ~SpriteRenderableFlags.WorldVolumeAndUV; sprite.atlasRegionOffset = new Vector4(); // @ts-ignore - expect(!!(spriteMask._dirtyUpdateFlag & SpriteMaskUpdateFlags.WorldVolumeAndUV)).to.eq(true); + expect(!!(spriteMask._dirtyUpdateFlag & SpriteRenderableFlags.WorldVolumeAndUV)).to.eq(true); // @ts-ignore - spriteMask._dirtyUpdateFlag &= ~SpriteMaskUpdateFlags.UV; + spriteMask._dirtyUpdateFlag &= ~SpriteRenderableFlags.UV; sprite.atlasRegion = new Rect(); // @ts-ignore - expect(!!(spriteMask._dirtyUpdateFlag & SpriteMaskUpdateFlags.UV)).to.eq(true); + expect(!!(spriteMask._dirtyUpdateFlag & SpriteRenderableFlags.UV)).to.eq(true); // @ts-ignore - spriteMask._dirtyUpdateFlag &= ~SpriteMaskUpdateFlags.WorldVolumeAndUV; + spriteMask._dirtyUpdateFlag &= ~SpriteRenderableFlags.WorldVolumeAndUV; sprite.pivot = new Vector2(0.3, 0.2); // @ts-ignore - expect(!!(spriteMask._dirtyUpdateFlag & SpriteMaskUpdateFlags.WorldVolumeAndUV)).to.eq(true); + expect(!!(spriteMask._dirtyUpdateFlag & SpriteRenderableFlags.WorldVolumeAndUV)).to.eq(true); }); it("clone", () => { @@ -176,7 +183,7 @@ describe("SpriteMask", async () => { // @ts-ignore spriteMask._render(context); // @ts-ignore - const subChunk = spriteMask._subChunk; + const subChunk = spriteMask._spriteData.subChunk; const vertices = subChunk.chunk.vertices; const positions: Array = []; const uvs: Array = []; @@ -222,17 +229,3 @@ describe("SpriteMask", async () => { expect(spriteMask.bounds.max).to.deep.eq(new Vector3(0.5, 1, 0)); }); }); - -/** - * @remarks Extends `RendererUpdateFlags`. - */ -enum SpriteMaskUpdateFlags { - /** UV. */ - UV = 0x2, - /** Automatic Size. */ - AutomaticSize = 0x4, - /** WorldVolume and UV. */ - WorldVolumeAndUV = 0x3, - /** All. */ - All = 0x7 -} diff --git a/tests/src/core/SpriteRenderer.test.ts b/tests/src/core/SpriteRenderer.test.ts index bbb4d98266..e7ea16c73e 100644 --- a/tests/src/core/SpriteRenderer.test.ts +++ b/tests/src/core/SpriteRenderer.test.ts @@ -189,7 +189,7 @@ describe("SpriteRenderer", async () => { spriteRenderer.width = 4; spriteRenderer.height = 5; // @ts-ignore - const subChunk = spriteRenderer._subChunk; + const subChunk = spriteRenderer._spriteData.subChunk; const vertices = subChunk.chunk.vertices; const positions: Array = []; const uvs: Array = []; @@ -261,7 +261,7 @@ describe("SpriteRenderer", async () => { spriteRenderer.sprite = sprite; spriteRenderer.drawMode = SpriteDrawMode.Sliced; // @ts-ignore - const subChunk = spriteRenderer._subChunk; + const subChunk = spriteRenderer._spriteData.subChunk; const vertices = subChunk.chunk.vertices; const positions: Array = []; const uvs: Array = []; @@ -365,7 +365,7 @@ describe("SpriteRenderer", async () => { spriteRenderer.sprite = sprite; spriteRenderer.drawMode = SpriteDrawMode.Tiled; // @ts-ignore - const subChunk = spriteRenderer._subChunk; + const subChunk = spriteRenderer._spriteData.subChunk; const vertices = subChunk.chunk.vertices; const positions: Array = []; const uvs: Array = []; @@ -1521,7 +1521,7 @@ describe("SpriteRenderer", async () => { // @ts-ignore expect(spriteRenderer._assembler).to.eq(null); // @ts-ignore - expect(spriteRenderer._subChunk).to.eq(null); + expect(spriteRenderer._spriteData.subChunk).to.eq(null); }); it("_render", () => { @@ -1532,7 +1532,7 @@ describe("SpriteRenderer", async () => { // @ts-ignore spriteRenderer._render(context); // @ts-ignore - const subChunk = spriteRenderer._subChunk; + const subChunk = spriteRenderer._spriteData.subChunk; const vertices = subChunk.chunk.vertices; const positions: Array = []; const uvs: Array = []; @@ -1584,17 +1584,15 @@ describe("SpriteRenderer", async () => { * @remarks Extends `RendererUpdateFlags`. */ enum SpriteRendererUpdateFlags { - /** UV. */ - UV = 0x2, /** Color. */ - Color = 0x4, - /** Automatic Size. */ - AutomaticSize = 0x8, + Color = 0x2, + /** UV. */ + UV = 0x4, /** WorldVolume and UV. */ - WorldVolumeAndUV = 0x3, + WorldVolumeAndUV = 0x5, /** WorldVolume, UV and Color. */ WorldVolumeUVAndColor = 0x7, /** All. */ - All = 0xf + All = 0x7 }