Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion e2e/.dev/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,15 @@ module.exports = {
strictPort: true
},
resolve: {
dedupe: ["@galacean/engine"]
dedupe: ["@galacean/engine"],
alias: {
// Serve the spine packages from source: their dist is downleveled to ES5, and an ES5
// subclass extending the external @esotericsoftware/spine-core native classes throws
// "Class constructor cannot be invoked without 'new'".
"@galacean/engine-spine-core-4.2": path.resolve(__dirname, "../../packages/spine-core-4.2/src/index.ts"),
"@galacean/engine-spine-core-3.8": path.resolve(__dirname, "../../packages/spine-core-3.8/src/index.ts"),
"@galacean/engine-spine": path.resolve(__dirname, "../../packages/spine/src/index.ts")
}
},
optimizeDeps: {
exclude: [
Expand Down
58 changes: 58 additions & 0 deletions e2e/case/spine-ui-canvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @title Spine UI Canvas
* @category Spine
*/
import { Camera, Logger, Vector3, WebGLEngine } from "@galacean/engine";
import { SpineAnimationRenderer } from "@galacean/engine-spine";
import "@galacean/engine-spine-core-4.2";
import { CanvasRenderMode, UICanvas, UIGroup } from "@galacean/engine-ui";
import { initScreenshot, updateForE2E } from "./.mockForE2E";

Logger.enable();

WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize();
const scene = engine.sceneManager.activeScene;
const root = scene.createRootEntity();

const cameraEntity = root.createChild("camera");
const camera = cameraEntity.addComponent(Camera);
cameraEntity.transform.setPosition(0, 0, 20);
cameraEntity.transform.lookAt(new Vector3(0, 0, 0));

engine.resourceManager
.load({
urls: ["/spineboy.json", "/spineboy.atlas", "/spineboy.png"],
type: "Spine"
})
.then((resource: any) => {
// Left: world space — the renderer joins the camera pipeline.
const worldEntity = resource.instantiate();
worldEntity.transform.setPosition(-5, -3.2, 0);
root.addChild(worldEntity);
worldEntity.getComponent(SpineAnimationRenderer).state.setAnimation(0, "idle", true);

// Middle & right: the same SpineAnimationRenderer hosted by a world-space UICanvas;
// the right one is faded by a UIGroup.
const canvasEntity = root.createChild("canvas");
const canvas = canvasEntity.addComponent(UICanvas);
canvas.renderMode = CanvasRenderMode.WorldSpace;

const uiEntity = resource.instantiate();
uiEntity.transform.setPosition(0, -3.2, 0);
canvasEntity.addChild(uiEntity);
uiEntity.getComponent(SpineAnimationRenderer).state.setAnimation(0, "idle", true);

const groupEntity = canvasEntity.createChild("group");
const group = groupEntity.addComponent(UIGroup);
group.alpha = 0.5;
const fadedEntity = resource.instantiate();
fadedEntity.transform.setPosition(5, -3.2, 0);
groupEntity.addChild(fadedEntity);
fadedEntity.getComponent(SpineAnimationRenderer).state.setAnimation(0, "idle", true);

updateForE2E(engine);
initScreenshot(engine, camera);
})
.catch((e) => console.error("CASE ERROR", e));
});
55 changes: 55 additions & 0 deletions e2e/case/spine-ui-rect-mask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @title Spine UI Rect Mask
* @category Spine
*/
import { Camera, Logger, Vector3, WebGLEngine } from "@galacean/engine";
import { SpineAnimationRenderer } from "@galacean/engine-spine";
import "@galacean/engine-spine-core-4.2";
import { CanvasRenderMode, RectMask2D, UICanvas, UITransform } from "@galacean/engine-ui";
import { initScreenshot, updateForE2E } from "./.mockForE2E";

Logger.enable();

WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize();
const scene = engine.sceneManager.activeScene;
const root = scene.createRootEntity();

const cameraEntity = root.createChild("camera");
const camera = cameraEntity.addComponent(Camera);
cameraEntity.transform.setPosition(0, 0, 20);
cameraEntity.transform.lookAt(new Vector3(0, 0, 0));

engine.resourceManager
.load({
urls: ["/spineboy.json", "/spineboy.atlas", "/spineboy.png"],
type: "Spine"
})
.then((resource: any) => {
const canvasEntity = root.createChild("canvas");
const canvas = canvasEntity.addComponent(UICanvas);
canvas.renderMode = CanvasRenderMode.WorldSpace;

// Left: unmasked reference.
const referenceEntity = resource.instantiate();
referenceEntity.transform.setPosition(-4, -3.2, 0);
canvasEntity.addChild(referenceEntity);
referenceEntity.getComponent(SpineAnimationRenderer).state.setAnimation(0, "idle", true);

// Right: the same skeleton clipped by a RectMask2D to a 8x4 window around the torso.
const maskEntity = canvasEntity.createChild("mask");
maskEntity.transform.setPosition(4, -1, 0);
const maskTransform = maskEntity.transform as UITransform;
maskTransform.size.set(8, 4);
maskEntity.addComponent(RectMask2D);

const maskedEntity = resource.instantiate();
maskedEntity.transform.setPosition(0, -2.2, 0);
maskEntity.addChild(maskedEntity);
maskedEntity.getComponent(SpineAnimationRenderer).state.setAnimation(0, "idle", true);

updateForE2E(engine);
initScreenshot(engine, camera);
})
.catch((e) => console.error("CASE ERROR", e));
});
12 changes: 12 additions & 0 deletions e2e/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ export const E2E_CONFIG = {
caseFileName: "spine-tint-black",
threshold: 0,
diffPercentage: 0.05
},
uiCanvas: {
category: "Spine",
caseFileName: "spine-ui-canvas",
threshold: 0,
diffPercentage: 0.05
},
uiRectMask: {
category: "Spine",
caseFileName: "spine-ui-rect-mask",
threshold: 0,
diffPercentage: 0.05
}
},
Animator: {
Expand Down
3 changes: 3 additions & 0 deletions e2e/fixtures/originImage/Spine_spine-ui-canvas.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions e2e/fixtures/originImage/Spine_spine-ui-rect-mask.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@galacean/engine-spine-core-4.2": "workspace:*",
"dat.gui": "^0.7.9",
"vite": "3.1.6",
"sass": "^1.55.0"
"sass": "^1.55.0",
"@galacean/engine-ui": "workspace:*"
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export * from "./renderingHardwareInterface/index";
export * from "./physics/index";
export * from "./Utils";
export * from "./audio/index";
export * from "./ui/index";
import { Polyfill } from "./Polyfill";

export { ShaderMacroCollection } from "./shader/ShaderMacroCollection";
Expand Down
43 changes: 43 additions & 0 deletions packages/core/src/shaderlib/extra/spine.fs.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,30 @@ varying vec4 v_lightColor;
varying vec3 v_darkColor;
#endif

#ifdef RENDERER_UI_RECT_CLIP
uniform vec4 renderer_UIRectClipRect;
uniform float renderer_UIRectClipEnabled;
uniform vec4 renderer_UIRectClipSoftness;
uniform float renderer_UIRectClipHardClip;

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;
}
#endif

void main()
{
vec4 texColor = texture2D(material_SpineTexture, v_uv);
Expand All @@ -26,4 +50,23 @@ void main()
#else
gl_FragColor = texColor * lightColor;
#endif

#ifdef RENDERER_UI_RECT_CLIP
if (renderer_UIRectClipEnabled > 0.5) {
float rectClipAlpha = getUIRectClipAlpha();
// Non-PMA blending scales rgb by src alpha at blend time; PMA (and PMA-additive, whose
// rgb ignores alpha entirely) needs rgb faded explicitly.
gl_FragColor.a *= rectClipAlpha;
gl_FragColor.rgb *= mix(1.0, rectClipAlpha, renderer_PremultipliedAlpha);
// Hard clip discards on the rect factor, not final alpha: additive slots legitimately
// emit zero-alpha fragments with visible rgb.
if (renderer_UIRectClipHardClip > 0.5 && rectClipAlpha < 0.001) {
discard;
}
}
#endif

#ifdef ENGINE_SHOULD_SRGB_CORRECT
gl_FragColor = outputSRGBCorrection(gl_FragColor);
#endif
}
11 changes: 11 additions & 0 deletions packages/core/src/shaderlib/extra/spine.vs.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ varying vec4 v_lightColor;
varying vec3 v_darkColor;
#endif

#ifdef RENDERER_UI_RECT_CLIP
// Spine vertices are entity-local (unlike UI chunk vertices, which are pre-transformed to
// world space), so the rect-clip world position must be derived from the model matrix.
uniform mat4 renderer_ModelMat;
varying vec2 v_worldPosition;
#endif

void main()
{
gl_Position = renderer_MVPMat * vec4(POSITION, 1.0);
Expand All @@ -25,4 +32,8 @@ void main()
#ifdef RENDERER_TINT_BLACK
v_darkColor = DARK_COLOR;
#endif

#ifdef RENDERER_UI_RECT_CLIP
v_worldPosition = (renderer_ModelMat * vec4(POSITION, 1.0)).xy;
#endif
}
9 changes: 8 additions & 1 deletion packages/core/src/ui/IUICanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ import { Camera } from "../Camera";
import { Entity } from "../Entity";
import { RenderContext } from "../RenderPipeline/RenderContext";
import { RenderElement } from "../RenderPipeline/RenderElement";
import type { DisorderedArray } from "../utils/DisorderedArray";
import { IUIElement } from "./IUIElement";

/**
* @internal
* The canvas contract the engine pipeline and canvas-hosted renderers work against.
* Underscore members are engine-internal plumbing implemented by the ui package's `UICanvas`.
*/
export interface IUICanvas {
entity: Entity;
sortOrder: number;
_canvasIndex: number;
_isRootCanvas: boolean;
_sortDistance: number;
_realRenderMode: number;
_renderElements: RenderElement[];
_disorderedElements: DisorderedArray<IUIElement>;
_canRender(camera: Camera): boolean;
_prepareRender(renderContext: RenderContext): void;
}
112 changes: 112 additions & 0 deletions packages/core/src/ui/IUIElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Ray, Vector3, Vector4 } from "@galacean/engine-math";
import { Entity } from "../Entity";
import type { DisorderedArray } from "../utils/DisorderedArray";
import { IUICanvas } from "./IUICanvas";

/**
* Flags describing a root-canvas level modification, broadcast to its elements.
*/
export enum RootCanvasModifyFlags {
None = 0x0,
ReferenceResolutionPerUnit = 0x1,
All = 0x1
}

/**
* Entity modify flags dispatched by UI components, extending `EntityModifyFlags`.
*/
export enum EntityUIModifyFlags {
CanvasEnableInScene = 0x4,
GroupEnableInScene = 0x8
}

/**
* Flags describing a UI group modification, broadcast to its elements.
*/
export enum GroupModifyFlags {
None = 0x0,
GlobalAlpha = 0x1,
GlobalInteractive = 0x2,
All = 0x3
}

/**
* An element owned by a root UI canvas: tracks its canvas and listens to hierarchy changes.
* Underscore members are engine-internal plumbing maintained by the ui package's `Utils`.
*/
export interface IUIElement {
entity: Entity;
_rootCanvas: IUICanvas;
_indexInRootCanvas: number;
_rootCanvasListeningEntities: Entity[];
_isRootCanvasDirty: boolean;

_getRootCanvas(): IUICanvas;
_rootCanvasListener: (flag: number, param?: any) => void;
_onRootCanvasModify?(flag: RootCanvasModifyFlags): void;
}

/**
* A UI group: cascades alpha/interactive state to its elements.
*/
export interface IUIGroup {
entity: Entity;
/** Marker used to identify a group component without a ui-package dependency. */
_isUIGroup: boolean;
_disorderedElements: DisorderedArray<IUIGroupAble>;
_getGlobalAlpha(): number;
}

/**
* An element that can belong to a UI group.
*/
export interface IUIGroupAble extends IUIElement {
_group: IUIGroup;
_indexInGroup: number;
_groupListeningEntities: Entity[];
_isGroupDirty: boolean;

_globalAlpha?: number;
_globalInteractive?: boolean;

_getGroup(): IUIGroup;
_onGroupModify(flag: GroupModifyFlags, isPass?: boolean): void;
_groupListener(flag: number): void;
}

/**
* The raycast hit output a canvas-hosted renderer fills.
*/
export interface IUIHitResult {
entity: Entity;
distance: number;
point: Vector3;
normal: Vector3;
component: any;
}

/**
* A renderer a root `UICanvas` collects and drives: ordered with the other UI elements,
* group-faded, rect-clipped and hit-tested by the canvas. The ui package's `UIRenderer` is
* the reference implementation; any other `Renderer` (e.g. spine) can implement it to be
* hosted by a canvas instead of rendering through the camera pipeline.
*
* @remarks
* Non-ui implementers switch their own `ComponentsManager` renderer registration off while
* hosted and push their render elements into `IUICanvas._renderElements` from `_render`.
*/
export interface IUIRenderer extends IUIGroupAble {
/** Marker checked by the canvas walk. */
_isUIRenderer: boolean;

raycastEnabled: boolean;
_raycast(ray: Ray, out: IUIHitResult, distance: number): boolean;

/** Rect masks assigned by the canvas walk (ui `RectMask2D` instances). */
_rectMasks: any[];
_rectMaskRect: Vector4;
_rectMaskSoftness: Vector4;
_rectMaskEnabled: boolean;
_rectMaskHardClip: boolean;
_setRectMasks(rectMasks: any[], count: number): void;
}
3 changes: 3 additions & 0 deletions packages/core/src/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { EntityUIModifyFlags, GroupModifyFlags, RootCanvasModifyFlags } from "./IUIElement";
export type { IUIElement, IUIGroup, IUIGroupAble, IUIHitResult, IUIRenderer } from "./IUIElement";
export type { IUICanvas } from "./IUICanvas";
Loading
Loading