diff --git a/packages/core/src/2d/SpriteRenderer.ts b/packages/core/src/2d/SpriteRenderer.ts deleted file mode 100644 index d46d1e29f7..0000000000 --- a/packages/core/src/2d/SpriteRenderer.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { Matrix, Quaternion, Vector3, Vector4 } from "@oasis-engine/math"; -import { Logger } from "../base/Logger"; -import { Camera } from "../Camera"; -import { Entity } from "../Entity"; -import { Renderer } from "../Renderer"; -import { Texture2D } from "../texture/Texture2D"; - -interface IUvRect { - u: number; - v: number; - width: number; - height: number; -} - -interface IRect { - x: number; - y: number; - width: number; - height: number; -} - -interface IPositionQuad { - leftTop: Vector3; - leftBottom: Vector3; - rightTop: Vector3; - rightBottom: Vector3; -} - -/** - * Sprite renderer. - * @class - */ -export class SpriteRenderer extends Renderer { - private static _tempVec40: Vector4 = new Vector4(); - private static _tempVec41: Vector4 = new Vector4(); - private static _tempVec42: Vector4 = new Vector4(); - private static _tempVec43: Vector4 = new Vector4(); - - private _uvRect: IUvRect; - private _worldSize: number[] = []; - private _positionQuad: IPositionQuad; - private _rotationAngle: number = 0; - private _anchor: number[]; - protected _texture: Texture2D; - protected _rect: IRect; - private _worldSizeFactor: number; - - /** - * Render mode: 2D or 3D, default is 2D. - */ - renderMode: string = "2D"; - /** - * Rendering color for the Sprite graphic. - */ - public tintColor: Vector4 = new Vector4(1, 1, 1, 1); - public transformMatrix: Matrix; - - /** - * Constructor of SpriteRenderer. - * @param {Entity} entity - */ - constructor(entity: Entity) { - super(entity); - this._worldSizeFactor = 100; - - this.setTexture(undefined); - this.setRect(undefined); - this.setAnchor(undefined); - this.setUvRect(); - this.setWorldSize(); - - this._positionQuad = { - leftTop: new Vector3(), - leftBottom: new Vector3(), - rightTop: new Vector3(), - rightBottom: new Vector3() - }; - } - - set texture(v) { - this.setTexture(v); - this.setRect(); - this.setUvRect(); - this.setWorldSize(); - } - - get texture() { - return this._texture; - } - - set anchor(v) { - this._anchor = v || [0.5, 0.5]; - } - - get anchor() { - return this._anchor; - } - - set rect(v) { - this.setRect(v); - this.setUvRect(); - this.setWorldSize(); - } - - get rect() { - return this._rect; - } - - protected setTexture(texture) { - // TODO: compatible resource - if (texture && texture.asset) { - texture = texture.asset; - } - - this._texture = texture; - } - - /** - * Angle of rotation. - * @member {Vector4} - */ - get rotationAngle() { - return this._rotationAngle; - } - - set rotationAngle(v) { - this._rotationAngle = v; - } - - protected setRect(rect?) { - let rectObject; - try { - if (rect) { - rectObject = JSON.parse(rect); - } - } catch (error) { - Logger.warn("Rect is not valid JSON format"); - } - - this._rect = rect || { - x: 0, - y: 0, - width: this._texture?.width ?? 0, - height: this._texture?.height ?? 0 - }; - } - - protected setAnchor(anchor) { - this._anchor = anchor || [0.5, 0.5]; - } - - protected setWorldSize() { - const { _worldSizeFactor } = this; - this._worldSize = [this._rect.width / _worldSizeFactor, this._rect.height / _worldSizeFactor]; - } - - protected setUvRect() { - let w, h; - - if (this._texture) { - w = this._texture.width; - h = this._texture.height; - } else { - w = this._rect.width; - h = this._rect.height; - } - this._uvRect = { - u: this._rect.x / w, - v: this._rect.y / h, - width: this._rect.width / w, - height: this._rect.height / h - }; - } - - /** - * @internal - */ - _render(camera: Camera): void { - this._updatePositionQuad(camera); - this._transformByMatrix(); - camera._renderPipeline.pushSprite( - this, - this._positionQuad, - this._uvRect, - this.tintColor, - this.texture, - this.renderMode, - camera - ); - } - - _transformByMatrix() { - if (!this.transformMatrix) return; - const matrix = this.transformMatrix; - - let temp: Vector3 = this._positionQuad.leftTop; - const leftTop: Vector4 = SpriteRenderer._tempVec40; - leftTop.setValue(temp.x, temp.y, temp.z, 1); - - temp = this._positionQuad.leftBottom; - const leftBottom: Vector4 = SpriteRenderer._tempVec41; - leftBottom.setValue(temp.x, temp.y, temp.z, 1); - - temp = this._positionQuad.rightTop; - const rightTop: Vector4 = SpriteRenderer._tempVec42; - rightTop.setValue(temp.x, temp.y, temp.z, 1); - - temp = this._positionQuad.rightBottom; - const rightBottom: Vector4 = SpriteRenderer._tempVec43; - rightBottom.setValue(temp.x, temp.y, temp.z, 1); - - Vector4.transform(leftTop, matrix, leftTop); - Vector4.transform(leftBottom, matrix, leftBottom); - Vector4.transform(rightTop, matrix, rightTop); - Vector4.transform(rightBottom, matrix, rightBottom); - - this._positionQuad.leftTop.setValue(leftTop.x, leftTop.y, leftTop.z); - this._positionQuad.leftBottom.setValue(leftBottom.x, leftBottom.y, leftBottom.z); - this._positionQuad.rightTop.setValue(rightTop.x, rightTop.y, rightTop.z); - this._positionQuad.rightBottom.setValue(rightBottom.x, rightBottom.y, rightBottom.z); - } - - /** - * Update position. - * @param {Camera} camera - * @private - */ - _updatePositionQuad(camera: Camera) { - if (this.renderMode === "2D") { - const m = camera.viewMatrix.elements; - const vx = new Vector3(m[0], m[4], m[8]); - const vy = new Vector3(m[1], m[5], m[9]); - //-- center pos - const c: Vector3 = this.entity.worldPosition.clone(); - const s = this._worldSize; - const ns = this.entity.scale; - - vx.scale(s[0] * ns.x); - vy.scale(s[1] * ns.y); - - if (this._rotationAngle !== 0) { - const vz = new Vector3(m[2], m[6], m[10]); - - const rotation: Quaternion = new Quaternion(); - Quaternion.rotationAxisAngle(vz, this._rotationAngle, rotation); - - Vector3.transformByQuat(vx, rotation, vx); - Vector3.transformByQuat(vy, rotation, vy); - } - - const cx: Vector3 = new Vector3(); - const cy: Vector3 = new Vector3(); - Vector3.scale(vx, (this.anchor[0] - 0.5) * 2, cx); - Vector3.scale(vy, (this.anchor[1] - 0.5) * 2, cy); - - c.subtract(cx).add(cy); - - //-- quad pos - const leftTop: Vector3 = this._positionQuad.leftTop; - Vector3.subtract(c, vx, leftTop); - leftTop.add(vy); - - const leftBottom: Vector3 = this._positionQuad.leftBottom; - Vector3.subtract(c, vx, leftBottom); - leftBottom.subtract(vy); - - const rightBottom: Vector3 = this._positionQuad.rightBottom; - Vector3.add(c, vx, rightBottom); - rightBottom.subtract(vy); - - const rightTop: Vector3 = this._positionQuad.rightTop; - Vector3.add(c, vx, rightTop); - rightTop.add(vy); - } else { - // TODO: 3D - } - } -} diff --git a/packages/core/src/2d/index.ts b/packages/core/src/2d/index.ts index 931067bf0e..4d2cecdeb1 100644 --- a/packages/core/src/2d/index.ts +++ b/packages/core/src/2d/index.ts @@ -1 +1 @@ -export { SpriteRenderer } from "./SpriteRenderer"; +export * from "./sprite/index"; diff --git a/packages/core/src/2d/sprite/Sprite.ts b/packages/core/src/2d/sprite/Sprite.ts new file mode 100644 index 0000000000..6d4724e1be --- /dev/null +++ b/packages/core/src/2d/sprite/Sprite.ts @@ -0,0 +1,229 @@ +import { Rect, Vector2 } from "@oasis-engine/math"; +import { RefObject } from "../../asset/RefObject"; +import { Engine } from "../../Engine"; +import { Texture2D } from "../../texture"; + +/** + * 2D sprite. + */ +export class Sprite extends RefObject { + private static _tempVec2: Vector2 = new Vector2(); + + /** @internal */ + _triangles: number[] = []; + /** @internal */ + _uv: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; + /** @internal */ + _positions: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; + + private _texture: Texture2D = null; + private _atlasRect: Rect = new Rect(); + private _pivot: Vector2 = new Vector2(); + private _rect: Rect = new Rect(); + private _pixelsPerUnit: number = 100; + private _dirtyFlag: number = DirtyFlag.all; + + /** + * The reference to the used texture. + */ + get texture(): Texture2D { + return this._texture; + } + + set texture(value: Texture2D) { + if (this._texture !== value) { + this._texture = value; + } + } + + /** + * The rectangle of the original texture on its atlas texture. + */ + get atlasRect(): Rect { + return this._atlasRect; + } + + set atlasRect(value: Rect) { + if (this._atlasRect !== value) { + this._atlasRect.x = value.x; + this._atlasRect.y = value.y; + this._atlasRect.width = value.width; + this._atlasRect.height = value.width; + } + } + + /** + * Location of the sprite's center point in the rect on the original texture, specified in pixels. + */ + get pivot(): Vector2 { + return this._pivot; + } + + set pivot(value: Vector2) { + if (this._pivot !== value) { + value.cloneTo(this._pivot); + this._setDirtyFlagTrue(DirtyFlag.positions); + } + } + + /** + * Location of the sprite on the original texture, specified in pixels. + */ + get rect(): Rect { + return this._rect; + } + + set rect(value: Rect) { + if (this._rect !== value) { + this._rect.x = value.x; + this._rect.y = value.y; + this._rect.width = value.width; + this._rect.height = value.width; + this._setDirtyFlagTrue(DirtyFlag.positions); + } + } + + /** + * The number of pixels in the sprite that correspond to one unit in world space. + */ + get pixelsPerUnit(): number { + return this._pixelsPerUnit; + } + + set pixelsPerUnit(value: number) { + if (this._pixelsPerUnit !== value) { + this._pixelsPerUnit = value; + this._setDirtyFlagTrue(DirtyFlag.positions); + } + } + + /** + * Constructor a sprite. + * @param engine - Engine to which the sprite belongs + * @param texture - Texture from which to obtain the sprite + * @param rect - Rectangular section of the texture to use for the sprite + * @param normalizedPivot - Sprite's normalized pivot point relative to its graphic rectangle + * @param pixelsPerUnit - The number of pixels in the sprite that correspond to one unit in world space + */ + constructor( + engine: Engine, + texture: Texture2D, + rect: Rect = null, + normalizedPivot: Vector2 = null, + pixelsPerUnit: number = 100 + ) { + super(engine); + + const rectangle = this.rect; + if (rect) { + rect.cloneTo(rectangle); + if (rectangle.x + rectangle.width > texture.width || rectangle.y + rectangle.height > texture.height) { + throw new Error("rect out of range"); + } + } else { + rectangle.setValue(0, 0, texture.width, texture.height); + } + + if (normalizedPivot) { + this.pivot.setValue(normalizedPivot.x * rectangle.width, normalizedPivot.y * rectangle.height); + } else { + this.pivot.setValue(0.5 * rectangle.width, 0.5 * rectangle.height); + } + + rectangle.cloneTo(this.atlasRect); + this.texture = texture; + this.pixelsPerUnit = pixelsPerUnit; + } + + /** + * @override + */ + _onDestroy(): void { + if (this._texture) { + this._texture = null; + } + } + + /** + * Update mesh. + */ + private _updateMesh(): void { + const { _pixelsPerUnit, _pivot, _positions, _uv, _triangles } = this; + const unitPivot = Sprite._tempVec2; + + if (this._isContainDirtyFlag(DirtyFlag.positions)) { + const { width, height } = this._rect; + + const pixelsPerUnitReciprocal = 1.0 / _pixelsPerUnit; + // Get the pivot coordinate in 3D space. + Vector2.scale(_pivot, pixelsPerUnitReciprocal, unitPivot); + // Get the width and height in 3D space. + const unitWidth = width * pixelsPerUnitReciprocal; + const unitHeight = height * pixelsPerUnitReciprocal; + + // Top-left. + _positions[0].setValue(-unitPivot.x, unitHeight - unitPivot.y); + // Top-right. + _positions[1].setValue(unitWidth - unitPivot.x, unitWidth - unitPivot.y); + // Bottom-right. + _positions[2].setValue(unitWidth - unitPivot.x, -unitPivot.y); + // Bottom-left. + _positions[3].setValue(-unitPivot.x, -unitPivot.y); + } + + if (this._isContainDirtyFlag(DirtyFlag.uv)) { + // Top-left. + _uv[0].setValue(0, 0); + // Top-right. + _uv[1].setValue(1, 0); + // Bottom-right. + _uv[2].setValue(1, 1); + // Bottom-left. + _uv[3].setValue(0, 1); + } + + if (this._isContainDirtyFlag(DirtyFlag.triangles)) { + _triangles[0] = 0; + _triangles[1] = 2; + _triangles[2] = 1; + _triangles[3] = 2; + _triangles[4] = 0; + _triangles[5] = 3; + } + } + + /** + * @internal + * Update mesh data of the sprite. + * @returns True if the data is refreshed, false otherwise. + */ + _updateMeshData(): boolean { + if (this._isContainDirtyFlag(DirtyFlag.all)) { + this._updateMesh(); + this._setDirtyFlagFalse(DirtyFlag.all); + + return true; + } + + return false; + } + + private _isContainDirtyFlag(type: number): boolean { + return (this._dirtyFlag & type) != 0; + } + + private _setDirtyFlagTrue(type: number): void { + this._dirtyFlag |= type; + } + + private _setDirtyFlagFalse(type: number): void { + this._dirtyFlag &= ~type; + } +} + +enum DirtyFlag { + positions = 0x1, + uv = 0x2, + triangles = 0x4, + all = 0x7 +} diff --git a/packages/rhi-webgl/src/GLSpriteMaterial.ts b/packages/core/src/2d/sprite/SpriteMaterial.ts similarity index 59% rename from packages/rhi-webgl/src/GLSpriteMaterial.ts rename to packages/core/src/2d/sprite/SpriteMaterial.ts index 45b42764f8..b2f97394d2 100644 --- a/packages/rhi-webgl/src/GLSpriteMaterial.ts +++ b/packages/core/src/2d/sprite/SpriteMaterial.ts @@ -1,25 +1,24 @@ "use strict"; -import { Shader } from "@oasis-engine/core"; +import { Shader } from "../../shader"; const spriteVertShader = ` precision highp float; -uniform mat4 matProjection; -uniform mat4 matView; +uniform mat4 u_VPMat; -attribute vec3 a_pos; -attribute vec2 a_uv; -attribute vec4 a_color; +attribute vec3 POSITION; +attribute vec2 TEXCOORD_0; +attribute vec4 COLOR_0; varying vec2 v_uv; varying vec4 v_color; void main() { - gl_Position = matProjection * matView * vec4(a_pos,1.0); - v_uv = a_uv; - v_color = a_color; + gl_Position = u_VPMat * vec4(POSITION, 1.0); + v_uv = TEXCOORD_0; + v_color = COLOR_0; } `; @@ -27,14 +26,14 @@ const spriteFragmentShader = ` precision mediump float; precision mediump int; -uniform sampler2D s_diffuse; +uniform sampler2D u_texture; varying vec2 v_uv; varying vec4 v_color; void main() { // Only use the Alpha of the texture as a mask, so that the tint color can still be controlled to fade out. - vec4 baseColor = texture2D(s_diffuse, v_uv); + vec4 baseColor = texture2D(u_texture, v_uv); gl_FragColor = baseColor * v_color; } `; diff --git a/packages/core/src/2d/sprite/SpriteRenderer.ts b/packages/core/src/2d/sprite/SpriteRenderer.ts new file mode 100644 index 0000000000..40617f350f --- /dev/null +++ b/packages/core/src/2d/sprite/SpriteRenderer.ts @@ -0,0 +1,209 @@ +import { Color, Vector3 } from "@oasis-engine/math"; +import { Camera } from "../../Camera"; +import { ignoreClone } from "../../clone/CloneManager"; +import { Entity } from "../../Entity"; +import { Material, RenderQueueType } from "../../material"; +import { Renderer } from "../../Renderer"; +import { SpriteElement } from "../../RenderPipeline/SpriteElement"; +import { BlendFactor, BlendOperation, CullMode, Shader } from "../../shader"; +import { ShaderProperty } from "../../shader/ShaderProperty"; +import { UpdateFlag } from "../../UpdateFlag"; +import { Sprite } from "./Sprite"; +import "./SpriteMaterial"; + +/** + * Renders a Sprite for 2D graphics. + */ +export class SpriteRenderer extends Renderer { + private static _textureProperty: ShaderProperty = Shader.getPropertyByName("u_texture"); + private static _tempVec3: Vector3 = new Vector3(); + private static _defaultMaterial: Material = null; + + private _positions: Vector3[] = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; + private _sprite: Sprite = null; + private _color: Color = new Color(1, 1, 1, 1); + private _flipX: boolean = false; + private _flipY: boolean = false; + private _cacheFlipX: boolean = false; + private _cacheFlipY: boolean = false; + private _dirtyFlag: number = DirtyFlag.All; + @ignoreClone + private _isWorldMatrixDirty: UpdateFlag; + + /** + * The Sprite to render. + */ + get sprite(): Sprite { + return this._sprite; + } + + set sprite(value: Sprite | null) { + if (this._sprite !== value) { + this._sprite = value; + this._setDirtyFlagTrue(DirtyFlag.Sprite); + } + } + + /** + * Rendering color for the Sprite graphic. + */ + get color(): Color { + return this._color; + } + + set color(value: Color) { + if (this._color !== value) { + value.cloneTo(this._color); + } + } + + /** + * Flips the sprite on the X axis. + */ + get flipX(): boolean { + return this._flipX; + } + + set flipX(value: boolean) { + if (this._flipX !== value) { + this._flipX = value; + this._setDirtyFlagTrue(DirtyFlag.Flip); + } + } + + /** + * Flips the sprite on the Y axis. + */ + get flipY(): boolean { + return this._flipY; + } + + set flipY(value: boolean) { + if (this._flipY !== value) { + this._flipY = value; + this._setDirtyFlagTrue(DirtyFlag.Flip); + } + } + + /** + * Create a sprite renderer instance. + * @param entity - Entity to which the sprite renderer belongs + */ + constructor(entity: Entity) { + super(entity); + this._isWorldMatrixDirty = entity.transform.registerWorldChangeFlag(); + } + + /** + * @internal + */ + _render(camera: Camera): void { + const { sprite } = this; + if (!sprite) { + return; + } + + const { _positions } = this; + const { transform } = this.entity; + + // Update sprite data. + const localDirty = sprite._updateMeshData(); + + if (this._isWorldMatrixDirty.flag || localDirty || this._isContainDirtyFlag(DirtyFlag.Sprite)) { + const localPositions = sprite._positions; + const localPosZ = transform.position.z; + const localVertexPos = SpriteRenderer._tempVec3; + const worldMatrix = transform.worldMatrix; + const { flipX, flipY } = this; + + for (let i = 0, n = _positions.length; i < n; i++) { + const curVertexPos = localPositions[i]; + localVertexPos.setValue( + flipX ? -curVertexPos.x : curVertexPos.x, + flipY ? -curVertexPos.y : curVertexPos.y, + localPosZ + ); + Vector3.transformToVec3(localVertexPos, worldMatrix, _positions[i]); + } + + this._setDirtyFlagFalse(DirtyFlag.Flip); + this._setDirtyFlagFalse(DirtyFlag.Sprite); + this._isWorldMatrixDirty.flag = false; + this._cacheFlipX = flipX; + this._cacheFlipY = flipY; + } else if (this._isContainDirtyFlag(DirtyFlag.Flip)) { + const { flipX, flipY } = this; + const flipXChange = this._cacheFlipX !== flipX; + const flipYChange = this._cacheFlipY !== flipY; + + if (flipXChange || flipYChange) { + const { x, y } = transform.worldPosition; + + for (let i = 0, n = _positions.length; i < n; i++) { + const curPos = _positions[i]; + + if (flipXChange) { + curPos.x = x * 2 - curPos.x; + } + + if (flipYChange) { + curPos.y = y * 2 - curPos.y; + } + } + } + + this._setDirtyFlagFalse(DirtyFlag.Flip); + this._cacheFlipX = flipX; + this._cacheFlipY = flipY; + } + + this.shaderData.setTexture(SpriteRenderer._textureProperty, sprite.texture); + const material = this.getMaterial() || this._getDefaultMaterial(); + + const spriteElement = SpriteElement.getFromPool(); + spriteElement.setValue(this, _positions, sprite._uv, sprite._triangles, this.color, material, camera); + camera._renderPipeline.pushPrimitive(spriteElement); + } + + /** + * @internal + */ + _onDestroy(): void { + this._isWorldMatrixDirty.destroy(); + super._onDestroy(); + } + + private _isContainDirtyFlag(type: number): boolean { + return (this._dirtyFlag & type) != 0; + } + + private _setDirtyFlagTrue(type: number): void { + this._dirtyFlag |= type; + } + + private _setDirtyFlagFalse(type: number): void { + this._dirtyFlag &= ~type; + } + + private _getDefaultMaterial(): Material { + if (!SpriteRenderer._defaultMaterial) { + // Create default material + const material = (SpriteRenderer._defaultMaterial = new Material(this.scene.engine, Shader.find("Sprite"))); + const target = material.renderState.blendState.targetBlendState; + target.sourceColorBlendFactor = target.sourceAlphaBlendFactor = BlendFactor.SourceAlpha; + target.destinationColorBlendFactor = target.destinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha; + target.colorBlendOperation = target.alphaBlendOperation = BlendOperation.Add; + material.renderState.depthState.writeEnabled = false; + material.renderQueueType = RenderQueueType.Transparent; + material.renderState.rasterState.cullMode = CullMode.Off; + } + + return SpriteRenderer._defaultMaterial; + } +} + +enum DirtyFlag { + Flip = 0x1, + Sprite = 0x2, + All = 0x3 +} diff --git a/packages/core/src/2d/sprite/index.ts b/packages/core/src/2d/sprite/index.ts new file mode 100644 index 0000000000..c7ba1b272a --- /dev/null +++ b/packages/core/src/2d/sprite/index.ts @@ -0,0 +1,2 @@ +export { Sprite } from "./Sprite"; +export { SpriteRenderer } from "./SpriteRenderer"; diff --git a/packages/core/src/Engine.ts b/packages/core/src/Engine.ts index c1bd44a05e..0206d1a8c9 100644 --- a/packages/core/src/Engine.ts +++ b/packages/core/src/Engine.ts @@ -7,6 +7,7 @@ import { Entity } from "./Entity"; import { FeatureManager } from "./FeatureManager"; import { HardwareRenderer } from "./HardwareRenderer"; import { RenderElement } from "./RenderPipeline/RenderElement"; +import { SpriteElement } from "./RenderPipeline/SpriteElement"; import { Scene } from "./Scene"; import { SceneManager } from "./SceneManager"; import { Shader } from "./shader/Shader"; @@ -176,6 +177,7 @@ export class Engine extends EventDispatcher { time.tick(); RenderElement._restPool(); + SpriteElement._restPool(); engineFeatureManager.callFeatureMethod(this, "preTick", [this, this._sceneManager._activeScene]); diff --git a/packages/core/src/RenderPipeline/BasicRenderPipeline.ts b/packages/core/src/RenderPipeline/BasicRenderPipeline.ts index a415dc33de..aa5869d1c4 100644 --- a/packages/core/src/RenderPipeline/BasicRenderPipeline.ts +++ b/packages/core/src/RenderPipeline/BasicRenderPipeline.ts @@ -1,25 +1,20 @@ import { Vector4 } from "@oasis-engine/math"; -import { ClearMode } from "../base"; import { Camera } from "../Camera"; -import { Component } from "../Component"; import { Layer } from "../Layer"; import { RenderQueueType } from "../material"; import { Material } from "../material/Material"; -import { BlendFactor, BlendOperation, CullMode, Shader } from "../shader"; import { TextureCubeFace } from "../texture/enums/TextureCubeFace"; import { RenderTarget } from "../texture/RenderTarget"; import { RenderContext } from "./RenderContext"; import { RenderElement } from "./RenderElement"; import { RenderPass } from "./RenderPass"; import { RenderQueue } from "./RenderQueue"; -import { SeparateSpritePass } from "./SeparateSpritePass"; +import { SpriteElement } from "./SpriteElement"; /** * Basic render pipeline. */ export class BasicRenderPipeline { - /** @internal */ - _defaultSpriteMaterial: Material; /** @internal */ _opaqueQueue: RenderQueue; /** @internal */ @@ -31,7 +26,6 @@ export class BasicRenderPipeline { private _defaultPass: RenderPass; private _renderPassArray: Array; private _canvasDepthPass; - private _separateSpritePass; /** * Create a basic render pipeline. @@ -39,23 +33,13 @@ export class BasicRenderPipeline { */ constructor(camera: Camera) { this._camera = camera; - this._opaqueQueue = new RenderQueue(); - this._alphaTestQueue = new RenderQueue(); - this._transparentQueue = new RenderQueue(); + this._opaqueQueue = new RenderQueue(camera.engine); + this._alphaTestQueue = new RenderQueue(camera.engine); + this._transparentQueue = new RenderQueue(camera.engine); this._renderPassArray = []; this._defaultPass = new RenderPass("default", 0, null, null, 0); this.addRenderPass(this._defaultPass); - - // TODO: remove in next version. - const material = (this._defaultSpriteMaterial = new Material(camera.engine, Shader.find("Sprite"))); - const target = material.renderState.blendState.targetBlendState; - target.sourceColorBlendFactor = target.sourceAlphaBlendFactor = BlendFactor.SourceAlpha; - target.destinationColorBlendFactor = target.destinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha; - target.colorBlendOperation = target.alphaBlendOperation = BlendOperation.Add; - material.renderState.depthState.writeEnabled = false; - material.renderQueueType = RenderQueueType.Transparent; - material.renderState.rasterState.cullMode = CullMode.Off; } /** @@ -147,18 +131,6 @@ export class BasicRenderPipeline { if (this._canvasDepthPass) this._canvasDepthPass.enabled = false; - if (this._separateSpritePass && this._separateSpritePass.isUsed) { - // If the default rendertarget is not canvas, you need to draw on the canvas again to ensure that there is depth information - if (this._defaultPass.renderTarget) { - if (!this._canvasDepthPass) { - this._canvasDepthPass = new RenderPass("CanvasDepthRenderPass", 0, null, null, 0); - this._canvasDepthPass.clearMode = ClearMode.DONT_CLEAR; - this.addRenderPass(this._canvasDepthPass); - } - this._canvasDepthPass.enabled = true; - } - } - for (let i = 0, len = this._renderPassArray.length; i < len; i++) { this._drawRenderPass(this._renderPassArray[i], camera, cubeFace); } @@ -192,7 +164,7 @@ export class BasicRenderPipeline { * Push a render element to the render queue. * @param element - Render element */ - pushPrimitive(element: RenderElement) { + pushPrimitive(element: RenderElement | SpriteElement) { const renderQueueType = element.material.renderQueueType; if (renderQueueType > (RenderQueueType.Transparent + RenderQueueType.AlphaTest) >> 1) { @@ -203,18 +175,4 @@ export class BasicRenderPipeline { this._opaqueQueue.pushPrimitive(element); } } - - pushSprite(component: Component, positionQuad, uvRect, tintColor, texture, renderMode, camera: Camera) { - if ((component as any).separateDraw) { - if (!this._separateSpritePass) { - this._separateSpritePass = new SeparateSpritePass(); - this.addRenderPass(this._separateSpritePass); - } - - this._separateSpritePass.pushSprite(component, positionQuad, uvRect, tintColor, texture, renderMode, camera); - return; - } - - this._transparentQueue.pushSprite(component, positionQuad, uvRect, tintColor, texture, renderMode, camera); - } } diff --git a/packages/core/src/RenderPipeline/RenderQueue.ts b/packages/core/src/RenderPipeline/RenderQueue.ts index 48c5526d36..76e1a0ed46 100644 --- a/packages/core/src/RenderPipeline/RenderQueue.ts +++ b/packages/core/src/RenderPipeline/RenderQueue.ts @@ -1,21 +1,12 @@ import { Camera } from "../Camera"; -import { Component } from "../Component"; +import { Engine } from "../Engine"; import { Layer } from "../Layer"; import { Material } from "../material/Material"; -import { Renderer } from "../Renderer"; import { Shader } from "../shader"; import { ShaderMacroCollection } from "../shader/ShaderMacroCollection"; import { RenderElement } from "./RenderElement"; - -interface SpriteElement { - component: Renderer; - positionQuad; - uvRect; - tintColor; - texture; - renderMode; - camera; -} +import { SpriteBatcher } from "./SpriteBatcher"; +import { SpriteElement } from "./SpriteElement"; type Item = RenderElement | SpriteElement; @@ -27,80 +18,37 @@ export class RenderQueue { * @internal */ static _compareFromNearToFar(a: Item, b: Item): number { - //@todo: delete after sprite refactor - const aIsPrimitive = !!(a as RenderElement).mesh; - const bIsPrimitive = !!(b as RenderElement).mesh; - - if (aIsPrimitive && bIsPrimitive) { - const aElement: RenderElement = a; - const bElement: RenderElement = b; - const renderQueueDif = aElement.material.renderQueueType - bElement.material.renderQueueType; + const renderQueueDif = a.material.renderQueueType - b.material.renderQueueType; - if (renderQueueDif) { - return renderQueueDif; - } - - return aElement.component._distanceForSort - bElement.component._distanceForSort; - } - - if (aIsPrimitive && !bIsPrimitive) { - return -1; - } - - if (!aIsPrimitive && bIsPrimitive) { - return 1; + if (renderQueueDif) { + return renderQueueDif; } + return a.component._distanceForSort - b.component._distanceForSort; } /** * @internal */ static _compareFromFarToNear(a: Item, b: Item): number { - //@todo: delete after sprite refactor - const aIsPrimitive = !!(a as RenderElement).mesh; - const bIsPrimitive = !!(b as RenderElement).mesh; - - if (aIsPrimitive && bIsPrimitive) { - const aElement: RenderElement = a; - const bElement: RenderElement = b; - const renderQueueDif = aElement.material.renderQueueType - bElement.material.renderQueueType; - - if (renderQueueDif) { - return renderQueueDif; - } - - return bElement.component._distanceForSort - aElement.component._distanceForSort; - } + const renderQueueDif = a.material.renderQueueType - b.material.renderQueueType; - if (aIsPrimitive && !bIsPrimitive) { - return -1; - } - - if (!aIsPrimitive && bIsPrimitive) { - return 1; + if (renderQueueDif) { + return renderQueueDif; } + return b.component._distanceForSort - a.component._distanceForSort; } readonly items: Item[] = []; + private _spriteBatcher: SpriteBatcher; + + constructor(engine: Engine) { + this._spriteBatcher = new SpriteBatcher(engine); + } /** * Push a render element. */ - pushPrimitive(element: RenderElement): void { - this.items.push(element); - } - - pushSprite(component: Component, positionQuad, uvRect, tintColor, texture, renderMode, camera) { - const element: SpriteElement = { - // @ts-ignore - component, - positionQuad, - uvRect, - tintColor, - texture, - renderMode, - camera - }; + pushPrimitive(element: RenderElement | SpriteElement): void { this.items.push(element); } @@ -110,7 +58,6 @@ export class RenderQueue { return; } - const spriteMaterial = camera._renderPipeline._defaultSpriteMaterial; const { engine, scene } = camera; const renderCount = engine._renderCount; const rhi = engine._hardwareRenderer; @@ -126,7 +73,7 @@ export class RenderQueue { } if (!!(item as RenderElement).mesh) { - rhi.flushSprite(engine, spriteMaterial); + this._spriteBatcher.flush(engine); const compileMacros = Shader._compileMacros; const element = item; @@ -187,19 +134,11 @@ export class RenderQueue { rhi.drawPrimitive(element.mesh, element.subMesh, program); } else { const spirteElement = item; - rhi.drawSprite( - spriteMaterial, - spirteElement.positionQuad, - spirteElement.uvRect, - spirteElement.tintColor, - spirteElement.texture, - spirteElement.renderMode, - spirteElement.camera - ); + this._spriteBatcher.drawSprite(spirteElement); } } - rhi.flushSprite(engine, spriteMaterial); + this._spriteBatcher.flush(engine); } /** @@ -207,6 +146,7 @@ export class RenderQueue { */ clear(): void { this.items.length = 0; + this._spriteBatcher.clear(); } /** diff --git a/packages/core/src/RenderPipeline/SeparateSpritePass.ts b/packages/core/src/RenderPipeline/SeparateSpritePass.ts deleted file mode 100644 index 5db87bd7e9..0000000000 --- a/packages/core/src/RenderPipeline/SeparateSpritePass.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Vector3 } from "@oasis-engine/math"; -import { ClearMode } from "../base"; -import { Camera } from "../Camera"; -import { RenderPass } from "./RenderPass"; - -/** - * @private - */ -export class SeparateSpritePass extends RenderPass { - private _spriteItems; - - constructor(name = "SeparateSprite", priority = 10) { - super(name, priority); - - this.clearMode = ClearMode.DONT_CLEAR; - this.renderOverride = true; - - this._spriteItems = []; - } - - get isUsed() { - return this._spriteItems.length > 0; - } - - preRender() { - this.enabled = this.isUsed; - } - - render(camera) { - const rhi = camera.renderHardware; - - this._sortByDistance(camera.eyePos); - const items = this._spriteItems; - const material = camera._renderPipeline._defaultSpriteMaterial; - - for (let i = 0; i < items.length; i++) { - const item = items[i]; - rhi.drawSprite( - material, - item.positionQuad, - item.uvRect, - item.tintColor, - item.texture, - item.renderMode, - item.camera - ); - } - - items.length = 0; - } - - postRender(camera) { - if (this.enabled) { - camera.renderHardware.flushSprite(camera.engine, camera._hardwareRenderer._defaultSpriteMaterial); - } - } - - _sortByDistance(eyePos) { - if (this._spriteItems.length > 1) { - this._spriteItems = this._spriteItems.sort(function (item1, item2) { - if (item1.component.renderPriority === item2.component.renderPriority) { - const pos1 = item1.component.node.worldPosition; - const pos2 = item2.component.node.worldPosition; - - const dis = Vector3.distanceSquared(pos2, eyePos) - Vector3.distanceSquared(pos1, eyePos); - return dis; - } else { - return item1.component.renderPriority - item2.component.renderPriority; - } - }); - } - } - - pushSprite(component, positionQuad, uvRect, tintColor, texture, renderMode, camera: Camera) { - this._spriteItems.push({ - component, - positionQuad, - uvRect, - tintColor, - texture, - renderMode, - camera - }); - } -} diff --git a/packages/core/src/RenderPipeline/SpriteBatcher.ts b/packages/core/src/RenderPipeline/SpriteBatcher.ts new file mode 100644 index 0000000000..4053a302fa --- /dev/null +++ b/packages/core/src/RenderPipeline/SpriteBatcher.ts @@ -0,0 +1,259 @@ +import { Engine } from "../Engine"; +import { MeshTopology, SubMesh } from "../graphic"; +import { Buffer } from "../graphic/Buffer"; +import { BufferBindFlag } from "../graphic/enums/BufferBindFlag"; +import { BufferUsage } from "../graphic/enums/BufferUsage"; +import { IndexFormat } from "../graphic/enums/IndexFormat"; +import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; +import { VertexElement } from "../graphic/VertexElement"; +import { BufferMesh } from "../mesh/BufferMesh"; +import { Shader } from "../shader"; +import { ShaderProperty } from "../shader/ShaderProperty"; +import { SystemInfo } from "../SystemInfo"; +import { SpriteElement } from "./SpriteElement"; + +/** + * @internal + */ +export class SpriteBatcher { + private static _textureProperty: ShaderProperty = Shader.getPropertyByName("u_texture"); + /** The maximum number of vertex. */ + private static MAX_VERTEX_COUNT: number = 4096; + private static _canUploadSameBuffer: boolean = !SystemInfo._isIos(); + private static _subMeshPool: SubMesh[] = []; + private static _subMeshPoolIndex: number = 0; + + static _getSubMeshFromPool(start: number, count: number, topology?: MeshTopology): SubMesh { + const { _subMeshPoolIndex: index, _subMeshPool: pool } = SpriteBatcher; + SpriteBatcher._subMeshPoolIndex++; + if (pool.length === index) { + const subMesh = new SubMesh(start, count, topology); + pool.push(subMesh); + return subMesh; + } else { + return pool[index]; + } + } + + /** + * @internal + */ + static _restPool() { + SpriteBatcher._subMeshPoolIndex = 0; + } + + private _batchedQueue: SpriteElement[] = []; + private _meshes: BufferMesh[] = []; + private _meshCount: number = 1; + private _vertexBuffers: Buffer[] = []; + private _indiceBuffers: Buffer[] = []; + private _vertices: Float32Array; + private _indices: Uint16Array; + private _vertexCount: number = 0; + private _spriteCount: number = 0; + private _flushId: number = 0; + + constructor(engine: Engine) { + const { MAX_VERTEX_COUNT } = SpriteBatcher; + this._vertices = new Float32Array(MAX_VERTEX_COUNT * 9); + this._indices = new Uint16Array(MAX_VERTEX_COUNT * 3); + + const { _meshes, _meshCount } = this; + for (let i = 0; i < _meshCount; i++) { + _meshes[i] = this._createMesh(engine, i); + } + } + + private _createMesh(engine: Engine, index: number): BufferMesh { + const { MAX_VERTEX_COUNT } = SpriteBatcher; + const mesh = new BufferMesh(engine, `SpriteBatchBufferMesh${index}`); + + const vertexElements = [ + new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0), + new VertexElement("TEXCOORD_0", 12, VertexElementFormat.Vector2, 0), + new VertexElement("COLOR_0", 20, VertexElementFormat.Vector4, 0) + ]; + const vertexStride = 36; + + // vertices + this._vertexBuffers[index] = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + MAX_VERTEX_COUNT * 4 * vertexStride, + BufferUsage.Dynamic + ); + // indices + this._indiceBuffers[index] = new Buffer( + engine, + BufferBindFlag.IndexBuffer, + MAX_VERTEX_COUNT * 3, + BufferUsage.Dynamic + ); + mesh.setVertexBufferBinding(this._vertexBuffers[index], vertexStride); + mesh.setIndexBufferBinding(this._indiceBuffers[index], IndexFormat.UInt16); + mesh.setVertexElements(vertexElements); + + return mesh; + } + + private _updateData(engine: Engine): void { + const { _meshes, _flushId } = this; + + if (!SpriteBatcher._canUploadSameBuffer && this._meshCount <= _flushId) { + this._meshCount++; + _meshes[_flushId] = this._createMesh(engine, _flushId); + } + + const { _getSubMeshFromPool } = SpriteBatcher; + const { _batchedQueue, _vertices, _indices } = this; + const mesh = _meshes[_flushId]; + mesh.clearSubMesh(); + + let vertexIndex = 0; + let indiceIndex = 0; + let vertexStartIndex = 0; + let vertexCount = 0; + let curIndiceStartIndex = 0; + let curMeshIndex = 0; + let preSpriteElement: SpriteElement = null; + for (let i = 0, len = _batchedQueue.length; i < len; i++) { + const curSpriteElement = _batchedQueue[i]; + const { positions, uv, triangles, color } = curSpriteElement; + + // Batch vertex + const verticesNum = positions.length; + for (let j = 0; j < verticesNum; j++) { + const curPos = positions[j]; + const curUV = uv[j]; + + _vertices[vertexIndex++] = curPos.x; + _vertices[vertexIndex++] = curPos.y; + _vertices[vertexIndex++] = curPos.z; + _vertices[vertexIndex++] = curUV.x; + _vertices[vertexIndex++] = curUV.y; + _vertices[vertexIndex++] = color.r; + _vertices[vertexIndex++] = color.g; + _vertices[vertexIndex++] = color.b; + _vertices[vertexIndex++] = color.a; + } + + // Batch indice + const triangleNum = triangles.length; + for (let j = 0; j < triangleNum; j++) { + _indices[indiceIndex++] = triangles[j] + curIndiceStartIndex; + } + + curIndiceStartIndex += verticesNum; + + if (preSpriteElement === null) { + vertexCount += triangleNum; + } else { + if (this._canBatch(preSpriteElement, curSpriteElement)) { + vertexCount += triangleNum; + } else { + mesh.addSubMesh(_getSubMeshFromPool(vertexStartIndex, vertexCount)); + vertexStartIndex += vertexCount; + vertexCount = triangleNum; + _batchedQueue[curMeshIndex++] = preSpriteElement; + } + } + + preSpriteElement = curSpriteElement; + } + + mesh.addSubMesh(_getSubMeshFromPool(vertexStartIndex, vertexCount)); + _batchedQueue[curMeshIndex] = preSpriteElement; + + this._vertexBuffers[_flushId].setData(_vertices, 0, 0, vertexIndex); + this._indiceBuffers[_flushId].setData(_indices, 0, 0, indiceIndex); + } + + private _drawBatches(engine: Engine): void { + const mesh = this._meshes[this._flushId]; + const subMeshes = mesh.subMeshes; + const { _batchedQueue } = this; + + for (let i = 0, len = subMeshes.length; i < len; i++) { + const subMesh = subMeshes[i]; + const spriteElement = _batchedQueue[i]; + + if (!subMesh || !spriteElement) { + return; + } + + const compileMacros = Shader._compileMacros; + compileMacros.clear(); + + const material = spriteElement.material; + const program = material.shader._getShaderProgram(engine, compileMacros); + if (!program.isValid) { + return; + } + + program.groupingOtherUniformBlock(); + const camera = spriteElement.camera; + program.uploadAll(program.sceneUniformBlock, camera.scene.shaderData); + program.uploadAll(program.cameraUniformBlock, camera.shaderData); + program.uploadAll(program.rendererUniformBlock, spriteElement.component.shaderData); + program.uploadAll(program.materialUniformBlock, material.shaderData); + + material.renderState._apply(engine); + + engine._hardwareRenderer.drawPrimitive(mesh, subMesh, program); + } + } + + private _canBatch(preSpriteElement: SpriteElement, curSpriteElement: SpriteElement): boolean { + // Currently only compare texture + const { _textureProperty } = SpriteBatcher; + const preTexture = preSpriteElement.component.shaderData.getTexture(_textureProperty); + const curTexture = curSpriteElement.component.shaderData.getTexture(_textureProperty); + if (preTexture !== curTexture) { + return false; + } + + return ( + preSpriteElement.material === curSpriteElement.material && preSpriteElement.camera === curSpriteElement.camera + ); + } + + /** + * Flush all sprites. + */ + flush(engine: Engine): void { + const { _batchedQueue } = this; + + if (_batchedQueue.length === 0) { + return; + } + + this._updateData(engine); + this._drawBatches(engine); + + if (!SpriteBatcher._canUploadSameBuffer) { + this._flushId++; + } + + SpriteBatcher._restPool(); + this._batchedQueue.length = 0; + this._vertexCount = 0; + this._spriteCount = 0; + } + + drawSprite(spriteElement: SpriteElement): void { + const len = spriteElement.positions.length; + if (this._vertexCount + len > SpriteBatcher.MAX_VERTEX_COUNT) { + this.flush(spriteElement.camera.engine); + } + + this._vertexCount += len; + this._batchedQueue[this._spriteCount++] = spriteElement; + } + + clear(): void { + this._flushId = 0; + this._vertexCount = 0; + this._spriteCount = 0; + this._batchedQueue.length = 0; + } +} diff --git a/packages/core/src/RenderPipeline/SpriteElement.ts b/packages/core/src/RenderPipeline/SpriteElement.ts new file mode 100644 index 0000000000..83dfdb56e4 --- /dev/null +++ b/packages/core/src/RenderPipeline/SpriteElement.ts @@ -0,0 +1,58 @@ +import { Color, Vector2, Vector3 } from "@oasis-engine/math"; +import { Camera } from "../Camera"; +import { Material } from "../material"; +import { Renderer } from "../Renderer"; + +export class SpriteElement { + private static _elementPoolIndex: number = 0; + private static _elementPool: SpriteElement[] = []; + + /** + * Get sprite element from pool. + * @remark The return value is only valid for the current frame, and the next frame will be automatically recycled for reuse. + */ + static getFromPool(): SpriteElement { + const { _elementPoolIndex: index, _elementPool: pool } = SpriteElement; + SpriteElement._elementPoolIndex++; + if (pool.length === index) { + const element = new SpriteElement(); + pool.push(element); + return element; + } else { + return pool[index]; + } + } + + /** + * @internal + */ + static _restPool() { + SpriteElement._elementPoolIndex = 0; + } + + component: Renderer; + positions: Vector3[]; + uv: Vector2[]; + triangles: number[]; + color: Color; + material: Material; + camera: Camera; + + setValue( + component: Renderer, + positions: Vector3[], + uv: Vector2[], + triangles: number[], + color: Color, + material: Material, + camera: Camera + ): void { + this.component = component; + this.positions = positions; + this.uv = uv; + this.triangles = triangles; + this.color = color; + this.material = material; + this.camera = camera; + } +} diff --git a/packages/core/src/SystemInfo.ts b/packages/core/src/SystemInfo.ts index 255d2277cc..52c5792a37 100644 --- a/packages/core/src/SystemInfo.ts +++ b/packages/core/src/SystemInfo.ts @@ -8,4 +8,12 @@ export class SystemInfo { static get devicePixelRatio(): number { return window.devicePixelRatio; } + + /** + * @internal + */ + static _isIos(): boolean { + const ua = window.navigator.userAgent.toLocaleLowerCase(); + return /iphone|ipad|ipod/.test(ua); + } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 358b28b72e..498ba8217a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -28,6 +28,7 @@ export { BasicRenderPipeline } from "./RenderPipeline/BasicRenderPipeline"; export { RenderQueue } from "./RenderPipeline/RenderQueue"; export { RenderPass } from "./RenderPipeline/RenderPass"; export { RenderElement } from "./RenderPipeline/RenderElement"; +export { SpriteElement } from "./RenderPipeline/SpriteElement"; export * from "./base"; // Lighting diff --git a/packages/core/tests/SpriteRenderer.test.ts b/packages/core/tests/SpriteRenderer.test.ts new file mode 100644 index 0000000000..00d40f38cc --- /dev/null +++ b/packages/core/tests/SpriteRenderer.test.ts @@ -0,0 +1,58 @@ +// @ts-nocheck +import { Color } from "@oasis-engine/math"; +import { Entity, Sprite, SpriteRenderer, Texture2D } from "../src/index"; +import { WebGLEngine } from "../../rhi-webgl/src/WebGLEngine"; + +describe("SpriteRenderer", () => { + const canvas = document.createElement("canvas"); + const engine = new WebGLEngine(canvas); + const scene = engine.sceneManager.activeScene; + + engine.run(); + + beforeEach(() => { + (Entity as any)._entitys.length = 0; + (Entity as any)._entitys._elements.length = 0; + scene.createRootEntity("root"); + }); + + it("Constructor", () => { + const rootEntity = scene.getRootEntity(); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + const defaultColor = new Color(1, 1, 1, 1); + + expect(spriteRenderer instanceof SpriteRenderer).toEqual(true); + expect(spriteRenderer.sprite).toEqual(null); + expect(Color.equals(spriteRenderer.color, defaultColor)).toEqual(true); + expect(spriteRenderer.flipX).toEqual(false); + expect(spriteRenderer.flipY).toEqual(false); + }); + + it("get set sprite", () => { + const rootEntity = scene.getRootEntity(); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + const texture = new Texture2D(engine, 100, 100); + const sprite = new Sprite(engine, texture); + spriteRenderer.sprite = sprite; + + expect(spriteRenderer.sprite).toBe(sprite); + }); + + it("get set color", () => { + const rootEntity = scene.getRootEntity(); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.color.setValue(1, 0, 0, 1); + + expect(Color.equals(spriteRenderer.color, new Color(1, 0, 0, 1))).toEqual(true); + }); + + it("get set flip", () => { + const rootEntity = scene.getRootEntity(); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.flipX = true; + spriteRenderer.flipY = true; + + expect(spriteRenderer.flipY).toEqual(true); + expect(spriteRenderer.flipY).toEqual(true); + }); +}); diff --git a/packages/math/src/Rect.ts b/packages/math/src/Rect.ts new file mode 100644 index 0000000000..838bff501e --- /dev/null +++ b/packages/math/src/Rect.ts @@ -0,0 +1,64 @@ +import { IClone } from "@oasis-engine/design"; + +// A 2d rectangle defined by x and y position, width and height. +export class Rect implements IClone { + /** The x coordinate of the rectangle. */ + public x: number; + /** The y coordinate of the rectangle. */ + public y: number; + /** The width of the rectangle, measured from the x position. */ + public width: number; + /** The height of the rectangle, measured from the y position. */ + public height: number; + + /** + * Constructor of Rect. + * @param x - The x coordinate of the rectangle, default 0 + * @param y - The y coordinate of the rectangle, default 0 + * @param width - The width of the rectangle, measured from the x position, default 0 + * @param height - The height of the rectangle, measured from the y position, default 0 + */ + constructor(x: number = 0, y: number = 0, width: number = 0, height: number = 0) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + /** + * Set the value of this rectangle. + * @param x - The x coordinate of the rectangle + * @param y - The y coordinate of the rectangle + * @param width - The width of the rectangle, measured from the x position + * @param height - The height of the rectangle, measured from the y position + * @returns This rectangle + */ + setValue(x: number, y: number, width: number, height: number): Rect { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + return this; + } + + /** + * Creates a clone of this rect. + * @returns A clone of this rect + */ + clone(): Rect { + return new Rect(this.x, this.y, this.width, this.height); + } + + /** + * Clones this rect to the specified rect. + * @param out - The specified rect + * @returns The specified rect + */ + cloneTo(out: Rect): Rect { + out.x = this.x; + out.y = this.y; + out.width = this.width; + out.height = this.height; + return out; + } +} diff --git a/packages/math/src/index.ts b/packages/math/src/index.ts index 63a1e4adf2..1355e47613 100755 --- a/packages/math/src/index.ts +++ b/packages/math/src/index.ts @@ -14,3 +14,4 @@ export { Vector3 } from "./Vector3"; export { Vector4 } from "./Vector4"; export { Plane } from "./Plane"; export { Color } from "./Color"; +export { Rect } from "./Rect"; \ No newline at end of file diff --git a/packages/rhi-webgl/src/GLSprite.ts b/packages/rhi-webgl/src/GLSprite.ts deleted file mode 100644 index 6c93065d03..0000000000 --- a/packages/rhi-webgl/src/GLSprite.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Logger } from "@oasis-engine/core"; -import { Vector2, Vector3, Vector4 } from "@oasis-engine/math"; - -/** - * @private - */ -export class GLSprite { - private gl: WebGLRenderingContext; - private _vbo: WebGLBuffer; - private _maxBatchCount: number; - private _vertBuffer; - private _vertCursor: number; - private _drawSpriteCount: number; - private _vertAttributes; - constructor(gl) { - this.gl = gl; - - //-- vertex attributes - this._initVertexAttributes(gl); - - this._vbo = gl.createBuffer(); - this._maxBatchCount = 0; - this._vertBuffer = null; - this._vertCursor = 0; - this._drawSpriteCount = 0; - } - - setMaxBatchCount(count) { - const requireSize = count * 6 * 9; - if (this._vertBuffer && this._vertBuffer.length >= requireSize) { - return; - } - - this._maxBatchCount = count; - this._vertBuffer = new Float32Array(requireSize); - } - - beginDraw(count) { - this._vertCursor = 0; - this._drawSpriteCount = 0; - - // Dynamic resize - if (count > this._maxBatchCount) { - this.setMaxBatchCount(count); - } - } - - drawSprite(positionQuad, uvRect, tintColor) { - this._drawSpriteCount++; - if (this._drawSpriteCount > this._maxBatchCount) { - Logger.warn("Sprite: sprite count overflow"); - return; - } - - const color = tintColor; - - const u = uvRect.u; - const v = uvRect.v; - const p = uvRect.u + uvRect.width; - const q = uvRect.v + uvRect.height; - - this._pushVertex(positionQuad.leftTop, new Vector2(u, v), color); - this._pushVertex(positionQuad.leftBottom, new Vector2(u, q), color); - this._pushVertex(positionQuad.rightBottom, new Vector2(p, q), color); - - this._pushVertex(positionQuad.rightBottom, new Vector2(p, q), color); - this._pushVertex(positionQuad.rightTop, new Vector2(p, v), color); - this._pushVertex(positionQuad.leftTop, new Vector2(u, v), color); - } - - endDraw(shaderProgram) { - const vertCount = this._vertCursor / 9; - if (vertCount <= 0) return; - - var gl = this.gl; - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); - gl.bindBuffer(gl.ARRAY_BUFFER, this._vbo); - gl.bufferData(gl.ARRAY_BUFFER, this._vertBuffer, gl.DYNAMIC_DRAW); - - const attributeLocation = shaderProgram.attributeLocation; - for (const k in attributeLocation) { - const location = attributeLocation[k]; - const attrib = this._vertAttributes[k]; - gl.vertexAttribPointer(location, attrib.size, attrib.type, attrib.normalized, attrib.stride, attrib.offset); - gl.enableVertexAttribArray(location); - } - - gl.drawArrays(gl.TRIANGLES, 0, vertCount); - - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); - gl.bindBuffer(gl.ARRAY_BUFFER, null); - // disable attributes - for (const k in attributeLocation) { - gl.disableVertexAttribArray(attributeLocation[k]); - } - } - - _initVertexAttributes(gl) { - const vertexStride = (3 + 2 + 4) * 4; - const posAtt: any = {}; - posAtt.name = "a_pos"; - posAtt.size = 3; - posAtt.offset = 0; - - const uvAtt: any = {}; - uvAtt.name = "a_uv"; - uvAtt.size = 2; - uvAtt.offset = 3 * 4; - - const colorAtt: any = {}; - colorAtt.name = "a_color"; - colorAtt.size = 4; - colorAtt.offset = 5 * 4; - - this._vertAttributes = { a_pos: posAtt, a_uv: uvAtt, a_color: colorAtt }; - for (const k in this._vertAttributes) { - const att = this._vertAttributes[k]; - att.type = gl.FLOAT; - att.normalized = false; - att.stride = vertexStride; - } - } - - _pushVertex(pos: Vector3, uv: Vector2, color: Vector4) { - const vb = this._vertBuffer; - const id = this._vertCursor; - - //-- pos - vb[id] = pos.x; - vb[id + 1] = pos.y; - vb[id + 2] = pos.z; - - //-- uv - vb[id + 3] = uv.x; - vb[id + 4] = uv.y; - - //-- color - vb[id + 5] = color.x; - vb[id + 6] = color.y; - vb[id + 7] = color.z; - vb[id + 8] = color.w; - - //-- - this._vertCursor += 9; - } - - finalize() { - if (this._vbo) { - this.gl.deleteBuffer(this._vbo); - this._vbo = null; - } - } -} diff --git a/packages/rhi-webgl/src/GLSpriteBatcher.ts b/packages/rhi-webgl/src/GLSpriteBatcher.ts deleted file mode 100644 index 2833ce5d2c..0000000000 --- a/packages/rhi-webgl/src/GLSpriteBatcher.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Camera, Engine, Logger, Material, Shader } from "@oasis-engine/core"; -import { GLSprite } from "./GLSprite"; -import "./GLSpriteMaterial"; - -/** - * @private - */ -export class GLSpriteBatcher { - private _gl: WebGLRenderingContext; - private _batchedQueue; - private _targetTexture; - private _glSprite: GLSprite; - private _camera; - - constructor(rhi) { - this._gl = rhi.gl; - - this._batchedQueue = []; - this._targetTexture = null; - - this._glSprite = new GLSprite(rhi.gl); - - this._camera = null; - } - - flush(engine: Engine, material: Material) { - if (this._batchedQueue.length === 0) { - return; - } - - if (!this._targetTexture) { - Logger.error("No texture!"); - return; - } - - const materialData = material.shaderData; - materialData.setTexture("s_diffuse", this._targetTexture); - materialData.setMatrix("matView", this._camera.viewMatrix); - materialData.setMatrix("matProjection", this._camera.projectionMatrix); - - //@ts-ignore - const compileMacros = Shader._compileMacros; - compileMacros.clear(); - - //@ts-ignore - const program = material.shader._getShaderProgram(engine, compileMacros); - if (!program.isValid) { - return; - } - - program.bind(); - program.groupingOtherUniformBlock(); - program.uploadAll(program.materialUniformBlock, materialData); - - //@ts-ignore - material.renderState._apply(engine); - - this._glSprite.beginDraw(this._batchedQueue.length); - for (let i = 0, len = this._batchedQueue.length; i < len; i++) { - const positionQuad = this._batchedQueue[i].positionQuad; - const uvRect = this._batchedQueue[i].uvRect; - const tintColor = this._batchedQueue[i].tintColor; - this._glSprite.drawSprite(positionQuad, uvRect, tintColor); - } - this._glSprite.endDraw(program); - - this._batchedQueue = []; - this._targetTexture = null; - this._camera = null; - } - - canBatch(texture, renderMode, camera: Camera) { - if (this._targetTexture === null) { - return true; - } - return texture === this._targetTexture && camera === this._camera; - } - - drawSprite(material: Material, positionQuad, uvRect, tintColor, texture, renderMode, camera: Camera) { - if (!this.canBatch(texture, renderMode, camera)) { - this.flush(camera.engine, material); - } - - this._targetTexture = texture; - this._camera = camera; - this._batchedQueue.push({ positionQuad, uvRect, tintColor }); - } - - finalize() { - this._glSprite.finalize(); - } -} diff --git a/packages/rhi-webgl/src/WebGLRenderer.ts b/packages/rhi-webgl/src/WebGLRenderer.ts index b6d096352a..9e4601ba38 100644 --- a/packages/rhi-webgl/src/WebGLRenderer.ts +++ b/packages/rhi-webgl/src/WebGLRenderer.ts @@ -7,7 +7,6 @@ import { GLCapabilityType, HardwareRenderer, Logger, - Material, Mesh, RenderTarget, SubMesh, @@ -19,7 +18,6 @@ import { GLCapability } from "./GLCapability"; import { GLExtensions } from "./GLExtensions"; import { GLPrimitive } from "./GLPrimitive"; import { GLRenderStates } from "./GLRenderStates"; -import { GLSpriteBatcher } from "./GLSpriteBatcher"; import { WebGLExtension } from "./type"; import { WebCanvas } from "./WebCanvas"; @@ -206,20 +204,6 @@ export class WebGLRenderer implements HardwareRenderer { } } - drawSprite(material, positionQuad, uvRect, tintColor, texture, renderMode, camera: Camera) { - if (!this._spriteBatcher) { - this._spriteBatcher = new GLSpriteBatcher(this); - } - - this._spriteBatcher.drawSprite(material, positionQuad, uvRect, tintColor, texture, renderMode, camera); - } - - flushSprite(engine: Engine, material: Material) { - if (this._spriteBatcher) { - this._spriteBatcher.flush(engine, material); - } - } - activeRenderTarget(renderTarget: RenderTarget, camera: Camera) { const gl = this._gl; if (renderTarget) {