diff --git a/e2e/.dev/vite.config.js b/e2e/.dev/vite.config.js index 2cfc11d824..498a9a0244 100644 --- a/e2e/.dev/vite.config.js +++ b/e2e/.dev/vite.config.js @@ -42,6 +42,7 @@ demoList.forEach(({ file }) => { fs.outputJSONSync(path.join(__dirname, OUT_PATH, ".demoList.json"), demoSorted); module.exports = { + publicDir: path.resolve(__dirname, "public"), server: { open: true, host: "0.0.0.0", diff --git a/e2e/case/particleRenderer-dream.ts b/e2e/case/particleRenderer-dream.ts index e515bd4fbf..6eb51637ed 100644 --- a/e2e/case/particleRenderer-dream.ts +++ b/e2e/case/particleRenderer-dream.ts @@ -68,8 +68,8 @@ WebGLEngine.create({ cameraEntity.addChild(fireEntity); - updateForE2E(engine, 500); - initScreenshot(engine, camera); + // updateForE2E(engine, 500); + // initScreenshot(engine, camera); }); }); diff --git a/examples/src/sprite-renderer-filled.ts b/examples/src/sprite-renderer-filled.ts new file mode 100644 index 0000000000..e31504875f --- /dev/null +++ b/examples/src/sprite-renderer-filled.ts @@ -0,0 +1,146 @@ +/** + * @title Sprite Filled + * @category 2D + */ + +import * as dat from "dat.gui"; +import { + AssetType, + Camera, + Sprite, + SpriteDrawMode, + SpriteFilledMode, + SpriteFilledOrigin, + SpriteRenderer, + Texture2D, + Vector3, + WebGLEngine +} from "@galacean/engine"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor.set(0.15, 0.15, 0.18, 1); + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(0, 0, 50); + cameraEntity.addComponent(Camera); + + // Load texture and create sprite + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*ApFPTZSqcMkAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D + }) + .then((texture) => { + const spriteEntity = rootEntity.createChild("sprite"); + spriteEntity.transform.position = new Vector3(0, 0, 0); + spriteEntity.transform.setScale(2, 2, 2); + const renderer = spriteEntity.addComponent(SpriteRenderer); + renderer.sprite = new Sprite(engine, texture); + + // Set filled mode + renderer.drawMode = SpriteDrawMode.Filled; + renderer.filledMode = SpriteFilledMode.Radial360; + renderer.filledOrigin = SpriteFilledOrigin.Bottom; + renderer.filledAmount = 0.75; + renderer.filledClockWise = true; + + addGUI(renderer); + }); + + engine.run(); + + function addGUI(renderer: SpriteRenderer) { + const gui = new dat.GUI(); + + const filledModeMap: Record = { + Horizontal: SpriteFilledMode.Horizontal, + Vertical: SpriteFilledMode.Vertical, + Radial90: SpriteFilledMode.Radial90, + Radial180: SpriteFilledMode.Radial180, + Radial360: SpriteFilledMode.Radial360 + }; + + const originForRadial360: string[] = ["Right", "Top", "Left", "Bottom"]; + const originForRadial180: string[] = ["Right", "Top", "Left", "Bottom"]; + const originForRadial90: string[] = ["BottomLeft", "BottomRight", "TopRight", "TopLeft"]; + const originForHorizontal: string[] = ["Left", "Right"]; + const originForVertical: string[] = ["Bottom", "Top"]; + + const originMap: Record = { + Right: SpriteFilledOrigin.Right, + TopRight: SpriteFilledOrigin.TopRight, + Top: SpriteFilledOrigin.Top, + TopLeft: SpriteFilledOrigin.TopLeft, + Left: SpriteFilledOrigin.Left, + BottomLeft: SpriteFilledOrigin.BottomLeft, + Bottom: SpriteFilledOrigin.Bottom, + BottomRight: SpriteFilledOrigin.BottomRight + }; + + const state = { + filledMode: "Radial360", + origin: "Bottom", + amount: 0.75, + clockWise: true + }; + + const folder = gui.addFolder("Filled Sprite"); + folder.open(); + + // Filled mode + folder.add(state, "filledMode", Object.keys(filledModeMap)).onChange((value: string) => { + renderer.filledMode = filledModeMap[value]; + updateOriginOptions(value); + }); + + // Origin + let originCtrl = folder.add(state, "origin", originForRadial360).onChange((value: string) => { + renderer.filledOrigin = originMap[value]; + }); + + // Amount + folder.add(state, "amount", 0.0, 1.0, 0.01).onChange((value: number) => { + renderer.filledAmount = value; + }); + + // ClockWise + folder.add(state, "clockWise").onChange((value: boolean) => { + renderer.filledClockWise = value; + }); + + function updateOriginOptions(mode: string) { + folder.remove(originCtrl); + + let options: string[]; + switch (mode) { + case "Horizontal": + options = originForHorizontal; + break; + case "Vertical": + options = originForVertical; + break; + case "Radial90": + options = originForRadial90; + break; + case "Radial180": + options = originForRadial180; + break; + default: + options = originForRadial360; + break; + } + + state.origin = options[0]; + renderer.filledOrigin = originMap[state.origin]; + + originCtrl = folder.add(state, "origin", options).onChange((value: string) => { + renderer.filledOrigin = originMap[value]; + }); + } + } +}); diff --git a/packages/core/src/2d/assembler/FilledSpriteAssembler.ts b/packages/core/src/2d/assembler/FilledSpriteAssembler.ts new file mode 100644 index 0000000000..894f1b07c6 --- /dev/null +++ b/packages/core/src/2d/assembler/FilledSpriteAssembler.ts @@ -0,0 +1,595 @@ +import { BoundingBox, Matrix, Vector2, Vector3 } from "@galacean/engine-math"; +import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; +import { StaticInterfaceImplement } from "../../base/StaticInterfaceImplement"; +import { SpriteFilledMode } from "../enums/SpriteFilledMode"; +import { SpriteFilledOrigin } from "../enums/SpriteFilledOrigin"; +import { ISpriteAssembler } from "./ISpriteAssembler"; +import { ISpriteRenderer } from "./ISpriteRenderer"; + +/** + * Assemble vertex data for the sprite renderer in filled mode. + */ +@StaticInterfaceImplement() +export class FilledSpriteAssembler { + private static _matrix = new Matrix(); + private static _worldPositions = [ + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3() + ]; + private static _uvs = [ + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2() + ]; + private static _inPositions: Vector3[] = []; + private static _inUVs: Vector2[] = []; + private static _outPositions: Vector3[] = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; + private static _outUVs: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; + private static _vertexOffset = 0; + private static _indicesOffset = 0; + + static resetData(renderer: ISpriteRenderer): void { + const manager = renderer._getChunkManager(); + const lastSubChunk = renderer._subChunk; + lastSubChunk && manager.freeSubChunk(lastSubChunk); + const subChunk = manager.allocateSubChunk(16); + subChunk.indices = []; + renderer._subChunk = subChunk; + } + + static updatePositions( + renderer: ISpriteRenderer, + worldMatrix: Matrix, + width: number, + height: number, + pivot: Vector2, + flipX: boolean, + flipY: boolean + ): void { + const { x: pivotX, y: pivotY } = pivot; + const modelMatrix = FilledSpriteAssembler._matrix; + const { elements: wE } = modelMatrix; + const { elements: pWE } = worldMatrix; + const sx = flipX ? -width : width; + const sy = flipY ? -height : height; + (wE[0] = pWE[0] * sx), (wE[1] = pWE[1] * sx), (wE[2] = pWE[2] * sx); + (wE[4] = pWE[4] * sy), (wE[5] = pWE[5] * sy), (wE[6] = pWE[6] * sy); + (wE[8] = pWE[8]), (wE[9] = pWE[9]), (wE[10] = pWE[10]); + wE[12] = pWE[12] - pivotX * wE[0] - pivotY * wE[4]; + wE[13] = pWE[13] - pivotX * wE[1] - pivotY * wE[5]; + wE[14] = pWE[14] - pivotX * wE[2] - pivotY * wE[6]; + + switch (renderer.filledMode) { + case SpriteFilledMode.Horizontal: + this._filledLinear(renderer, modelMatrix, true); + break; + case SpriteFilledMode.Vertical: + this._filledLinear(renderer, modelMatrix, false); + break; + case SpriteFilledMode.Radial90: + this._filledRadial90(renderer, modelMatrix, renderer.filledOrigin, renderer.filledAmount, renderer.filledClockWise); + break; + case SpriteFilledMode.Radial180: + this._filledRadial180(renderer, modelMatrix, renderer.filledOrigin, renderer.filledAmount, renderer.filledClockWise); + break; + case SpriteFilledMode.Radial360: + this._filledRadial360(renderer, modelMatrix, renderer.filledOrigin, renderer.filledAmount, renderer.filledClockWise); + break; + default: + break; + } + + // @ts-ignore + BoundingBox.transform(renderer.sprite._getBounds(), modelMatrix, renderer._bounds); + } + + static updateUVs(renderer: ISpriteRenderer): void { + // UVs are computed in updatePositions. + } + + static updateColor(renderer: ISpriteRenderer, alpha: number): void { + const subChunk = renderer._subChunk; + const { r, g, b, a } = renderer.color; + const finalAlpha = a * alpha; + const vertices = subChunk.chunk.vertices; + const vertexArea = subChunk.vertexArea; + for (let i = 0, o = vertexArea.start + 5, n = vertexArea.size / 9; i < n; ++i, o += 9) { + vertices[o] = r; + vertices[o + 1] = g; + vertices[o + 2] = b; + vertices[o + 3] = finalAlpha; + } + } + + private static _filledLinear(renderer: ISpriteRenderer, matrix: Matrix, isHorizontal: boolean): void { + const amount = renderer.filledAmount; + if (amount <= 0.001) { + renderer._subChunk.indices.length = 0; + return; + } + + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[3]; + + const subChunk = renderer._subChunk; + const vertices = subChunk.chunk.vertices; + + let x0: number, y0: number, u0: number, v0: number; + let x1: number, y1: number, u1: number, v1: number; + let x2: number, y2: number, u2: number, v2: number; + let x3: number, y3: number, u3: number, v3: number; + + if (isHorizontal) { + const originIsStart = renderer.filledOrigin === SpriteFilledOrigin.Left; + const startX = originIsStart ? lPosLB.x : lPosRB.x - (lPosRB.x - lPosLB.x) * amount; + const endX = originIsStart ? lPosLB.x + (lPosRB.x - lPosLB.x) * amount : lPosRB.x; + const startU = originIsStart ? left : right - (right - left) * amount; + const endU = originIsStart ? left + (right - left) * amount : right; + (x0 = startX), (y0 = lPosLB.y), (u0 = startU), (v0 = bottom); + (x1 = endX), (y1 = lPosRB.y), (u1 = endU), (v1 = bottom); + (x2 = startX), (y2 = lPosLT.y), (u2 = startU), (v2 = top); + (x3 = endX), (y3 = lPosRT.y), (u3 = endU), (v3 = top); + } else { + const originIsStart = renderer.filledOrigin === SpriteFilledOrigin.Bottom; + const startY = originIsStart ? lPosLB.y : lPosLT.y - (lPosLT.y - lPosLB.y) * amount; + const endY = originIsStart ? lPosLB.y + (lPosLT.y - lPosLB.y) * amount : lPosLT.y; + const startV = originIsStart ? bottom : top - (top - bottom) * amount; + const endV = originIsStart ? bottom + (top - bottom) * amount : top; + (x0 = lPosLB.x), (y0 = startY), (u0 = left), (v0 = startV); + (x1 = lPosRB.x), (y1 = startY), (u1 = right), (v1 = startV); + (x2 = lPosLT.x), (y2 = endY), (u2 = left), (v2 = endV); + (x3 = lPosRT.x), (y3 = endY), (u3 = right), (v3 = endV); + } + + const { elements: wE } = matrix; + const start = subChunk.vertexArea.start; + // LB + vertices[start] = wE[0] * x0 + wE[4] * y0 + wE[12]; + vertices[start + 1] = wE[1] * x0 + wE[5] * y0 + wE[13]; + vertices[start + 2] = wE[2] * x0 + wE[6] * y0 + wE[14]; + vertices[start + 3] = u0; + vertices[start + 4] = v0; + // RB + vertices[start + 9] = wE[0] * x1 + wE[4] * y1 + wE[12]; + vertices[start + 10] = wE[1] * x1 + wE[5] * y1 + wE[13]; + vertices[start + 11] = wE[2] * x1 + wE[6] * y1 + wE[14]; + vertices[start + 12] = u1; + vertices[start + 13] = v1; + // LT + vertices[start + 18] = wE[0] * x2 + wE[4] * y2 + wE[12]; + vertices[start + 19] = wE[1] * x2 + wE[5] * y2 + wE[13]; + vertices[start + 20] = wE[2] * x2 + wE[6] * y2 + wE[14]; + vertices[start + 21] = u2; + vertices[start + 22] = v2; + // RT + vertices[start + 27] = wE[0] * x3 + wE[4] * y3 + wE[12]; + vertices[start + 28] = wE[1] * x3 + wE[5] * y3 + wE[13]; + vertices[start + 29] = wE[2] * x3 + wE[6] * y3 + wE[14]; + vertices[start + 30] = u3; + vertices[start + 31] = v3; + + const indices = subChunk.indices; + indices[0] = 0; + indices[1] = 1; + indices[2] = 2; + indices[3] = 2; + indices[4] = 1; + indices[5] = 3; + indices.length = 6; + } + + private static _filledRadial90( + renderer: ISpriteRenderer, + matrix: Matrix, + origin: SpriteFilledOrigin, + amount: number, + cw: boolean + ): void { + if (amount <= 0.001) { + renderer._subChunk.indices.length = 0; + return; + } + + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[3]; + + // Transform 4 corners to world space + const [wLB, wRB, wLT, wRT] = this._worldPositions; + const [uvLB, uvRB, uvLT, uvRT] = this._uvs; + wLB.set(lPosLB.x, lPosLB.y, 0).transformToVec3(matrix), uvLB.set(left, bottom); + wRB.set(lPosRB.x, lPosRB.y, 0).transformToVec3(matrix), uvRB.set(right, bottom); + wLT.set(lPosLT.x, lPosLT.y, 0).transformToVec3(matrix), uvLT.set(left, top); + wRT.set(lPosRT.x, lPosRT.y, 0).transformToVec3(matrix), uvRT.set(right, top); + + // Map vertices based on origin corner: + // [center, CW-adjacent, CCW-adjacent, opposite] + const { _inPositions: inPositions, _inUVs: inUVs, _outPositions: outPositions, _outUVs: outUVs } = this; + switch (origin) { + case SpriteFilledOrigin.BottomLeft: + (inPositions[0] = wLB), (inUVs[0] = uvLB); + (inPositions[1] = wRB), (inUVs[1] = uvRB); + (inPositions[2] = wLT), (inUVs[2] = uvLT); + (inPositions[3] = wRT), (inUVs[3] = uvRT); + break; + case SpriteFilledOrigin.BottomRight: + (inPositions[0] = wRB), (inUVs[0] = uvRB); + (inPositions[1] = wRT), (inUVs[1] = uvRT); + (inPositions[2] = wLB), (inUVs[2] = uvLB); + (inPositions[3] = wLT), (inUVs[3] = uvLT); + break; + case SpriteFilledOrigin.TopRight: + (inPositions[0] = wRT), (inUVs[0] = uvRT); + (inPositions[1] = wLT), (inUVs[1] = uvLT); + (inPositions[2] = wRB), (inUVs[2] = uvRB); + (inPositions[3] = wLB), (inUVs[3] = uvLB); + break; + case SpriteFilledOrigin.TopLeft: + (inPositions[0] = wLT), (inUVs[0] = uvLT); + (inPositions[1] = wLB), (inUVs[1] = uvLB); + (inPositions[2] = wRT), (inUVs[2] = uvRT); + (inPositions[3] = wRB), (inUVs[3] = uvRB); + break; + default: + break; + } + + const startAngle = cw ? 90 - amount * 90 : 0; + const endAngle = cw ? 90 : amount * 90; + + this._vertexOffset = this._indicesOffset = 0; + this._radialCut(renderer._subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + renderer._subChunk.indices.length = this._indicesOffset; + } + + private static _filledRadial180( + renderer: ISpriteRenderer, + matrix: Matrix, + origin: SpriteFilledOrigin, + amount: number, + cw: boolean + ): void { + if (amount <= 0.001) { + renderer._subChunk.indices.length = 0; + return; + } + + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[3]; + + // Transform corners and compute edge midpoints + const [wLB, wMB, wRB, wLM, , wRM, wLT, wMT, wRT] = this._worldPositions; + const [uvLB, uvMB, uvRB, uvLM, , uvRM, uvLT, uvMT, uvRT] = this._uvs; + wLB.set(lPosLB.x, lPosLB.y, 0).transformToVec3(matrix), uvLB.set(left, bottom); + wRB.set(lPosRB.x, lPosRB.y, 0).transformToVec3(matrix), uvRB.set(right, bottom); + wLT.set(lPosLT.x, lPosLT.y, 0).transformToVec3(matrix), uvLT.set(left, top); + wRT.set(lPosRT.x, lPosRT.y, 0).transformToVec3(matrix), uvRT.set(right, top); + Vector3.lerp(wLB, wRB, 0.5, wMB), Vector2.lerp(uvLB, uvRB, 0.5, uvMB); + Vector3.lerp(wLB, wLT, 0.5, wLM), Vector2.lerp(uvLB, uvLT, 0.5, uvLM); + Vector3.lerp(wLT, wRT, 0.5, wMT), Vector2.lerp(uvLT, uvRT, 0.5, uvMT); + Vector3.lerp(wRB, wRT, 0.5, wRM), Vector2.lerp(uvRB, uvRT, 0.5, uvRM); + + const startAngle = cw ? 180 - amount * 180 : 0; + const endAngle = cw ? 180 : amount * 180; + + this._vertexOffset = this._indicesOffset = 0; + const { _inPositions: inPositions, _inUVs: inUVs, _outPositions: outPositions, _outUVs: outUVs } = this; + const { _subChunk: subChunk } = renderer; + + // Center is at the origin edge midpoint; two quadrants cover the full sprite. + // Quadrant A (0°-90°), Quadrant B (90°-180°) + switch (origin) { + case SpriteFilledOrigin.Bottom: + // Center=MB, A: [MB,RB,MT,RT], B: [MB,MT,LB,LT] + (inPositions[0] = wMB), (inUVs[0] = uvMB); + (inPositions[1] = wRB), (inUVs[1] = uvRB); + (inPositions[2] = wMT), (inUVs[2] = uvMT); + (inPositions[3] = wRT), (inUVs[3] = uvRT); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wMT), (inUVs[1] = uvMT); + (inPositions[2] = wLB), (inUVs[2] = uvLB); + (inPositions[3] = wLT), (inUVs[3] = uvLT); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + case SpriteFilledOrigin.Top: + // Center=MT, A: [MT,LT,MB,LB], B: [MT,MB,RT,RB] + (inPositions[0] = wMT), (inUVs[0] = uvMT); + (inPositions[1] = wLT), (inUVs[1] = uvLT); + (inPositions[2] = wMB), (inUVs[2] = uvMB); + (inPositions[3] = wLB), (inUVs[3] = uvLB); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wMB), (inUVs[1] = uvMB); + (inPositions[2] = wRT), (inUVs[2] = uvRT); + (inPositions[3] = wRB), (inUVs[3] = uvRB); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + case SpriteFilledOrigin.Left: + // Center=LM, A: [LM,LB,RM,RB], B: [LM,RM,LT,RT] + (inPositions[0] = wLM), (inUVs[0] = uvLM); + (inPositions[1] = wLB), (inUVs[1] = uvLB); + (inPositions[2] = wRM), (inUVs[2] = uvRM); + (inPositions[3] = wRB), (inUVs[3] = uvRB); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wRM), (inUVs[1] = uvRM); + (inPositions[2] = wLT), (inUVs[2] = uvLT); + (inPositions[3] = wRT), (inUVs[3] = uvRT); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + case SpriteFilledOrigin.Right: + // Center=RM, A: [RM,RT,LM,LT], B: [RM,LM,RB,LB] + (inPositions[0] = wRM), (inUVs[0] = uvRM); + (inPositions[1] = wRT), (inUVs[1] = uvRT); + (inPositions[2] = wLM), (inUVs[2] = uvLM); + (inPositions[3] = wLT), (inUVs[3] = uvLT); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wLM), (inUVs[1] = uvLM); + (inPositions[2] = wRB), (inUVs[2] = uvRB); + (inPositions[3] = wLB), (inUVs[3] = uvLB); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + default: + break; + } + + subChunk.indices.length = this._indicesOffset; + } + + private static _filledRadial360( + renderer: ISpriteRenderer, + matrix: Matrix, + origin: SpriteFilledOrigin, + amount: number, + cw: boolean + ): void { + if (amount <= 0.001) { + renderer._subChunk.indices.length = 0; + return; + } + + let startAngle = 0; + switch (origin) { + case SpriteFilledOrigin.Right: + startAngle = cw ? 360 - amount * 360 : 0; + break; + case SpriteFilledOrigin.Top: + startAngle = cw ? 450 - amount * 360 : 90; + break; + case SpriteFilledOrigin.Left: + startAngle = cw ? 540 - amount * 360 : 180; + break; + case SpriteFilledOrigin.Bottom: + startAngle = cw ? 630 - amount * 360 : 270; + break; + default: + break; + } + const endAngle = startAngle + amount * 360; + + this._processRadialGrid(renderer, matrix, startAngle, endAngle); + } + + /** + * Prepare the 3x3 grid and process 4 quadrants for radial fill. + */ + private static _processRadialGrid( + renderer: ISpriteRenderer, + matrix: Matrix, + startAngle: number, + endAngle: number + ): void { + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[3]; + + // --------------- + // LT - MT - RT + // | | | + // LM - C - RM + // | | | + // LB - MB - RB + // --------------- + const [wPosLB, wPosMB, wPosRB, wPosLM, wPosC, wPosRM, wPosLT, wPosMT, wPosRT] = this._worldPositions; + const [uvLB, uvMB, uvRB, uvLM, uvC, uvRM, uvLT, uvMT, uvRT] = this._uvs; + + wPosLB.set(lPosLB.x, lPosLB.y, 0).transformToVec3(matrix), uvLB.set(left, bottom); + wPosRB.set(lPosRB.x, lPosRB.y, 0).transformToVec3(matrix), uvRB.set(right, bottom); + wPosLT.set(lPosLT.x, lPosLT.y, 0).transformToVec3(matrix), uvLT.set(left, top); + wPosRT.set(lPosRT.x, lPosRT.y, 0).transformToVec3(matrix), uvRT.set(right, top); + Vector3.lerp(wPosLB, wPosRB, 0.5, wPosMB), Vector2.lerp(uvLB, uvRB, 0.5, uvMB); + Vector3.lerp(wPosLB, wPosLT, 0.5, wPosLM), Vector2.lerp(uvLB, uvLT, 0.5, uvLM); + Vector3.lerp(wPosLT, wPosRT, 0.5, wPosMT), Vector2.lerp(uvLT, uvRT, 0.5, uvMT); + Vector3.lerp(wPosRB, wPosRT, 0.5, wPosRM), Vector2.lerp(uvRB, uvRT, 0.5, uvRM); + Vector3.lerp(wPosLB, wPosRT, 0.5, wPosC), Vector2.lerp(uvLB, uvRT, 0.5, uvC); + + this._vertexOffset = this._indicesOffset = 0; + const { _inPositions: inPositions, _inUVs: inUVs, _outPositions: outPositions, _outUVs: outUVs } = this; + const { _subChunk: subChunk } = renderer; + let quadrantStart = 0; + let quadrantEnd = 0; + (inPositions[0] = wPosC), (inUVs[0] = uvC); + + { + // First quadrant (0°-90°) + if (startAngle >= 90) { + quadrantStart = startAngle - 360; + quadrantEnd = endAngle - 360; + } else { + quadrantStart = startAngle; + quadrantEnd = endAngle; + } + (inPositions[1] = wPosRM), (inUVs[1] = uvRM); + (inPositions[2] = wPosMT), (inUVs[2] = uvMT); + (inPositions[3] = wPosRT), (inUVs[3] = uvRT); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + { + // Second quadrant (90°-180°) + if (startAngle >= 180) { + quadrantStart = startAngle - 360 - 90; + quadrantEnd = endAngle - 360 - 90; + } else { + quadrantStart = startAngle - 90; + quadrantEnd = endAngle - 90; + } + (inPositions[1] = wPosMT), (inUVs[1] = uvMT); + (inPositions[2] = wPosLM), (inUVs[2] = uvLM); + (inPositions[3] = wPosLT), (inUVs[3] = uvLT); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + { + // Third quadrant (180°-270°) + if (startAngle >= 270) { + quadrantStart = startAngle - 360 - 180; + quadrantEnd = endAngle - 360 - 180; + } else { + quadrantStart = startAngle - 180; + quadrantEnd = endAngle - 180; + } + (inPositions[1] = wPosLM), (inUVs[1] = uvLM); + (inPositions[2] = wPosMB), (inUVs[2] = uvMB); + (inPositions[3] = wPosLB), (inUVs[3] = uvLB); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + { + // Fourth quadrant (270°-360°) + if (startAngle >= 360) { + quadrantStart = startAngle - 360 - 270; + quadrantEnd = endAngle - 360 - 270; + } else { + quadrantStart = startAngle - 270; + quadrantEnd = endAngle - 270; + } + (inPositions[1] = wPosMB), (inUVs[1] = uvMB); + (inPositions[2] = wPosRM), (inUVs[2] = uvRM); + (inPositions[3] = wPosRB), (inUVs[3] = uvRB); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + subChunk.indices.length = this._indicesOffset; + } + + private static _radialCut( + subChunk: SubPrimitiveChunk, + positions: Vector3[], + uvs: Vector2[], + start: number, + end: number, + outPositions: Vector3[], + outUVs: Vector2[] + ): void { + if (start >= 90 || end <= 0) return; + outPositions[0].copyFrom(positions[0]); + outUVs[0].copyFrom(uvs[0]); + + if (start <= 0) { + outPositions[1].copyFrom(positions[1]); + outUVs[1].copyFrom(uvs[1]); + } else { + const startTan = Math.tan((start * Math.PI) / 180); + if (startTan < 1) { + Vector3.lerp(positions[1], positions[3], startTan, outPositions[1]); + Vector2.lerp(uvs[1], uvs[3], startTan, outUVs[1]); + } else { + Vector3.lerp(positions[2], positions[3], 1 / startTan, outPositions[1]); + Vector2.lerp(uvs[2], uvs[3], 1 / startTan, outUVs[1]); + } + } + + if (end >= 90) { + outPositions[2].copyFrom(positions[2]); + outUVs[2].copyFrom(uvs[2]); + } else { + const endTan = Math.tan((end * Math.PI) / 180); + if (endTan < 1) { + Vector3.lerp(positions[1], positions[3], endTan, outPositions[2]); + Vector2.lerp(uvs[1], uvs[3], endTan, outUVs[2]); + } else { + Vector3.lerp(positions[2], positions[3], 1 / endTan, outPositions[2]); + Vector2.lerp(uvs[2], uvs[3], 1 / endTan, outUVs[2]); + } + } + + if (start < 45 && end > 45) { + outPositions[3].copyFrom(positions[3]); + outUVs[3].copyFrom(uvs[3]); + this._addQuad(subChunk, outPositions, outUVs); + } else { + this._addTriangle(subChunk, outPositions, outUVs); + } + } + + private static _addTriangle(subChunk: SubPrimitiveChunk, positions: Vector3[], uvs: Vector2[]): void { + const vertices = subChunk.chunk.vertices; + const indices = subChunk.indices; + const vertexOffset = this._vertexOffset; + const vertexCount = vertexOffset / 9; + const start = subChunk.vertexArea.start + vertexOffset; + for (let i = 0, o = start; i < 3; ++i, o += 9) { + const position = positions[i]; + const uv = uvs[i]; + vertices[o] = position.x; + vertices[o + 1] = position.y; + vertices[o + 2] = position.z; + vertices[o + 3] = uv.x; + vertices[o + 4] = uv.y; + } + const indicesOffset = this._indicesOffset; + indices[indicesOffset] = vertexCount; + indices[indicesOffset + 1] = vertexCount + 1; + indices[indicesOffset + 2] = vertexCount + 2; + this._vertexOffset += 3 * 9; + this._indicesOffset += 3; + } + + private static _addQuad(subChunk: SubPrimitiveChunk, positions: Vector3[], uvs: Vector2[]): void { + const vertices = subChunk.chunk.vertices; + const indices = subChunk.indices; + const vertexOffset = this._vertexOffset; + const vertexCount = vertexOffset / 9; + const start = subChunk.vertexArea.start + vertexOffset; + for (let i = 0, o = start; i < 4; ++i, o += 9) { + const position = positions[i]; + const uv = uvs[i]; + vertices[o] = position.x; + vertices[o + 1] = position.y; + vertices[o + 2] = position.z; + vertices[o + 3] = uv.x; + vertices[o + 4] = uv.y; + } + const indicesOffset = this._indicesOffset; + indices[indicesOffset] = vertexCount; + indices[indicesOffset + 1] = vertexCount + 1; + indices[indicesOffset + 2] = vertexCount + 2; + indices[indicesOffset + 3] = vertexCount + 2; + indices[indicesOffset + 4] = vertexCount + 1; + indices[indicesOffset + 5] = vertexCount + 3; + this._vertexOffset += 4 * 9; + this._indicesOffset += 6; + } +} diff --git a/packages/core/src/2d/assembler/ISpriteRenderer.ts b/packages/core/src/2d/assembler/ISpriteRenderer.ts index a72f4e9436..04b50b3fc9 100644 --- a/packages/core/src/2d/assembler/ISpriteRenderer.ts +++ b/packages/core/src/2d/assembler/ISpriteRenderer.ts @@ -1,6 +1,8 @@ import { Color } from "@galacean/engine-math"; import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; +import { SpriteFilledMode } from "../enums/SpriteFilledMode"; +import { SpriteFilledOrigin } from "../enums/SpriteFilledOrigin"; import { SpriteTileMode } from "../enums/SpriteTileMode"; import { Sprite } from "../sprite"; @@ -12,6 +14,10 @@ export interface ISpriteRenderer { color?: Color; tileMode?: SpriteTileMode; tiledAdaptiveThreshold?: number; + filledMode?: SpriteFilledMode; + filledAmount?: number; + filledOrigin?: SpriteFilledOrigin; + filledClockWise?: boolean; _subChunk: SubPrimitiveChunk; _getChunkManager(): PrimitiveChunkManager; } diff --git a/packages/core/src/2d/enums/SpriteDrawMode.ts b/packages/core/src/2d/enums/SpriteDrawMode.ts index 46bcfd3783..6f35d8c1ef 100644 --- a/packages/core/src/2d/enums/SpriteDrawMode.ts +++ b/packages/core/src/2d/enums/SpriteDrawMode.ts @@ -7,5 +7,7 @@ export enum SpriteDrawMode { /** When modifying the size of the renderer, it scales to fill the range according to the sprite border settings. */ Sliced, /** When modifying the size of the renderer, it will tile to fill the range according to the sprite border settings. */ - Tiled + Tiled, + /** Fill the sprite partially, controlled by fill amount, mode and origin. */ + Filled } diff --git a/packages/core/src/2d/enums/SpriteFilledMode.ts b/packages/core/src/2d/enums/SpriteFilledMode.ts new file mode 100644 index 0000000000..a01cda9e53 --- /dev/null +++ b/packages/core/src/2d/enums/SpriteFilledMode.ts @@ -0,0 +1,15 @@ +/** + * Sprite's filled mode enumeration. + */ +export enum SpriteFilledMode { + /** Fill horizontally. */ + Horizontal, + /** Fill vertically. */ + Vertical, + /** Fill radially over 90 degrees. */ + Radial90, + /** Fill radially over 180 degrees. */ + Radial180, + /** Fill radially over 360 degrees. */ + Radial360 +} diff --git a/packages/core/src/2d/enums/SpriteFilledOrigin.ts b/packages/core/src/2d/enums/SpriteFilledOrigin.ts new file mode 100644 index 0000000000..e9b3193970 --- /dev/null +++ b/packages/core/src/2d/enums/SpriteFilledOrigin.ts @@ -0,0 +1,21 @@ +/** + * Sprite's filled origin enumeration. + */ +export enum SpriteFilledOrigin { + /** Origin at the right. */ + Right, + /** Origin at the top-right. */ + TopRight, + /** Origin at the top. */ + Top, + /** Origin at the top-left. */ + TopLeft, + /** Origin at the left. */ + Left, + /** Origin at the bottom-left. */ + BottomLeft, + /** Origin at the bottom. */ + Bottom, + /** Origin at the bottom-right. */ + BottomRight +} diff --git a/packages/core/src/2d/index.ts b/packages/core/src/2d/index.ts index 47be64ccfc..5481f42b28 100644 --- a/packages/core/src/2d/index.ts +++ b/packages/core/src/2d/index.ts @@ -1,11 +1,14 @@ export type { ISpriteAssembler } from "./assembler/ISpriteAssembler"; export type { ISpriteRenderer } from "./assembler/ISpriteRenderer"; +export { FilledSpriteAssembler } from "./assembler/FilledSpriteAssembler"; export { SimpleSpriteAssembler } from "./assembler/SimpleSpriteAssembler"; export { SlicedSpriteAssembler } from "./assembler/SlicedSpriteAssembler"; export { TiledSpriteAssembler } from "./assembler/TiledSpriteAssembler"; export { SpriteAtlas } from "./atlas/SpriteAtlas"; export { FontStyle } from "./enums/FontStyle"; export { SpriteDrawMode } from "./enums/SpriteDrawMode"; +export { SpriteFilledMode } from "./enums/SpriteFilledMode"; +export { SpriteFilledOrigin } from "./enums/SpriteFilledOrigin"; export { SpriteMaskInteraction } from "./enums/SpriteMaskInteraction"; export { SpriteModifyFlags } from "./enums/SpriteModifyFlags"; export { SpriteTileMode } from "./enums/SpriteTileMode"; diff --git a/packages/core/src/2d/sprite/SpriteRenderer.ts b/packages/core/src/2d/sprite/SpriteRenderer.ts index c1b183ee7a..a75dac1510 100644 --- a/packages/core/src/2d/sprite/SpriteRenderer.ts +++ b/packages/core/src/2d/sprite/SpriteRenderer.ts @@ -10,10 +10,13 @@ import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManage import { ShaderProperty } from "../../shader/ShaderProperty"; import { ISpriteAssembler } from "../assembler/ISpriteAssembler"; import { ISpriteRenderer } from "../assembler/ISpriteRenderer"; +import { FilledSpriteAssembler } from "../assembler/FilledSpriteAssembler"; import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; import { SlicedSpriteAssembler } from "../assembler/SlicedSpriteAssembler"; import { TiledSpriteAssembler } from "../assembler/TiledSpriteAssembler"; import { SpriteDrawMode } from "../enums/SpriteDrawMode"; +import { SpriteFilledMode } from "../enums/SpriteFilledMode"; +import { SpriteFilledOrigin } from "../enums/SpriteFilledOrigin"; import { SpriteMaskInteraction } from "../enums/SpriteMaskInteraction"; import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; import { SpriteTileMode } from "../enums/SpriteTileMode"; @@ -39,6 +42,14 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; @assignmentClone private _tiledAdaptiveThreshold: number = 0.5; + @assignmentClone + private _filledMode: SpriteFilledMode = SpriteFilledMode.Radial360; + @assignmentClone + private _filledAmount: number = 1; + @assignmentClone + private _filledOrigin: SpriteFilledOrigin = SpriteFilledOrigin.Bottom; + @assignmentClone + private _filledClockWise: boolean = true; @deepClone private _color: Color = new Color(1, 1, 1, 1); @@ -78,6 +89,9 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._assembler = TiledSpriteAssembler; break; + case SpriteDrawMode.Filled: + this._assembler = FilledSpriteAssembler; + break; default: break; } @@ -119,6 +133,74 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { } } + /** + * The fill amount of the sprite renderer, range from 0 to 1. (Only works in filled mode.) + */ + get filledAmount(): number { + return this._filledAmount; + } + + set filledAmount(value: number) { + value = MathUtil.clamp(value, 0, 1); + if (this._filledAmount !== value) { + this._filledAmount = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill mode of the sprite renderer. (Only works in filled mode.) + */ + get filledMode(): SpriteFilledMode { + return this._filledMode; + } + + set filledMode(value: SpriteFilledMode) { + if (this._filledMode !== value) { + this._filledMode = value; + // Reset origin to a valid default for the new mode + this._filledOrigin = + value === SpriteFilledMode.Radial90 ? SpriteFilledOrigin.BottomLeft : SpriteFilledOrigin.Bottom; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill origin of the sprite renderer. (Only works in filled mode.) + */ + get filledOrigin(): SpriteFilledOrigin { + return this._filledOrigin; + } + + set filledOrigin(value: SpriteFilledOrigin) { + if (this._filledOrigin !== value) { + this._filledOrigin = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * Whether the fill is clockwise. (Only works in filled radial mode.) + */ + get filledClockWise(): boolean { + return this._filledClockWise; + } + + set filledClockWise(value: boolean) { + if (this._filledClockWise !== value) { + this._filledClockWise = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + /** * The Sprite to render. */ @@ -438,6 +520,9 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeUVAndColor; break; + case SpriteDrawMode.Filled: + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeUVAndColor; + break; } break; case SpriteModifyFlags.border: diff --git a/packages/core/src/Camera.ts b/packages/core/src/Camera.ts index 53b689dd84..3faf19a2ef 100644 --- a/packages/core/src/Camera.ts +++ b/packages/core/src/Camera.ts @@ -9,7 +9,7 @@ import { Transform } from "./Transform"; import { UpdateFlagManager } from "./UpdateFlagManager"; import { VirtualCamera } from "./VirtualCamera"; import { GLCapabilityType, Logger } from "./base"; -import { deepClone, ignoreClone } from "./clone/CloneManager"; +import { assignmentClone, deepClone, ignoreClone } from "./clone/CloneManager"; import { AntiAliasing } from "./enums/AntiAliasing"; import { CameraClearFlags } from "./enums/CameraClearFlags"; import { CameraModifyFlags } from "./enums/CameraModifyFlags"; @@ -125,8 +125,10 @@ export class Camera extends Component { @deepClone _virtualCamera: VirtualCamera = new VirtualCamera(); /** @internal */ + @assignmentClone _replacementShader: Shader = null; /** @internal */ + @assignmentClone _replacementSubShaderTag: ShaderTagKey = null; /** @internal */ _replacementFailureStrategy: ReplacementFailureStrategy = null; @@ -934,7 +936,15 @@ export class Camera extends Component { private _getInvViewProjMat(): Matrix { if (this._isInvViewProjDirty.flag) { this._isInvViewProjDirty.flag = false; - Matrix.multiply(this._entity.transform.worldMatrix, this._getInverseProjectionMatrix(), this._invViewProjMat); + const matrix = this._invViewProjMat; + if (this._isCustomViewMatrix) { + Matrix.invert(this.viewMatrix, matrix); + } else { + // Ignore scale, consistent with viewMatrix getter + const transform = this._entity.transform; + Matrix.rotationTranslation(transform.worldRotationQuaternion, transform.worldPosition, matrix); + } + matrix.multiply(this._getInverseProjectionMatrix()); } return this._invViewProjMat; } diff --git a/packages/core/src/ComponentsManager.ts b/packages/core/src/ComponentsManager.ts index e9b9a1cc0d..96924c1a60 100644 --- a/packages/core/src/ComponentsManager.ts +++ b/packages/core/src/ComponentsManager.ts @@ -36,8 +36,6 @@ export class ComponentsManager { // Render private _onUpdateRenderers = new DisorderedArray(); - // Delay dispose active/inActive Pool - private _componentsContainerPool: Component[][] = []; addCamera(camera: Camera) { camera._cameraIndex = this._activeCameras.length; @@ -270,14 +268,6 @@ export class ComponentsManager { ); } - getActiveChangedTempList(): Component[] { - return this._componentsContainerPool.length ? this._componentsContainerPool.pop() : []; - } - - putActiveChangedTempList(componentContainer: Component[]): void { - componentContainer.length = 0; - this._componentsContainerPool.push(componentContainer); - } /** * @internal diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index 08503f5498..43a32f7514 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -122,7 +122,7 @@ export class Entity extends EngineObject { private _transform: Transform; private _templateResource: ReferResource; private _parent: Entity = null; - private _activeChangedComponents: Component[]; + private _isActiveChanging: boolean = false; private _modifyFlagManager: UpdateFlagManager; /** @@ -377,6 +377,14 @@ export class Entity extends EngineObject { return this; } + // Some imported animation clips are normalized to include the single scene root + // name (for example "mixamorig:Hips/..."), while the Animator may already sit on + // that root entity. Accept a self-name prefix so wrapped model roots and + // standalone single-root clips resolve through the same path convention. + if (splits[0] === this.name) { + return splits.length === 1 ? this : Entity._findChildByName(this, 0, splits, 1); + } + return Entity._findChildByName(this, 0, splits, 0); } @@ -565,24 +573,24 @@ export class Entity extends EngineObject { * @internal */ _processActive(activeChangeFlag: ActiveChangeFlag): void { - if (this._activeChangedComponents) { + if (this._isActiveChanging) { throw "Note: can't set the 'main inActive entity' active in hierarchy, if the operation is in main inActive entity or it's children script's onDisable Event."; } - this._activeChangedComponents = this._scene._componentsManager.getActiveChangedTempList(); - this._setActiveInHierarchy(this._activeChangedComponents, activeChangeFlag); - this._setActiveComponents(true, activeChangeFlag); + this._isActiveChanging = true; + this._setActiveInHierarchy(activeChangeFlag); + this._isActiveChanging = false; } /** * @internal */ _processInActive(activeChangeFlag: ActiveChangeFlag): void { - if (this._activeChangedComponents) { + if (this._isActiveChanging) { throw "Note: can't set the 'main active entity' inActive in hierarchy, if the operation is in main active entity or it's children script's onEnable Event."; } - this._activeChangedComponents = this._scene._componentsManager.getActiveChangedTempList(); - this._setInActiveInHierarchy(this._activeChangedComponents, activeChangeFlag); - this._setActiveComponents(false, activeChangeFlag); + this._isActiveChanging = true; + this._setInActiveInHierarchy(activeChangeFlag); + this._isActiveChanging = false; } /** @@ -690,42 +698,33 @@ export class Entity extends EngineObject { } } - private _setActiveComponents(isActive: boolean, activeChangeFlag: ActiveChangeFlag): void { - const activeChangedComponents = this._activeChangedComponents; - for (let i = 0, length = activeChangedComponents.length; i < length; ++i) { - activeChangedComponents[i]._setActive(isActive, activeChangeFlag); - } - this._scene._componentsManager.putActiveChangedTempList(activeChangedComponents); - this._activeChangedComponents = null; - } - - private _setActiveInHierarchy(activeChangedComponents: Component[], activeChangeFlag: ActiveChangeFlag): void { + private _setActiveInHierarchy(activeChangeFlag: ActiveChangeFlag): void { activeChangeFlag & ActiveChangeFlag.Hierarchy && (this._isActiveInHierarchy = true); activeChangeFlag & ActiveChangeFlag.Scene && (this._isActiveInScene = true); const components = this._components; for (let i = 0, n = components.length; i < n; i++) { const component = components[i]; - (component.enabled || !component._awoken) && activeChangedComponents.push(component); + (component.enabled || !component._awoken) && component._setActive(true, activeChangeFlag); } const children = this._children; - for (let i = 0, n = children.length; i < n; i++) { + for (let i = children.length - 1; i >= 0; i--) { const child = children[i]; - child.isActive && child._setActiveInHierarchy(activeChangedComponents, activeChangeFlag); + child.isActive && child._setActiveInHierarchy(activeChangeFlag); } } - private _setInActiveInHierarchy(activeChangedComponents: Component[], activeChangeFlag: ActiveChangeFlag): void { + private _setInActiveInHierarchy(activeChangeFlag: ActiveChangeFlag): void { activeChangeFlag & ActiveChangeFlag.Hierarchy && (this._isActiveInHierarchy = false); activeChangeFlag & ActiveChangeFlag.Scene && (this._isActiveInScene = false); const components = this._components; for (let i = 0, n = components.length; i < n; i++) { const component = components[i]; - component.enabled && activeChangedComponents.push(component); + component.enabled && component._setActive(false, activeChangeFlag); } const children = this._children; - for (let i = 0, n = children.length; i < n; i++) { + for (let i = children.length - 1; i >= 0; i--) { const child = children[i]; - child.isActive && child._setInActiveInHierarchy(activeChangedComponents, activeChangeFlag); + child.isActive && child._setInActiveInHierarchy(activeChangeFlag); } } diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 180c2d1356..c25c30e3c8 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -218,13 +218,31 @@ export class Animator extends Component { * @param stateName - The state name * @param layerIndex - The layer index(default -1). If layer is -1, find the first state with the given state name */ - findAnimatorState(stateName: string, layerIndex: number = -1): AnimatorState { - return this._getAnimatorStateInfo(stateName, layerIndex).state; + /** + * Find the per-instance play data for a state by name. + * The returned object's `speed` is per-instance and safe to modify without affecting other Animator instances. + * @param stateName - The state name + * @param layerIndex - The layer index (default -1, searches all layers) + * @returns Per-instance AnimatorStatePlayData, or null if not found + */ + findAnimatorState(stateName: string, layerIndex: number = -1): AnimatorStatePlayData { + const { state, layerIndex: foundLayer } = this._getAnimatorStateInfo(stateName, layerIndex); + if (!state || foundLayer < 0) return null; + const layerData = this._animatorLayersData[foundLayer]; + if (!layerData) return null; + // Check srcPlayData and destPlayData for the matching state + if (layerData.srcPlayData.state === state) return layerData.srcPlayData; + if (layerData.destPlayData.state === state) return layerData.destPlayData; + // State exists in controller but not currently playing — return srcPlayData initialized with the state + return layerData.srcPlayData; } /** * Get the layer by name. * @param name - The layer's name. + * @todo Return per-instance layer data (like AnimatorStatePlayData for states) instead of shared asset. + * Currently returns the shared AnimatorControllerLayer — modifying `weight` affects all instances. + * Should follow Unity's pattern: Animator.SetLayerWeight/GetLayerWeight (per-instance). */ findLayerByName(name: string): AnimatorControllerLayer { return this._animatorController?._layersMap[name]; @@ -616,7 +634,7 @@ export class Animator extends Component { const { srcPlayData } = layerData; const { state } = srcPlayData; - const playSpeed = state.speed * this.speed; + const playSpeed = srcPlayData.speed * this.speed; const playDeltaTime = playSpeed * deltaTime; srcPlayData.updateOrientation(playDeltaTime); @@ -883,7 +901,7 @@ export class Animator extends Component { return; } - const playSpeed = state.speed * this.speed; + const playSpeed = destPlayData.speed * this.speed; const playDeltaTime = playSpeed * deltaTime; destPlayData.updateOrientation(playDeltaTime); @@ -989,7 +1007,7 @@ export class Animator extends Component { ): void { const playData = layerData.srcPlayData; const { state } = playData; - const actualSpeed = state.speed * this.speed; + const actualSpeed = playData.speed * this.speed; const actualDeltaTime = actualSpeed * deltaTime; playData.updateOrientation(actualDeltaTime); diff --git a/packages/core/src/animation/index.ts b/packages/core/src/animation/index.ts index b829ffde54..bd201a871f 100644 --- a/packages/core/src/animation/index.ts +++ b/packages/core/src/animation/index.ts @@ -11,6 +11,7 @@ export { Animator } from "./Animator"; export { AnimatorController } from "./AnimatorController"; export { AnimatorControllerLayer } from "./AnimatorControllerLayer"; export { AnimatorState } from "./AnimatorState"; +export { AnimatorStatePlayData } from "./internal/AnimatorStatePlayData"; export { AnimatorStateMachine } from "./AnimatorStateMachine"; export { AnimatorStateTransition } from "./AnimatorStateTransition"; export { AnimatorConditionMode } from "./enums/AnimatorConditionMode"; diff --git a/packages/core/src/animation/internal/AnimatorStatePlayData.ts b/packages/core/src/animation/internal/AnimatorStatePlayData.ts index 7d10fc2324..5cfa2fd3eb 100644 --- a/packages/core/src/animation/internal/AnimatorStatePlayData.ts +++ b/packages/core/src/animation/internal/AnimatorStatePlayData.ts @@ -1,20 +1,63 @@ +import { AnimationClip } from "../AnimationClip"; import { AnimatorState } from "../AnimatorState"; +import { AnimatorStateTransition } from "../AnimatorStateTransition"; import { AnimatorStatePlayState } from "../enums/AnimatorStatePlayState"; import { WrapMode } from "../enums/WrapMode"; +import { StateMachineScript } from "../StateMachineScript"; import { AnimatorStateData } from "./AnimatorStateData"; /** - * @internal + * Per-instance runtime data for an AnimatorState. + * Proxies read-only properties from the shared AnimatorState asset, + * while providing per-instance mutable properties (e.g. speed). */ export class AnimatorStatePlayData { + /** @internal */ state: AnimatorState; + /** @internal */ stateData: AnimatorStateData; + /** @internal */ playedTime: number; playState: AnimatorStatePlayState; + /** @internal */ clipTime: number; + /** @internal */ currentEventIndex: number; + /** @internal */ isForward = true; + /** @internal */ offsetFrameTime: number; + /** Per-instance speed. Initialized from AnimatorState.speed, safe to modify without affecting other instances. */ + speed: number = 1.0; + + // ── Proxy properties from AnimatorState (read-only) ── + + /** The name of the state. */ + get name(): string { + return this.state.name; + } + + /** The clip played by this state. */ + get clip(): AnimationClip { + return this.state.clip; + } + + /** The wrap mode. */ + get wrapMode(): WrapMode { + return this.state.wrapMode; + } + + /** The transitions going out of this state. */ + get transitions(): Readonly { + return this.state.transitions; + } + + /** + * Add a state machine script to the underlying AnimatorState. + */ + addStateMachineScript(scriptType: new () => T): T { + return this.state.addStateMachineScript(scriptType); + } private _changedOrientation = false; @@ -27,6 +70,7 @@ export class AnimatorStatePlayData { this.clipTime = state.clipStartTime * state.clip.length; this.currentEventIndex = 0; this.isForward = true; + this.speed = state.speed; this.state._transitionCollection.needResetCurrentCheckIndex = true; } diff --git a/packages/core/src/asset/ResourceManager.ts b/packages/core/src/asset/ResourceManager.ts index 0395b2b29b..b83a9360c4 100644 --- a/packages/core/src/asset/ResourceManager.ts +++ b/packages/core/src/asset/ResourceManager.ts @@ -58,7 +58,7 @@ export class ResourceManager { * Create a ResourceManager. * @param engine - Engine to which the current ResourceManager belongs */ - constructor(public readonly engine: Engine) {} + constructor(public readonly engine: Engine) { } /** * Load the asset asynchronously by asset item information. @@ -340,7 +340,12 @@ export class ResourceManager { } private _assignDefaultOptions(assetInfo: LoadItem): LoadItem { - assetInfo.type = assetInfo.type ?? ResourceManager._getTypeByUrl(assetInfo.url); + const remoteConfig = this._virtualPathResourceMap[assetInfo.url]; + if (remoteConfig) { + assetInfo.type = remoteConfig.type; + } else { + assetInfo.type = assetInfo.type ?? ResourceManager._getTypeByUrl(assetInfo.url); + } if (assetInfo.type === undefined) { throw `asset type should be specified: ${assetInfo.url}`; } @@ -621,7 +626,7 @@ export class ResourceManager { * @param extNames - Name of file extension */ export function resourceLoader(assetType: string, extNames: string[], useCache: boolean = true) { - return >(Target: { new (useCache: boolean): T }) => { + return >(Target: { new(useCache: boolean): T }) => { const loader = new Target(useCache); ResourceManager._addLoader(assetType, loader, extNames); }; @@ -632,18 +637,18 @@ const reEscapeChar = /\\(\\)?/g; const rePropName = RegExp( // Match anything that isn't a dot or bracket. "[^.[\\]]+" + - "|" + - // Or match property names within brackets. - "\\[(?:" + - // Match a non-string expression. - "([^\"'][^[]*)" + - "|" + - // Or match strings (supports escaping characters). - "([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2" + - ")\\]" + - "|" + - // Or match "" as the space between consecutive dots or empty brackets. - "(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))", + "|" + + // Or match property names within brackets. + "\\[(?:" + + // Match a non-string expression. + "([^\"'][^[]*)" + + "|" + + // Or match strings (supports escaping characters). + "([\"'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2" + + ")\\]" + + "|" + + // Or match "" as the space between consecutive dots or empty brackets. + "(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))", "g" ); diff --git a/packages/core/src/clone/CloneManager.ts b/packages/core/src/clone/CloneManager.ts index be46462982..cd4129776a 100644 --- a/packages/core/src/clone/CloneManager.ts +++ b/packages/core/src/clone/CloneManager.ts @@ -104,6 +104,7 @@ export class CloneManager { deepInstanceMap: Map ): void { const sourceProperty = source[k]; + let effectiveCloneMode = cloneMode; // Remappable references (Entity/Component) are always remapped, regardless of clone decorator if (sourceProperty instanceof Object && (sourceProperty)._remap) { @@ -111,10 +112,28 @@ export class CloneManager { return; } - if (cloneMode === CloneMode.Ignore) return; + if (effectiveCloneMode === CloneMode.Ignore) return; + + const targetProperty = target[k]; + if ( + effectiveCloneMode === undefined && + sourceProperty instanceof Object && + targetProperty && + targetProperty !== sourceProperty && + targetProperty.constructor === sourceProperty.constructor + ) { + // Component constructors already create instance-local mutable objects. + // Preserve that isolation when cloning prefab templates instead of + // overwriting the clone with the template's shared reference. + effectiveCloneMode = CloneMode.Deep; + } // Primitives, undecorated, or @assignmentClone: direct assign - if (!(sourceProperty instanceof Object) || cloneMode === undefined || cloneMode === CloneMode.Assignment) { + if ( + !(sourceProperty instanceof Object) || + effectiveCloneMode === undefined || + effectiveCloneMode === CloneMode.Assignment + ) { target[k] = sourceProperty; return; } @@ -137,6 +156,24 @@ export class CloneManager { targetPropertyT.set(sourceProperty); } break; + case Map: + let targetPropertyM = >target[k]; + if (targetPropertyM == null) { + target[k] = targetPropertyM = new Map(); + } else { + targetPropertyM.clear(); + } + (>sourceProperty).forEach((value, key) => targetPropertyM.set(key, value)); + break; + case Set: + let targetPropertyS = >target[k]; + if (targetPropertyS == null) { + target[k] = targetPropertyS = new Set(); + } else { + targetPropertyS.clear(); + } + (>sourceProperty).forEach((value) => targetPropertyS.add(value)); + break; case Array: let targetPropertyA = >target[k]; const length = (>sourceProperty).length; @@ -150,7 +187,7 @@ export class CloneManager { >sourceProperty, targetPropertyA, i, - cloneMode, + effectiveCloneMode, srcRoot, targetRoot, deepInstanceMap @@ -158,16 +195,18 @@ export class CloneManager { } break; default: + // Check if we've already visited this source object (cycle detection) + if (deepInstanceMap.has(sourceProperty)) { + target[k] = deepInstanceMap.get(sourceProperty); + return; + } + let targetProperty = target[k]; - // If the target property is undefined, create new instance and keep reference sharing like the source if (!targetProperty) { - targetProperty = deepInstanceMap.get(sourceProperty); - if (!targetProperty) { - targetProperty = new sourceProperty.constructor(); - deepInstanceMap.set(sourceProperty, targetProperty); - } + targetProperty = new sourceProperty.constructor(); target[k] = targetProperty; } + deepInstanceMap.set(sourceProperty, targetProperty); if ((sourceProperty).copyFrom) { (targetProperty).copyFrom(sourceProperty); diff --git a/packages/core/src/mesh/ModelMesh.ts b/packages/core/src/mesh/ModelMesh.ts index 27b92ced49..8ee92c4825 100644 --- a/packages/core/src/mesh/ModelMesh.ts +++ b/packages/core/src/mesh/ModelMesh.ts @@ -925,7 +925,7 @@ export class ModelMesh extends Mesh { return null; } if (!buffer.readable) { - throw "Not allowed to access data while vertex buffer readable is false."; + throw new Error("Not allowed to access data while vertex buffer readable is false."); } const vertexCount = this.vertexCount; diff --git a/packages/core/src/mesh/Skin.ts b/packages/core/src/mesh/Skin.ts index 7a9b3444b0..18489805c5 100644 --- a/packages/core/src/mesh/Skin.ts +++ b/packages/core/src/mesh/Skin.ts @@ -15,7 +15,7 @@ export class Skin extends EngineObject { inverseBindMatrices = new Array(); /** @internal */ - @ignoreClone + @deepClone _skinMatrices: Float32Array; /** @internal */ @ignoreClone diff --git a/packages/core/src/particle/ParticleGenerator.ts b/packages/core/src/particle/ParticleGenerator.ts index d180c7f929..75c50b5cf6 100644 --- a/packages/core/src/particle/ParticleGenerator.ts +++ b/packages/core/src/particle/ParticleGenerator.ts @@ -112,6 +112,7 @@ export class ParticleGenerator { @ignoreClone _subPrimitive = new SubMesh(0, 0, MeshTopology.Triangles); /** @internal */ + @ignoreClone readonly _renderer: ParticleRenderer; /** @internal */ diff --git a/packages/core/src/particle/ParticleRenderer.ts b/packages/core/src/particle/ParticleRenderer.ts index 633e166989..563648f126 100644 --- a/packages/core/src/particle/ParticleRenderer.ts +++ b/packages/core/src/particle/ParticleRenderer.ts @@ -5,7 +5,7 @@ import { Renderer, RendererUpdateFlags } from "../Renderer"; import { TransformModifyFlags } from "../Transform"; import { GLCapabilityType } from "../base/Constant"; import { Logger } from "../base/Logger"; -import { deepClone, ignoreClone, shallowClone } from "../clone/CloneManager"; +import { assignmentClone, deepClone, ignoreClone, shallowClone } from "../clone/CloneManager"; import { ModelMesh } from "../mesh/ModelMesh"; import { ShaderMacro } from "../shader/ShaderMacro"; import { ShaderProperty } from "../shader/ShaderProperty"; @@ -47,7 +47,9 @@ export class ParticleRenderer extends Renderer { @ignoreClone _transformedBounds = new BoundingBox(); + @assignmentClone private _renderMode: ParticleRenderMode; + @assignmentClone private _currentRenderModeMacro: ShaderMacro; private _mesh: ModelMesh; private _supportInstancedArrays: boolean; @@ -258,6 +260,23 @@ export class ParticleRenderer extends Renderer { context.camera._renderPipeline.pushRenderElement(context, renderElement); } + /** + * @internal + */ + override _cloneTo(target: ParticleRenderer): void { + super._cloneTo(target); + + // ShaderData internals (_macroCollection, _propertyValueMap) are @ignoreClone, + // so the cloned shaderData only has the constructor-set Billboard macro. + // Must re-apply the source's shaderData state. + this.shaderData.cloneTo(target.shaderData); + + // Rebuild GPU resources to match cloned renderMode/mesh/maxParticles. + const gen = target.generator; + gen._reorganizeGeometryBuffers(); + gen._resizeInstanceBuffer(true, gen.main.maxParticles); + } + protected override _onDestroy(): void { const mesh = this._mesh; if (mesh) { diff --git a/packages/core/src/shader/ShaderData.ts b/packages/core/src/shader/ShaderData.ts index 72a2f096f0..f2a8a7ccd5 100644 --- a/packages/core/src/shader/ShaderData.ts +++ b/packages/core/src/shader/ShaderData.ts @@ -607,7 +607,11 @@ export class ShaderData implements IReferable, IClone { cloneTo(target: ShaderData): void { CloneManager.deepCloneObject(this._macroCollection, target._macroCollection, new Map()); - Object.assign(target._macroMap, this._macroMap); + const targetMacroMap = target._macroMap; + for (const key in targetMacroMap) { + delete targetMacroMap[key]; + } + Object.assign(targetMacroMap, this._macroMap); const referCount = target._getReferCount(); const propertyValueMap = this._propertyValueMap; const targetPropertyValueMap = target._propertyValueMap; diff --git a/packages/loader/src/gltf/parser/GLTFAnimationParser.ts b/packages/loader/src/gltf/parser/GLTFAnimationParser.ts index d1d7400c2f..10714899e7 100644 --- a/packages/loader/src/gltf/parser/GLTFAnimationParser.ts +++ b/packages/loader/src/gltf/parser/GLTFAnimationParser.ts @@ -118,6 +118,14 @@ export class GLTFAnimationParser extends GLTFParser { continue; } + // For single-root scenes, the scene root IS the top-level node (e.g. mixamorig:Hips). + // When this clip is used on a multi-root model (which wraps nodes in a GLTF_ROOT container), + // the Animator sits on GLTF_ROOT and needs the root node name in the path. + // Include the scene root name for single-root scenes to ensure consistent bone paths. + const sceneNodes = context.glTF.scenes[context.glTF.scene ?? 0]?.nodes; + if (sceneNodes?.length === 1) { + relativePath = relativePath === "" ? entity.name : `${entity.name}/${relativePath}`; + } let ComponentType: ComponentConstructor; let propertyName: string; switch (target.path) { diff --git a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts index e79ea06864..fe92bbcf6a 100644 --- a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts +++ b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts @@ -125,7 +125,14 @@ export class ReflectionParser { if (!entity) return Promise.resolve(null); const type = Loader.getClass(value.componentType); if (!type) return Promise.resolve(null); - return Promise.resolve(entity.getComponents(type, [])[value.componentIndex] ?? null); + // Try direct components first, fallback to children search (for GLB clone entities + // where the component lives on a child entity inside the clone) + const direct = entity.getComponents(type, []); + const result = direct[value.componentIndex]; + if (result) return Promise.resolve(result); + const includeChildren: any[] = []; + entity.getComponentsIncludeChildren(type, includeChildren); + return Promise.resolve(includeChildren[value.componentIndex] ?? null); } else if (ReflectionParser._isEntityRef(value)) { return Promise.resolve(this._resolveEntityByPath(value.entityPath)); } else if (ReflectionParser._isSignalRef(value)) { diff --git a/packages/rhi-webgl/src/GLPrimitive.ts b/packages/rhi-webgl/src/GLPrimitive.ts index fa7d89f1e8..b679bc4699 100644 --- a/packages/rhi-webgl/src/GLPrimitive.ts +++ b/packages/rhi-webgl/src/GLPrimitive.ts @@ -118,6 +118,7 @@ export class GLPrimitive implements IPlatformPrimitive { const element = attributes[name]; if (element) { + if(!vertexBufferBindings[element.bindingIndex]) continue; const { buffer, stride } = vertexBufferBindings[element.bindingIndex]; vbo = buffer._platformBuffer._glBuffer; // prevent binding the vbo which already bound at the last loop, e.g. a buffer with multiple attributes. diff --git a/packages/shader-lab/src/Preprocessor.ts b/packages/shader-lab/src/Preprocessor.ts index 17e907da73..8f3e1abb27 100644 --- a/packages/shader-lab/src/Preprocessor.ts +++ b/packages/shader-lab/src/Preprocessor.ts @@ -5,6 +5,7 @@ import { ShaderLib } from "@galacean/engine"; export enum MacroValueType { Number, // 1, 1.1 Symbol, // variable name + MemberAccess, // member access, e.g. input.v_uv, v.rgb FunctionCall, // function call, e.g. clamp(a, 0.0, 1.0) Other // shaderLab does not check this } @@ -27,6 +28,7 @@ export class Preprocessor { private static readonly _macroRegex = /^\s*#define\s+(\w+)[ ]*(\(([^)]*)\))?[ ]+(\(?\w+\)?.*?)(?:\/\/.*|\/\*.*?\*\/)?\s*$/gm; private static readonly _symbolReg = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + private static readonly _memberAccessReg = /^([a-zA-Z_][a-zA-Z0-9_]*)(\.[a-zA-Z_][a-zA-Z0-9_]*)+$/; private static readonly _funcCallReg = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*)\)$/; private static readonly _macroDefineIncludeMap = new Map(); @@ -61,6 +63,10 @@ export class Preprocessor { const referencedName = valueType === MacroValueType.FunctionCall ? info.functionCallName : info.value; if (info.params.indexOf(referencedName) !== -1) continue; if (out.indexOf(referencedName) === -1) out.push(referencedName); + } else if (valueType === MacroValueType.MemberAccess) { + // Extract root symbol: "input.v_uv" → "input" + const rootName = info.value.substring(0, info.value.indexOf(".")); + if (out.indexOf(rootName) === -1) out.push(rootName); } else if (valueType === MacroValueType.Other) { // #if _VERBOSE Logger.warn( @@ -110,6 +116,8 @@ export class Preprocessor { valueType = MacroValueType.Number; } else if (this._symbolReg.test(value)) { valueType = MacroValueType.Symbol; + } else if (this._memberAccessReg.test(value)) { + valueType = MacroValueType.MemberAccess; } else { const callMatch = this._funcCallReg.exec(value); if (callMatch) { diff --git a/packages/shader-lab/src/codeGen/CodeGenVisitor.ts b/packages/shader-lab/src/codeGen/CodeGenVisitor.ts index 35beaa1d98..f4d560c987 100644 --- a/packages/shader-lab/src/codeGen/CodeGenVisitor.ts +++ b/packages/shader-lab/src/codeGen/CodeGenVisitor.ts @@ -31,13 +31,19 @@ export abstract class CodeGenVisitor { protected static _tmpArrayPool = new ReturnableObjectPool(TempArray, 10); + protected static readonly _memberAccessReg = /\b(\w+)\.(\w+)\b/g; + defaultCodeGen(children: NodeChild[]) { const pool = CodeGenVisitor._tmpArrayPool; let ret = pool.get(); ret.dispose(); for (const child of children) { if (child instanceof BaseToken) { - ret.array.push(child.lexeme); + if (child.type === Keyword.MACRO_DEFINE_EXPRESSION) { + ret.array.push(this._transformMacroDefineValue(child.lexeme)); + } else { + ret.array.push(child.lexeme); + } } else { ret.array.push(child.codeGen(this)); } @@ -46,6 +52,32 @@ export abstract class CodeGenVisitor { return ret.array.join(" "); } + protected _transformMacroDefineValue( + lexeme: string, + overrideMap?: Record + ): string { + const context = VisitorContext.context; + const structVarMap = overrideMap ?? context._structVarMap; + if (!structVarMap) return lexeme; + + const spaceIdx = lexeme.indexOf(" "); + if (spaceIdx === -1) return lexeme; + + const macroName = lexeme.substring(0, spaceIdx); + let value = lexeme.substring(spaceIdx); + + const reg = CodeGenVisitor._memberAccessReg; + reg.lastIndex = 0; + value = value.replace(reg, (match, varName, propName) => { + const role = structVarMap[varName]; + if (!role) return match; + context.referenceStructPropByName(role, propName); + return propName; + }); + + return macroName + value; + } + visitPostfixExpression(node: ASTNode.PostfixExpression): string { const children = node.children; const derivationLength = children.length; @@ -212,7 +244,19 @@ export abstract class CodeGenVisitor { const children = node.children; const fullType = children[0]; if (fullType instanceof ASTNode.FullySpecifiedType && fullType.typeSpecifier.isCustom) { - VisitorContext.context.referenceGlobal(fullType.type, ESymbolType.STRUCT); + const context = VisitorContext.context; + const typeLexeme = fullType.typeSpecifier.lexeme; + const role = context.getStructRole(typeLexeme); + if (role) { + // Global variable of a varying/attribute/mrt struct type (e.g. "Varyings o;"). + // Don't output as uniform; register the variable in struct var maps instead. + const ident = children[1]; + if (ident instanceof BaseToken) { + context.registerStructVar(ident.lexeme, role); + } + return ""; + } + context.referenceGlobal(fullType.type, ESymbolType.STRUCT); } return `uniform ${this.defaultCodeGen(children)}`; } @@ -351,6 +395,8 @@ export abstract class CodeGenVisitor { const fnName = fnNode.protoType.ident.lexeme; const context = VisitorContext.context; + this._collectStructVars(fnNode, context); + if (fnName == context.stageEntry) { const statements = fnNode.statements.codeGen(this); return `void main() ${statements}`; @@ -359,6 +405,74 @@ export abstract class CodeGenVisitor { } } + private _collectStructVars(fnNode: ASTNode.FunctionDefinition, context: VisitorContext): void { + const map = context._structVarMap; + // Clear previous function's mappings + for (const key in map) delete map[key]; + + // Collect from function parameters + const paramList = fnNode.protoType.parameterList; + if (paramList) { + for (const param of paramList) { + if (param.ident && param.typeInfo && typeof param.typeInfo.type === "string") { + const role = context.getStructRole(param.typeInfo.typeLexeme); + if (role) map[param.ident.lexeme] = role; + } + } + } + + // Collect from local variable declarations in function body + this._collectStructVarsFromNode(fnNode.statements, context, map); + } + + private _collectStructVarsFromNode( + node: TreeNode, + context: VisitorContext, + map: Record + ): void { + const children = node.children; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (child instanceof ASTNode.InitDeclaratorList) { + const typeLexeme = child.typeInfo?.typeLexeme; + if (typeLexeme) { + const role = context.getStructRole(typeLexeme); + if (role) { + // Extract variable name from SingleDeclaration or comma-separated identifiers + this._extractVarNamesFromInitDeclaratorList(child, map, role); + } + } + } else if (child instanceof TreeNode) { + this._collectStructVarsFromNode(child, context, map); + } + } + } + + protected _extractVarNamesFromInitDeclaratorList( + node: ASTNode.InitDeclaratorList, + map: Record, + role: "varying" | "attribute" | "mrt" + ): void { + const children = node.children; + if (children.length === 1) { + // SingleDeclaration: type ident + const singleDecl = children[0] as ASTNode.SingleDeclaration; + const identChildren = singleDecl.children; + if (identChildren.length >= 2 && identChildren[1] instanceof BaseToken) { + map[identChildren[1].lexeme] = role; + } + } else if (children.length >= 3) { + // InitDeclaratorList , ident ... + const initDeclList = children[0]; + if (initDeclList instanceof ASTNode.InitDeclaratorList) { + this._extractVarNamesFromInitDeclaratorList(initDeclList, map, role); + } + if (children[2] instanceof BaseToken) { + map[children[2].lexeme] = role; + } + } + } + protected _reportError(loc: ShaderRange | ShaderPosition, message: string): void { // #if _VERBOSE this.errors.push(new GSError(GSErrorName.CompilationError, message, loc, ShaderLab._processingPassText)); diff --git a/packages/shader-lab/src/codeGen/GLES100.ts b/packages/shader-lab/src/codeGen/GLES100.ts index 995dccdeec..2c2e0faa93 100644 --- a/packages/shader-lab/src/codeGen/GLES100.ts +++ b/packages/shader-lab/src/codeGen/GLES100.ts @@ -47,7 +47,7 @@ export class GLES100Visitor extends GLESVisitor { return ""; } const expression = node.children[1] as ASTNode.Expression; - return `gl_FragColor = ${expression.codeGen(this)}`; + return `gl_FragColor = ${expression.codeGen(this)};`; } return super.visitJumpStatement(node); } diff --git a/packages/shader-lab/src/codeGen/GLESVisitor.ts b/packages/shader-lab/src/codeGen/GLESVisitor.ts index fbf611729f..d7596ae9ab 100644 --- a/packages/shader-lab/src/codeGen/GLESVisitor.ts +++ b/packages/shader-lab/src/codeGen/GLESVisitor.ts @@ -5,6 +5,7 @@ import { Keyword } from "../common/enums/Keyword"; import { ASTNode, TreeNode } from "../parser/AST"; import { ShaderData } from "../parser/ShaderInfo"; import { ESymbolType, FnSymbol, StructSymbol, SymbolInfo } from "../parser/symbolTable"; +import { NodeChild } from "../parser/types"; import { CodeGenVisitor } from "./CodeGenVisitor"; import { ICodeSegment } from "./types"; import { VisitorContext } from "./VisitorContext"; @@ -37,10 +38,15 @@ export abstract class GLESVisitor extends CodeGenVisitor { this.reset(); const shaderData = node.shaderData; - VisitorContext.context._passSymbolTable = shaderData.symbolTable; + const context = VisitorContext.context; + context._passSymbolTable = shaderData.symbolTable; const outerGlobalMacroDeclarations = shaderData.getOuterGlobalMacroDeclarations(); + // Build combined _globalStructVarMap from both entry functions before per-stage processing. + // This must happen here because vertex runs first and doesn't yet know fragment's variables. + this._buildGlobalStructVarMap(vertexEntry, fragmentEntry, shaderData, outerGlobalMacroDeclarations, context); + return { vertex: this._vertexMain(vertexEntry, shaderData, outerGlobalMacroDeclarations), fragment: this._fragmentMain(fragmentEntry, shaderData, outerGlobalMacroDeclarations) @@ -109,6 +115,9 @@ export abstract class GLESVisitor extends CodeGenVisitor { } }); + // Pre-register global #define member access references for this stage + this._registerGlobalMacroReferences(outerGlobalMacroDeclarations, context); + const globalCodeArray = this._globalCodeArray; VisitorContext.context.referenceGlobal(entry, ESymbolType.FN); @@ -173,6 +182,9 @@ export abstract class GLESVisitor extends CodeGenVisitor { } }); + // Pre-register global #define member access references for this stage + this._registerGlobalMacroReferences(outerGlobalMacroStatements, context); + const globalCodeArray = this._globalCodeArray; VisitorContext.context.referenceGlobal(entry, ESymbolType.FN); @@ -193,10 +205,178 @@ export abstract class GLESVisitor extends CodeGenVisitor { return globalCode; } + /** + * Build _globalStructVarMap from both entry functions before per-stage processing. + * Classifies struct types by their position in function signatures: + * - vertex param[0] → attribute, vertex return type → varying + * - fragment param[0] → varying, fragment return type → mrt + */ + private _buildGlobalStructVarMap( + vertexEntry: string, + fragmentEntry: string, + data: ShaderData, + globalMacros: ASTNode.GlobalDeclaration[], + context: VisitorContext + ): void { + const map = context._globalStructVarMap; + const lookupSymbol = GLESVisitor._lookupSymbol; + const { symbolTable } = data; + + // Map struct type names to roles based on function signature positions + const structTypeRoles: Record = Object.create(null); + + // Vertex entry: param[0] type → attribute, return type → varying + lookupSymbol.set(vertexEntry, ESymbolType.FN); + const vertexFns = symbolTable.getSymbols(lookupSymbol, true, []); + for (const fn of vertexFns) { + const proto = fn.astNode.protoType; + const param0 = proto.parameterList?.[0]; + if (param0 && typeof param0.typeInfo.type === "string") { + structTypeRoles[param0.typeInfo.typeLexeme] = "attribute"; + } + if (typeof proto.returnType.type === "string") { + structTypeRoles[proto.returnType.type] = "varying"; + } + } + + // Fragment entry: param[0] type → varying, return type → mrt + lookupSymbol.set(fragmentEntry, ESymbolType.FN); + const fragmentFns = symbolTable.getSymbols(lookupSymbol, true, []); + for (const fn of fragmentFns) { + const proto = fn.astNode.protoType; + const param0 = proto.parameterList?.[0]; + if (param0 && typeof param0.typeInfo.type === "string") { + structTypeRoles[param0.typeInfo.typeLexeme] = "varying"; + } + if (typeof proto.returnType.type === "string") { + structTypeRoles[proto.returnType.type] = "mrt"; + } + } + + // Scan all entry functions' params and local vars, classify by structTypeRoles + for (const fn of [...vertexFns, ...fragmentFns]) { + const fnNode = fn.astNode; + const paramList = fnNode.protoType.parameterList; + if (paramList) { + for (const param of paramList) { + if (param.ident && param.typeInfo && typeof param.typeInfo.type === "string") { + const role = structTypeRoles[param.typeInfo.typeLexeme]; + if (role) map[param.ident.lexeme] = role; + } + } + } + this._collectStructVarsFromBody(fnNode.statements, structTypeRoles, map); + } + + // Also scan global macros for root variable names that might be global struct variables. + // e.g. #define VSOutput_worldPos o.v_worldPos → root "o" → look up in symbol table + let hasRoles = false; + for (const _ in structTypeRoles) { + hasRoles = true; + break; + } + if (hasRoles) { + const checked = new Set(); + const symOut: SymbolInfo[] = []; + this._forEachMacroMemberAccess(globalMacros, (rootName) => { + if (map[rootName] || checked.has(rootName)) return; + checked.add(rootName); + lookupSymbol.set(rootName, ESymbolType.VAR); + symbolTable.getSymbols(lookupSymbol, true, symOut); + for (const sym of symOut) { + if (sym.dataType) { + const role = structTypeRoles[sym.dataType.typeLexeme]; + if (role) { + map[rootName] = role; + break; + } + } + } + }); + } + } + + private _collectStructVarsFromBody( + node: TreeNode, + structTypeRoles: Record, + map: Record + ): void { + const children = node.children; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (child instanceof ASTNode.InitDeclaratorList) { + const typeLexeme = child.typeInfo?.typeLexeme; + if (typeLexeme) { + const role = structTypeRoles[typeLexeme]; + if (role) { + this._extractVarNamesFromInitDeclaratorList(child, map, role); + } + } + } else if (child instanceof TreeNode) { + this._collectStructVarsFromBody(child, structTypeRoles, map); + } + } + } + + /** + * Pre-register attribute/varying/mrt references from global #define member access patterns, + * so that declarations are emitted by _getCustomStruct for the current stage. + */ + private _registerGlobalMacroReferences(globalMacros: ASTNode.GlobalDeclaration[], context: VisitorContext): void { + const map = context._globalStructVarMap; + let hasEntries = false; + for (const _ in map) { + hasEntries = true; + break; + } + if (!hasEntries) return; + this._forEachMacroMemberAccess(globalMacros, (rootName, propName) => { + const role = map[rootName]; + if (role) context.referenceStructPropByName(role, propName); + }); + } + + /** + * Traverse global macro declarations, extracting member access patterns (e.g. "o.v_uv") + * and invoking the callback with (rootName, propName) for each match. + */ + private _forEachMacroMemberAccess( + macros: ASTNode.GlobalDeclaration[], + callback: (rootName: string, propName: string) => void + ): void { + const reg = CodeGenVisitor._memberAccessReg; + for (const macro of macros) { + this._walkMacroChildren(macro.children, reg, callback); + } + } + + private _walkMacroChildren( + children: NodeChild[], + reg: RegExp, + callback: (rootName: string, propName: string) => void + ): void { + for (const child of children) { + if (child instanceof BaseToken && child.type === Keyword.MACRO_DEFINE_EXPRESSION) { + const spaceIdx = child.lexeme.indexOf(" "); + if (spaceIdx === -1) continue; + const value = child.lexeme.substring(spaceIdx); + reg.lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = reg.exec(value)) !== null) { + callback(match[1], match[2]); + } + } else if (child instanceof TreeNode) { + this._walkMacroChildren(child.children, reg, callback); + } + } + } + private _getGlobalSymbol(out: ICodeSegment[]): void { - const { _referencedGlobals } = VisitorContext.context; + const context = VisitorContext.context; + const { _referencedGlobals } = context; - const lastLength = Object.keys(_referencedGlobals).length; + let lastLength = 0; + for (const _ in _referencedGlobals) lastLength++; if (lastLength === 0) return; for (const ident in _referencedGlobals) { @@ -206,7 +386,9 @@ export abstract class GLESVisitor extends CodeGenVisitor { const symbols = _referencedGlobals[ident]; for (let i = 0; i < symbols.length; i++) { const sm = symbols[i]; - const text = sm.astNode.codeGen(this) + (sm.type === ESymbolType.VAR ? ";" : ""); + const codeGenResult = sm.astNode.codeGen(this); + if (!codeGenResult) continue; + const text = codeGenResult + (sm.type === ESymbolType.VAR ? ";" : ""); if (!sm.isInMacroBranch) { out.push({ text, @@ -216,7 +398,9 @@ export abstract class GLESVisitor extends CodeGenVisitor { } } - if (Object.keys(_referencedGlobals).length !== lastLength) { + let newLength = 0; + for (const _ in _referencedGlobals) newLength++; + if (newLength !== lastLength) { this._getGlobalSymbol(out); } } @@ -233,6 +417,7 @@ export abstract class GLESVisitor extends CodeGenVisitor { private _getGlobalMacroDeclarations(macros: ASTNode.GlobalDeclaration[], out: ICodeSegment[]): void { const context = VisitorContext.context; + const globalMap = context._globalStructVarMap; const referencedGlobals = context._referencedGlobals; const referencedGlobalMacroASTs = context._referencedGlobalMacroASTs; referencedGlobalMacroASTs.length = 0; @@ -253,7 +438,12 @@ export abstract class GLESVisitor extends CodeGenVisitor { let result: ICodeSegment[] = []; result.push( ...macro.macroExpressions.map((item) => ({ - text: item instanceof BaseToken ? item.lexeme : item.codeGen(this), + text: + item instanceof BaseToken + ? item.type === Keyword.MACRO_DEFINE_EXPRESSION + ? this._transformMacroDefineValue(item.lexeme, globalMap) + : item.lexeme + : item.codeGen(this), index: item.location.start.index })) ); @@ -264,6 +454,8 @@ export abstract class GLESVisitor extends CodeGenVisitor { .sort((a, b) => a.index - b.index) .map((item) => item.text) .join("\n"); + } else if (child instanceof BaseToken && child.type === Keyword.MACRO_DEFINE_EXPRESSION) { + text = this._transformMacroDefineValue(child.lexeme, globalMap); } else { text = macro.codeGen(this); } diff --git a/packages/shader-lab/src/codeGen/VisitorContext.ts b/packages/shader-lab/src/codeGen/VisitorContext.ts index 25211f008a..1c1972c0db 100644 --- a/packages/shader-lab/src/codeGen/VisitorContext.ts +++ b/packages/shader-lab/src/codeGen/VisitorContext.ts @@ -38,6 +38,10 @@ export class VisitorContext { _referencedMRTList: Record; _referencedGlobals: Record; _referencedGlobalMacroASTs: TreeNode[] = []; + /** Maps variable names to their struct role for function-body #define value transformation. */ + _structVarMap: Record; + /** Combined mapping from all entry functions for global #define transformation. */ + _globalStructVarMap: Record; _passSymbolTable: SymbolTable; @@ -56,6 +60,36 @@ export class VisitorContext { this._referencedMRTList = Object.create(null); this._referencedGlobals = Object.create(null); this._referencedGlobalMacroASTs.length = 0; + this._structVarMap = Object.create(null); + if (resetAll) { + this._globalStructVarMap = Object.create(null); + } + } + + getStructRole(typeLexeme: string): "varying" | "attribute" | "mrt" | undefined { + if (this.isAttributeStruct(typeLexeme)) return "attribute"; + if (this.isVaryingStruct(typeLexeme)) return "varying"; + if (this.isMRTStruct(typeLexeme)) return "mrt"; + } + + registerStructVar(varName: string, role: "varying" | "attribute" | "mrt"): void { + this._structVarMap[varName] = role; + this._globalStructVarMap[varName] = role; + } + + referenceStructPropByName(role: "varying" | "attribute" | "mrt", propName: string): void { + const list = role === "varying" ? this.varyingList : role === "attribute" ? this.attributeList : this.mrtList; + const refList = + role === "varying" + ? this._referencedVaryingList + : role === "attribute" + ? this._referencedAttributeList + : this._referencedMRTList; + if (refList[propName]) return; + const props = list.filter((item) => item.ident.lexeme === propName); + if (props.length) { + refList[propName] = props; + } } isAttributeStruct(type: string) { diff --git a/packages/shader-lab/src/macroProcessor/MacroParser.ts b/packages/shader-lab/src/macroProcessor/MacroParser.ts index d845102e1a..a2ed90c504 100644 --- a/packages/shader-lab/src/macroProcessor/MacroParser.ts +++ b/packages/shader-lab/src/macroProcessor/MacroParser.ts @@ -355,7 +355,7 @@ export class MacroParser { scanner.advance(1); scanner.skipSpace(false); const parenExpr = this._parseParenthesisExpression(scanner); - if ((operator === "!" && typeof parenExpr !== "boolean") || (operator !== "!" && typeof parenExpr !== "number")) { + if (operator !== "!" && typeof parenExpr !== "number") { this._reportError(opPos, "invalid operator.", scanner.source, scanner.file); } diff --git a/packages/shader-lab/src/parser/AST.ts b/packages/shader-lab/src/parser/AST.ts index 4c90c01444..243944df8f 100644 --- a/packages/shader-lab/src/parser/AST.ts +++ b/packages/shader-lab/src/parser/AST.ts @@ -4,7 +4,7 @@ import { ETokenType, GalaceanDataType, ShaderRange, TokenType, TypeAny } from ". import { BaseToken } from "../common/BaseToken"; import { Keyword } from "../common/enums/Keyword"; import { ParserUtils } from "../ParserUtils"; -import { Preprocessor } from "../Preprocessor"; +import { MacroValueType, Preprocessor } from "../Preprocessor"; import { ShaderLabUtils } from "../ShaderLabUtils"; import { BuiltinFunction, BuiltinVariable, NonGenericGalaceanType } from "./builtin"; import { NoneTerminal } from "./GrammarSymbol"; @@ -1424,7 +1424,12 @@ export namespace ASTNode { sa.reportWarning(this.location, `Please sure the identifier "${name}" will be declared before used.`); // #endif } else { - this.typeInfo = symbols[0].dataType?.type; + // For member access macros (e.g. #define FRAG_UV v.v_uv), the referenceSymbolNames + // contains the root variable ("v") whose type is the struct ("Varyings"), not the + // member type ("vec2"). Skip type inference in this case — keep TypeAny. + if (child instanceof BaseToken || !this._isMemberAccessMacro(sa, child)) { + this.typeInfo = symbols[0].dataType?.type; + } const currentScopeSymbol = sa.symbolTableStack.scope.getSymbol(lookupSymbol, true); if (currentScopeSymbol) { if ( @@ -1443,6 +1448,12 @@ export namespace ASTNode { } } + private _isMemberAccessMacro(sa: SemanticAnalyzer, child: MacroCallSymbol | MacroCallFunction): boolean { + const macroName = child.macroName; + const infos = sa.macroDefineList[macroName]; + return infos?.some((info) => info.valueType === MacroValueType.MemberAccess) ?? false; + } + override codeGen(visitor: CodeGenVisitor): string { return this.setCache(visitor.visitVariableIdentifier(this)); } diff --git a/packages/shader-lab/src/parser/builtin/functions.ts b/packages/shader-lab/src/parser/builtin/functions.ts index 46c0d49d29..859951641c 100644 --- a/packages/shader-lab/src/parser/builtin/functions.ts +++ b/packages/shader-lab/src/parser/builtin/functions.ts @@ -26,6 +26,39 @@ function isGenericType(t: BuiltinType) { return t >= EGenType.GenType && t <= EGenType.GSampler2DArray; } +/** + * Resolve a generic return type from the actual type of a generic parameter. + * + * For GVec4 return type, maps sampler variants to the correct vec4 type: + * sampler2D/sampler3D/samplerCube → vec4 + * isampler2D/isampler3D/... → ivec4 + * usampler2D/usampler3D/... → uvec4 + * + * For all other generic return types (GenType etc.), passes through the actual param type directly. + */ +function resolveGenericReturnType( + genericReturnType: EGenType, + actualParamType: NonGenericGalaceanType +): NonGenericGalaceanType { + if (genericReturnType === EGenType.GVec4) { + switch (actualParamType) { + case Keyword.I_SAMPLER2D: + case Keyword.I_SAMPLER3D: + case Keyword.I_SAMPLER_CUBE: + case Keyword.I_SAMPLER2D_ARRAY: + return Keyword.IVEC4; + case Keyword.U_SAMPLER2D: + case Keyword.U_SAMPLER3D: + case Keyword.U_SAMPLER_CUBE: + case Keyword.U_SAMPLER2D_ARRAY: + return Keyword.UVEC4; + default: + return Keyword.VEC4; + } + } + return actualParamType; +} + const BuiltinFunctionTable: Map = new Map(); export class BuiltinFunction { @@ -78,12 +111,14 @@ export class BuiltinFunction { const argLength = fnArgs.length; if (argLength !== parameterTypes.length) continue; // Try to match generic parameter type. - let returnType = TypeAny; + let resolvedReturnType: NonGenericGalaceanType = TypeAny; let found = true; for (let i = 0; i < argLength; i++) { const curFnArg = fnArgs[i]; if (isGenericType(curFnArg)) { - if (returnType === TypeAny) returnType = parameterTypes[i]; + if (resolvedReturnType === TypeAny) { + resolvedReturnType = resolveGenericReturnType(fn._returnType as EGenType, parameterTypes[i]); + } } else { if (curFnArg !== parameterTypes[i] && parameterTypes[i] !== TypeAny) { found = false; @@ -92,7 +127,9 @@ export class BuiltinFunction { } } if (found) { - fn._realReturnType = returnType; + fn._realReturnType = isGenericType(fn._returnType) + ? resolvedReturnType + : (fn._returnType as NonGenericGalaceanType); return fn; } } @@ -300,13 +337,14 @@ BuiltinFunction._create("textureLod", EGenType.GVec4, EGenType.GSampler3D, Keywo BuiltinFunction._create("textureLod", EGenType.GVec4, EGenType.GSamplerCube, Keyword.VEC3, Keyword.FLOAT); BuiltinFunction._create("textureLod", Keyword.FLOAT, Keyword.SAMPLER2D_SHADOW, Keyword.VEC3, Keyword.FLOAT); BuiltinFunction._create("textureLod", EGenType.GVec4, EGenType.GSampler2DArray, Keyword.VEC3, Keyword.FLOAT); +BuiltinFunction._create("texture2DLod", Keyword.VEC4, Keyword.SAMPLER2D, Keyword.VEC2, Keyword.FLOAT); BuiltinFunction._create("texture2DLodEXT", EGenType.GVec4, EGenType.GSampler2D, Keyword.VEC2, Keyword.FLOAT); BuiltinFunction._create("texture2DLodEXT", EGenType.GVec4, EGenType.GSampler3D, Keyword.VEC3, Keyword.FLOAT); -BuiltinFunction._create("textureCube", Keyword.SAMPLER_CUBE, Keyword.VEC3); -BuiltinFunction._create("textureCube", Keyword.SAMPLER_CUBE, Keyword.VEC3, Keyword.FLOAT); +BuiltinFunction._create("textureCube", Keyword.VEC4, Keyword.SAMPLER_CUBE, Keyword.VEC3); +BuiltinFunction._create("textureCube", Keyword.VEC4, Keyword.SAMPLER_CUBE, Keyword.VEC3, Keyword.FLOAT); BuiltinFunction._create("textureCube", EGenType.GVec4, EGenType.GSamplerCube, Keyword.VEC3, Keyword.FLOAT); -BuiltinFunction._create("textureCubeLod", Keyword.SAMPLER_CUBE, Keyword.VEC3, Keyword.FLOAT); +BuiltinFunction._create("textureCubeLod", Keyword.VEC4, Keyword.SAMPLER_CUBE, Keyword.VEC3, Keyword.FLOAT); BuiltinFunction._create("textureCubeLodEXT", EGenType.GVec4, EGenType.GSamplerCube, Keyword.VEC3, Keyword.FLOAT); BuiltinFunction._create( diff --git a/packages/ui/src/component/UIRenderer.ts b/packages/ui/src/component/UIRenderer.ts index 59a2fc434c..71420f43bb 100644 --- a/packages/ui/src/component/UIRenderer.ts +++ b/packages/ui/src/component/UIRenderer.ts @@ -71,7 +71,7 @@ export class UIRenderer extends Renderer implements IGraphics { _subChunk; @assignmentClone - private _raycastEnabled: boolean = true; + private _raycastEnabled: boolean = false; @deepClone protected _color: Color = new Color(1, 1, 1, 1); diff --git a/packages/ui/src/component/advanced/Image.ts b/packages/ui/src/component/advanced/Image.ts index b8ca6ffe55..7f074fed75 100644 --- a/packages/ui/src/component/advanced/Image.ts +++ b/packages/ui/src/component/advanced/Image.ts @@ -1,6 +1,7 @@ import { BoundingBox, Entity, + FilledSpriteAssembler, ISpriteAssembler, ISpriteRenderer, MathUtil, @@ -10,6 +11,8 @@ import { SlicedSpriteAssembler, Sprite, SpriteDrawMode, + SpriteFilledMode, + SpriteFilledOrigin, SpriteModifyFlags, SpriteTileMode, TiledSpriteAssembler, @@ -21,6 +24,16 @@ import { RootCanvasModifyFlags } from "../UICanvas"; import { UIRenderer, UIRendererUpdateFlags } from "../UIRenderer"; import { UITransform, UITransformModifyFlags } from "../UITransform"; +/** + * Determines how the Image element's size is controlled relative to the sprite. + */ +export enum SpriteSizeMode { + /** The image size is controlled manually via UITransform (default, existing behavior). */ + Custom = 0, + /** The image size is automatically set to the sprite's natural dimensions when the sprite changes. */ + Automatic = 1 +} + /** * UI element that renders an image. */ @@ -35,6 +48,16 @@ export class Image extends UIRenderer implements ISpriteRenderer { private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; @assignmentClone private _tiledAdaptiveThreshold: number = 0.5; + @assignmentClone + private _filledMode: SpriteFilledMode = SpriteFilledMode.Radial360; + @assignmentClone + private _filledAmount: number = 1; + @assignmentClone + private _filledOrigin: SpriteFilledOrigin = SpriteFilledOrigin.Bottom; + @assignmentClone + private _filledClockWise: boolean = true; + @assignmentClone + private _sizeMode: SpriteSizeMode = SpriteSizeMode.Custom; /** * The draw mode of the image. @@ -56,6 +79,9 @@ export class Image extends UIRenderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._assembler = TiledSpriteAssembler; break; + case SpriteDrawMode.Filled: + this._assembler = FilledSpriteAssembler; + break; default: break; } @@ -64,6 +90,23 @@ export class Image extends UIRenderer implements ISpriteRenderer { } } + /** + * The size mode of the image. When set to `Automatic`, the UITransform size + * is automatically synchronized to the sprite's natural dimensions. + */ + get sizeMode(): SpriteSizeMode { + return this._sizeMode; + } + + set sizeMode(value: SpriteSizeMode) { + if (this._sizeMode !== value) { + this._sizeMode = value; + if (value === SpriteSizeMode.Automatic && this._sprite) { + this._applySpriteSize(); + } + } + } + /** * The tiling mode of the image. (Only works in tiled mode.) */ @@ -97,6 +140,73 @@ export class Image extends UIRenderer implements ISpriteRenderer { } } + /** + * The fill amount of the image, range from 0 to 1. (Only works in filled mode.) + */ + get filledAmount(): number { + return this._filledAmount; + } + + set filledAmount(value: number) { + value = MathUtil.clamp(value, 0, 1); + if (this._filledAmount !== value) { + this._filledAmount = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill mode of the image. (Only works in filled mode.) + */ + get filledMode(): SpriteFilledMode { + return this._filledMode; + } + + set filledMode(value: SpriteFilledMode) { + if (this._filledMode !== value) { + this._filledMode = value; + this._filledOrigin = + value === SpriteFilledMode.Radial90 ? SpriteFilledOrigin.BottomLeft : SpriteFilledOrigin.Bottom; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill origin of the image. (Only works in filled mode.) + */ + get filledOrigin(): SpriteFilledOrigin { + return this._filledOrigin; + } + + set filledOrigin(value: SpriteFilledOrigin) { + if (this._filledOrigin !== value) { + this._filledOrigin = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * Whether the fill is clockwise. (Only works in filled radial mode.) + */ + get filledClockWise(): boolean { + return this._filledClockWise; + } + + set filledClockWise(value: boolean) { + if (this._filledClockWise !== value) { + this._filledClockWise = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + /** * The Sprite to render. */ @@ -122,6 +232,9 @@ export class Image extends UIRenderer implements ISpriteRenderer { this.shaderData.setTexture(UIRenderer._textureProperty, null); } this._sprite = value; + if (this._sizeMode === SpriteSizeMode.Automatic && value) { + this._applySpriteSize(); + } } } @@ -274,6 +387,9 @@ export class Image extends UIRenderer implements ISpriteRenderer { this.shaderData.setTexture(UIRenderer._textureProperty, this.sprite.texture); break; case SpriteModifyFlags.size: + if (this._sizeMode === SpriteSizeMode.Automatic) { + this._applySpriteSize(); + } switch (this._drawMode) { case SpriteDrawMode.Sliced: this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; @@ -281,6 +397,9 @@ export class Image extends UIRenderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; break; + case SpriteDrawMode.Filled: + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; + break; default: break; } @@ -309,6 +428,13 @@ export class Image extends UIRenderer implements ISpriteRenderer { break; } } + + private _applySpriteSize(): void { + const sprite = this._sprite; + if (sprite) { + (this._transformEntity.transform).size.set(sprite.width, sprite.height); + } + } } /** diff --git a/packages/ui/src/component/index.ts b/packages/ui/src/component/index.ts index 1f89431265..fc93806fb6 100644 --- a/packages/ui/src/component/index.ts +++ b/packages/ui/src/component/index.ts @@ -3,7 +3,7 @@ export { UIGroup } from "./UIGroup"; export { UIRenderer } from "./UIRenderer"; export { UITransform } from "./UITransform"; export { Button } from "./advanced/Button"; -export { Image } from "./advanced/Image"; +export { Image, SpriteSizeMode } from "./advanced/Image"; export { Text } from "./advanced/Text"; export { ColorTransition } from "./interactive/transition/ColorTransition"; export { ScaleTransition } from "./interactive/transition/ScaleTransition"; diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index c37d476a92..9a255a5184 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -1046,6 +1046,48 @@ describe("Animator test", function () { expect(animator.entity.clone().getComponent(Animator).animatorController).to.eq(animator.animatorController); }); + it("samples self-name-prefixed curve paths on wrapped roots", () => { + const wrappedRoot = new Entity(engine, "GLTF_ROOT"); + const hips = new Entity(engine, "mixamorig:Hips"); + const spine = new Entity(engine, "mixamorig:Spine"); + hips.parent = wrappedRoot; + spine.parent = hips; + + const clip = new AnimationClip("idle"); + const hipsCurve = new AnimationFloatCurve(); + const spineCurve = new AnimationFloatCurve(); + const hipsStart = new Keyframe(); + const hipsEnd = new Keyframe(); + hipsStart.time = 0; + hipsStart.value = 0; + hipsEnd.time = 0.1; + hipsEnd.value = 1; + hipsCurve.addKey(hipsStart); + hipsCurve.addKey(hipsEnd); + + const spineStart = new Keyframe(); + const spineEnd = new Keyframe(); + spineStart.time = 0; + spineStart.value = 0; + spineEnd.time = 0.1; + spineEnd.value = 1; + spineCurve.addKey(spineStart); + spineCurve.addKey(spineEnd); + + clip.addCurveBinding("mixamorig:Hips", Transform, "position.x", hipsCurve); + clip.addCurveBinding("mixamorig:Hips/mixamorig:Spine", Transform, "position.y", spineCurve); + + expect(wrappedRoot.findByPath("mixamorig:Hips")).to.eq(hips); + expect(wrappedRoot.findByPath("mixamorig:Hips/mixamorig:Spine")).to.eq(spine); + + // @ts-ignore + clip._sampleAnimation(wrappedRoot, 0.1); + + expect(wrappedRoot.transform.position.x).to.eq(0); + expect(hips.transform.position.x).to.eq(1); + expect(spine.transform.position.y).to.eq(1); + }); + it("anyState transition interrupts crossFade", () => { const { animatorController } = animator; animatorController.addParameter("interrupt", false); diff --git a/tests/src/core/Camera.test.ts b/tests/src/core/Camera.test.ts index 52ae7890d1..5f77077f7f 100644 --- a/tests/src/core/Camera.test.ts +++ b/tests/src/core/Camera.test.ts @@ -245,10 +245,64 @@ describe("camera test", function () { expect(Math.abs(ray.direction.z)).not.eq(Infinity); }); + it("screenPointToRay should ignore inherited scale from parent entity", () => { + // Simulate UICanvas scenario: camera is a child of a scaled parent entity + const scene = engine.sceneManager.scenes[0]; + const parentEntity = scene.createRootEntity("scaledParent"); + parentEntity.transform.setScale(1.5, 1.5, 1.5); + parentEntity.transform.setPosition(100, 200, 0); + + const childEntity = parentEntity.createChild("cameraChild"); + childEntity.transform.setPosition(0, 0, 500); + const scaledCamera = childEntity.addComponent(Camera); + scaledCamera.isOrthographic = true; + scaledCamera.orthographicSize = 5; + scaledCamera.nearClipPlane = 0.1; + scaledCamera.farClipPlane = 1000; + + // A camera without inherited scale at the same world position/rotation for comparison + const refEntity = scene.createRootEntity("refCamera"); + refEntity.transform.setWorldPosition( + childEntity.transform.worldPosition.x, + childEntity.transform.worldPosition.y, + childEntity.transform.worldPosition.z + ); + const refCamera = refEntity.addComponent(Camera); + refCamera.isOrthographic = true; + refCamera.orthographicSize = 5; + refCamera.nearClipPlane = 0.1; + refCamera.farClipPlane = 1000; + + // Both cameras should produce the same ray for the same screen point + const screenPoint = new Vector2(128, 128); + const rayScaled = scaledCamera.screenPointToRay(screenPoint, new Ray()); + const rayRef = refCamera.screenPointToRay(screenPoint, new Ray()); + + expect(rayScaled.origin.x).to.be.closeTo(rayRef.origin.x, 0.001); + expect(rayScaled.origin.y).to.be.closeTo(rayRef.origin.y, 0.001); + expect(rayScaled.direction.x).to.be.closeTo(rayRef.direction.x, 0.001); + expect(rayScaled.direction.y).to.be.closeTo(rayRef.direction.y, 0.001); + expect(rayScaled.direction.z).to.be.closeTo(rayRef.direction.z, 0.001); + + // Round-trip: worldToViewportPoint -> viewportToWorldPoint should be accurate + const worldPoint = new Vector3(105, 210, 0); + const viewportPoint = scaledCamera.worldToViewportPoint(worldPoint, new Vector3()); + const recoveredPoint = scaledCamera.viewportToWorldPoint(viewportPoint, new Vector3()); + expect(recoveredPoint.x).to.be.closeTo(worldPoint.x, 0.01); + expect(recoveredPoint.y).to.be.closeTo(worldPoint.y, 0.01); + expect(recoveredPoint.z).to.be.closeTo(worldPoint.z, 0.01); + + // Clean up + scaledCamera.destroy(); + refCamera.destroy(); + parentEntity.destroy(); + refEntity.destroy(); + }); + /* Attention: - Below methods will change the default view of current Camera. - If executed in advance, it will affect the expected results of other test cases, + Below methods will change the default view of current Camera. + If executed in advance, it will affect the expected results of other test cases, so it should be placed at the end of the test case execution. */ it("projection matrix", () => { diff --git a/tests/src/core/Entity.test.ts b/tests/src/core/Entity.test.ts index 2fe8f908ad..eba73370c0 100644 --- a/tests/src/core/Entity.test.ts +++ b/tests/src/core/Entity.test.ts @@ -320,6 +320,19 @@ describe("Entity", async () => { expect(parent.findByPath("child/grandson")).eq(grandson2); }); + it("findByPath accepts self-name prefix", () => { + const parent = new Entity(engine, "parent"); + parent.parent = scene.getRootEntity(); + const child = new Entity(engine, "child"); + child.parent = parent; + const grandson = new Entity(engine, "grandson"); + grandson.parent = child; + + expect(parent.findByPath("parent")).eq(parent); + expect(parent.findByPath("parent/child")).eq(child); + expect(parent.findByPath("parent/child/grandson")).eq(grandson); + }); + it("clearChildren", () => { const parent = new Entity(engine, "parent"); @@ -681,4 +694,4 @@ describe("Entity", async () => { expect(script.onDestroy).toHaveBeenCalledTimes(1); }); }); -}); \ No newline at end of file +}); diff --git a/tests/src/core/ShaderData.test.ts b/tests/src/core/ShaderData.test.ts new file mode 100644 index 0000000000..6903c67150 --- /dev/null +++ b/tests/src/core/ShaderData.test.ts @@ -0,0 +1,184 @@ +import { CloneManager, ShaderData, ShaderDataGroup, ShaderMacro } from "@galacean/engine-core"; +import { CloneMode } from "@galacean/engine-core/src/clone/enums/CloneMode"; +import { describe, expect, it } from "vitest"; + +describe("ShaderData", () => { + describe("Macro operations", () => { + it("enableMacro and disableMacro", () => { + const shaderData = new ShaderData(ShaderDataGroup.Renderer); + const macro = ShaderMacro.getByName("TEST_ENABLE_DISABLE"); + + shaderData.enableMacro("TEST_ENABLE_DISABLE"); + expect(shaderData._macroCollection.isEnable(macro)).to.be.true; + + const macros = shaderData.getMacros() as ShaderMacro[]; + expect(macros).to.have.lengthOf(1); + expect(macros[0]).to.equal(macro); + + shaderData.disableMacro("TEST_ENABLE_DISABLE"); + expect(shaderData._macroCollection.isEnable(macro)).to.be.false; + expect(shaderData.getMacros() as ShaderMacro[]).to.have.lengthOf(0); + }); + + it("enableMacro with value replaces same-name macro", () => { + const shaderData = new ShaderData(ShaderDataGroup.Renderer); + + shaderData.enableMacro("TEST_VALUE_MACRO", "1"); + const macro1 = ShaderMacro.getByName("TEST_VALUE_MACRO", "1"); + expect(shaderData._macroCollection.isEnable(macro1)).to.be.true; + + shaderData.enableMacro("TEST_VALUE_MACRO", "2"); + const macro2 = ShaderMacro.getByName("TEST_VALUE_MACRO", "2"); + expect(shaderData._macroCollection.isEnable(macro1)).to.be.false; + expect(shaderData._macroCollection.isEnable(macro2)).to.be.true; + expect(shaderData.getMacros() as ShaderMacro[]).to.have.lengthOf(1); + }); + }); + + describe("cloneTo", () => { + it("should produce identical macros in target", () => { + const source = new ShaderData(ShaderDataGroup.Renderer); + source.enableMacro("CLONE_MACRO_A"); + source.enableMacro("CLONE_MACRO_B"); + + const target = new ShaderData(ShaderDataGroup.Renderer); + source.cloneTo(target); + + const macroA = ShaderMacro.getByName("CLONE_MACRO_A"); + const macroB = ShaderMacro.getByName("CLONE_MACRO_B"); + expect(target._macroCollection.isEnable(macroA)).to.be.true; + expect(target._macroCollection.isEnable(macroB)).to.be.true; + + const targetMacros = target.getMacros() as ShaderMacro[]; + expect(targetMacros).to.have.lengthOf(2); + }); + + it("should clear stale macros in target before cloning", () => { + const source = new ShaderData(ShaderDataGroup.Renderer); + source.enableMacro("SOURCE_ONLY_MACRO"); + + const target = new ShaderData(ShaderDataGroup.Renderer); + target.enableMacro("TARGET_STALE_MACRO"); + + source.cloneTo(target); + + const staleMacro = ShaderMacro.getByName("TARGET_STALE_MACRO"); + const sourceMacro = ShaderMacro.getByName("SOURCE_ONLY_MACRO"); + + expect(target._macroCollection.isEnable(staleMacro)).to.be.false; + const targetMacros = target.getMacros() as ShaderMacro[]; + expect(targetMacros).to.have.lengthOf(1); + expect(targetMacros[0]).to.equal(sourceMacro); + + const macroMap = (target as any)._macroMap; + for (const key in macroMap) { + expect(Number(key)).to.equal(macroMap[key]._nameId); + } + }); + + it("should not have duplicate macros with same name under different keys", () => { + const target = new ShaderData(ShaderDataGroup.Renderer); + target.enableMacro("MACRO_X"); + target.enableMacro("MACRO_Y"); + target.enableMacro("MACRO_Z"); + + const source = new ShaderData(ShaderDataGroup.Renderer); + source.enableMacro("MACRO_Y"); + + source.cloneTo(target); + + const targetMacros = target.getMacros() as ShaderMacro[]; + expect(targetMacros).to.have.lengthOf(1); + expect(targetMacros[0].name).to.equal("MACRO_Y"); + + const names = targetMacros.map((m) => m.name); + const uniqueNames = [...new Set(names)]; + expect(names.length).to.equal(uniqueNames.length); + }); + + it("clone() should produce a clean independent copy", () => { + const source = new ShaderData(ShaderDataGroup.Renderer); + source.enableMacro("CLONE_INDEPENDENT_A"); + source.enableMacro("CLONE_INDEPENDENT_B"); + + const cloned = source.clone(); + + const macroA = ShaderMacro.getByName("CLONE_INDEPENDENT_A"); + const macroB = ShaderMacro.getByName("CLONE_INDEPENDENT_B"); + expect(cloned._macroCollection.isEnable(macroA)).to.be.true; + expect(cloned._macroCollection.isEnable(macroB)).to.be.true; + + source.disableMacro("CLONE_INDEPENDENT_A"); + expect(cloned._macroCollection.isEnable(macroA)).to.be.true; + }); + }); + + describe("CloneManager", () => { + it("default cloneMode deep-clones same-constructor objects", () => { + const sourceObj = { name: "B", id: 2 }; + const targetObj = { name: "A", id: 1 }; + const source = { _field: sourceObj }; + const target = { _field: targetObj }; + + CloneManager.cloneProperty(source, target, "_field", undefined, null, null, new Map()); + + expect(target._field).to.equal(targetObj); + expect(targetObj.name).to.equal("B"); + expect(targetObj.id).to.equal(2); + }); + + it("CloneMode.Assignment prevents singleton corruption", () => { + const macroA = ShaderMacro.getByName("SINGLETON_FIX_A"); + const macroB = ShaderMacro.getByName("SINGLETON_FIX_B"); + + const source = { _macro: macroB }; + const target = { _macro: macroA }; + + CloneManager.cloneProperty(source, target, "_macro", CloneMode.Assignment, null, null, new Map()); + + expect(macroA.name).to.equal("SINGLETON_FIX_A"); + expect(macroA._nameId).to.not.equal(macroB._nameId); + expect(target._macro).to.equal(macroB); + }); + + it("should not infinite loop on circular references", () => { + // Simulate AnimatorState ↔ AnimatorStateTransition cycle: + // State has transitions array, each Transition has destinationState pointing back to a State + class FakeState { + transitions: FakeTransition[] = []; + } + class FakeTransition { + destinationState: FakeState = null; + } + + // Build circular graph: stateA → transitionAB → stateB → transitionBA → stateA + const srcStateA = new FakeState(); + const srcStateB = new FakeState(); + const srcTransAB = new FakeTransition(); + const srcTransBA = new FakeTransition(); + srcTransAB.destinationState = srcStateB; + srcTransBA.destinationState = srcStateA; + srcStateA.transitions = [srcTransAB]; + srcStateB.transitions = [srcTransBA]; + + // Target has its own independent state graph + const tgtStateA = new FakeState(); + const tgtStateB = new FakeState(); + const tgtTransAB = new FakeTransition(); + const tgtTransBA = new FakeTransition(); + tgtTransAB.destinationState = tgtStateB; + tgtTransBA.destinationState = tgtStateA; + tgtStateA.transitions = [tgtTransAB]; + tgtStateB.transitions = [tgtTransBA]; + + const source = { state: srcStateA }; + const target = { state: tgtStateA }; + + // This must not hang — should complete within milliseconds + CloneManager.cloneProperty(source, target, "state", CloneMode.Deep, null, null, new Map()); + + // After clone, target's state graph should reflect source's data + expect(target.state.transitions).to.have.lengthOf(1); + }); + }); +}); diff --git a/tests/src/loader/GLTFLoader.test.ts b/tests/src/loader/GLTFLoader.test.ts index 7b35b7b330..402f2150c5 100644 --- a/tests/src/loader/GLTFLoader.test.ts +++ b/tests/src/loader/GLTFLoader.test.ts @@ -481,6 +481,17 @@ describe("glTF Loader test", function () { expect(renderer).to.exist; expect(renderer.blendShapeWeights).to.deep.include([1, 1]); }); + + it("single-root animation root channel should bind to the root node path", async () => { + const glTFResource: GLTFResource = await engine.resourceManager.load({ + type: AssetType.GLTF, + url: "mock/path/testA.gltf" + }); + + const clip = glTFResource.animations?.[0]; + expect(clip).to.exist; + expect(clip.curveBindings[0].relativePath).to.equal("entity1"); + }); }); describe("glTF instance test", function () { diff --git a/tests/src/shader-lab/ShaderLab.test.ts b/tests/src/shader-lab/ShaderLab.test.ts index e998533e5e..b1d5085044 100644 --- a/tests/src/shader-lab/ShaderLab.test.ts +++ b/tests/src/shader-lab/ShaderLab.test.ts @@ -247,8 +247,128 @@ describe("ShaderLab", async () => { glslValidate(engine, shaderSource, shaderLabRelease); }); + it("macro-negate-number (!0, !1 in #if expressions)", async () => { + const shaderSource = await readFile("./shaders/macro-negate-number.shader"); + glslValidate(engine, shaderSource, shaderLabVerbose); + glslValidate(engine, shaderSource, shaderLabRelease); + }); + it("mrt-struct", async () => { const shaderSource = await readFile("./shaders/mrt-struct.shader"); glslValidate(engine, shaderSource, shaderLabRelease); }); + + it("texture-generic (GVec4 → vec4 resolve)", async () => { + const shaderSource = await readFile("./shaders/texture-generic.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + }); + + it("generic-return-type (builtin generic return as arg to user function)", async () => { + const shaderSource = await readFile("./shaders/generic-return-type.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + }); + + it("define-struct-access-global (global #define with struct member access)", async () => { + const shaderSource = await readFile("./shaders/define-struct-access-global.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + + const shader = shaderLabVerbose._parseShaderSource(shaderSource); + const passSource = shader.subShaders[0].passes[0]; + const { vertex, fragment } = shaderLabVerbose._parseShaderPass( + passSource.contents, + passSource.vertexEntry, + passSource.fragmentEntry, + 0, + "" + )!; + + const expectedVert = await readFile("./expected/define-struct-access-global.vert.glsl"); + const expectedFrag = await readFile("./expected/define-struct-access-global.frag.glsl"); + expect(vertex).to.equal(expectedVert); + expect(fragment).to.equal(expectedFrag); + }); + + it("define-struct-access (function-body #define with struct member access)", async () => { + const shaderSource = await readFile("./shaders/define-struct-access.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + + const shader = shaderLabVerbose._parseShaderSource(shaderSource); + const passSource = shader.subShaders[0].passes[0]; + const { vertex, fragment } = shaderLabVerbose._parseShaderPass( + passSource.contents, + passSource.vertexEntry, + passSource.fragmentEntry, + 0, + "" + )!; + + const expectedVert = await readFile("./expected/define-struct-access.vert.glsl"); + const expectedFrag = await readFile("./expected/define-struct-access.frag.glsl"); + expect(vertex).to.equal(expectedVert); + expect(fragment).to.equal(expectedFrag); + }); + + it("macro-member-access-builtin-arg (Cocos FSInput pattern: member access macro as builtin fn arg)", async () => { + const shaderSource = await readFile("./shaders/macro-member-access-builtin-arg.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + + // Also verify verbose mode (semantic analysis) succeeds — this was the original bug: + // member access macros like #define FSInput_worldNormal v.v_normal.xyz resolved to + // struct type "Varyings" instead of TypeAny, causing builtin overload matching to fail. + const shader = shaderLabVerbose._parseShaderSource(shaderSource); + const passSource = shader.subShaders[0].passes[0]; + const { vertex, fragment } = shaderLabVerbose._parseShaderPass( + passSource.contents, + passSource.vertexEntry, + passSource.fragmentEntry, + 0, + "" + )!; + + expect(vertex).to.be.a("string").and.not.empty; + expect(fragment).to.be.a("string").and.not.empty; + + // Verify key builtins are present in output (macros expanded correctly) + expect(fragment).to.contain("normalize"); + expect(fragment).to.contain("dot"); + expect(fragment).to.contain("texture2D"); + }); + + it("global-varying-var (Cocos VSOutput pattern: global Varyings var with #define macros)", async () => { + const shaderSource = await readFile("./shaders/global-varying-var.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + + // Verify verbose mode: global "Varyings o;" should not produce "uniform Varyings o;" + // and should not duplicate varying declarations. + const shader = shaderLabVerbose._parseShaderSource(shaderSource); + const passSource = shader.subShaders[0].passes[0]; + const { vertex, fragment } = shaderLabVerbose._parseShaderPass( + passSource.contents, + passSource.vertexEntry, + passSource.fragmentEntry, + 0, + "" + )!; + + expect(vertex).to.be.a("string").and.not.empty; + expect(fragment).to.be.a("string").and.not.empty; + + // No "uniform Varyings o;" in output + expect(vertex).to.not.contain("uniform Varyings"); + expect(fragment).to.not.contain("uniform Varyings"); + + // Macros should be transformed: "o.v_worldPos" → "v_worldPos" + expect(vertex).to.contain("#define VSOutput_worldPos v_worldPos"); + expect(vertex).to.contain("#define VSOutput_worldNormal v_normal.xyz"); + + // No duplicate varying declarations + const varyingMatches = vertex.match(/varying vec3 v_worldPos/g); + expect(varyingMatches).to.have.lengthOf(1); + }); + + it("frag-return-vec4 (Cocos pattern: fragment entry returns vec4 instead of void)", async () => { + const shaderSource = await readFile("./shaders/frag-return-vec4.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + glslValidate(engine, shaderSource, shaderLabVerbose); + }); }); diff --git a/tests/src/shader-lab/expected/define-struct-access-global.frag.glsl b/tests/src/shader-lab/expected/define-struct-access-global.frag.glsl new file mode 100644 index 0000000000..e766b6e6a1 --- /dev/null +++ b/tests/src/shader-lab/expected/define-struct-access-global.frag.glsl @@ -0,0 +1,10 @@ +varying vec2 v_uv; + +uniform sampler2D u_texture; +#define ATTR_POS POSITION + +#define VARYING_UV v_uv + +#define FRAG_UV v_uv + +void main() { gl_FragColor = texture2D ( u_texture , FRAG_UV ) ; } \ No newline at end of file diff --git a/tests/src/shader-lab/expected/define-struct-access-global.vert.glsl b/tests/src/shader-lab/expected/define-struct-access-global.vert.glsl new file mode 100644 index 0000000000..782ad3ee52 --- /dev/null +++ b/tests/src/shader-lab/expected/define-struct-access-global.vert.glsl @@ -0,0 +1,16 @@ +uniform mat4 renderer_MVPMat; +attribute vec4 POSITION; +attribute vec2 TEXCOORD_0; + +varying vec2 v_uv; + +#define ATTR_POS POSITION + +#define VARYING_UV v_uv + +#define FRAG_UV v_uv + +void main() { +gl_Position = renderer_MVPMat * ATTR_POS ; +VARYING_UV = TEXCOORD_0 ; + } \ No newline at end of file diff --git a/tests/src/shader-lab/expected/define-struct-access.frag.glsl b/tests/src/shader-lab/expected/define-struct-access.frag.glsl new file mode 100644 index 0000000000..2c8ba502ac --- /dev/null +++ b/tests/src/shader-lab/expected/define-struct-access.frag.glsl @@ -0,0 +1,11 @@ +varying vec2 v_uv; +varying vec3 v_normal; + +uniform sampler2D u_texture; +uniform vec3 u_lightDir; +void main() { #define FRAG_UV v_uv + +#define FRAG_NORMAL v_normal + +float NdotL = dot ( FRAG_NORMAL , u_lightDir ) ; +gl_FragColor = texture2D ( u_texture , FRAG_UV ) * NdotL ; } \ No newline at end of file diff --git a/tests/src/shader-lab/expected/define-struct-access.vert.glsl b/tests/src/shader-lab/expected/define-struct-access.vert.glsl new file mode 100644 index 0000000000..1ee6d28522 --- /dev/null +++ b/tests/src/shader-lab/expected/define-struct-access.vert.glsl @@ -0,0 +1,18 @@ +uniform mat4 renderer_MVPMat; +attribute vec4 POSITION; +attribute vec2 TEXCOORD_0; + +varying vec2 v_uv; +varying vec3 v_normal; + +void main() { +#define ATTR_POS POSITION + +#define VARYING_UV v_uv + +#define VARYING_NORMAL v_normal + +gl_Position = renderer_MVPMat * ATTR_POS ; +VARYING_UV = TEXCOORD_0 ; +VARYING_NORMAL = vec3 ( 0.0 , 1.0 , 0.0 ) ; + } \ No newline at end of file diff --git a/tests/src/shader-lab/shaders/define-struct-access-global.shader b/tests/src/shader-lab/shaders/define-struct-access-global.shader new file mode 100644 index 0000000000..a8abffe570 --- /dev/null +++ b/tests/src/shader-lab/shaders/define-struct-access-global.shader @@ -0,0 +1,31 @@ +Shader "define-struct-access-global-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec4 POSITION; vec2 TEXCOORD_0; }; + struct Varyings { vec2 v_uv; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + + // Global #define referencing entry function parameter names + #define ATTR_POS attr.POSITION + #define VARYING_UV o.v_uv + #define FRAG_UV v.v_uv + + Varyings vert(Attributes attr) { + Varyings o; + gl_Position = renderer_MVPMat * ATTR_POS; + VARYING_UV = attr.TEXCOORD_0; + return o; + } + + void frag(Varyings v) { + gl_FragColor = texture2D(u_texture, FRAG_UV); + } + } + } +} diff --git a/tests/src/shader-lab/shaders/define-struct-access.shader b/tests/src/shader-lab/shaders/define-struct-access.shader new file mode 100644 index 0000000000..a28171dc40 --- /dev/null +++ b/tests/src/shader-lab/shaders/define-struct-access.shader @@ -0,0 +1,34 @@ +Shader "define-struct-access-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec4 POSITION; vec2 TEXCOORD_0; }; + struct Varyings { vec2 v_uv; vec3 v_normal; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + vec3 u_lightDir; + + Varyings vert(Attributes attr) { + Varyings o; + #define ATTR_POS attr.POSITION + #define VARYING_UV o.v_uv + #define VARYING_NORMAL o.v_normal + gl_Position = renderer_MVPMat * ATTR_POS; + VARYING_UV = attr.TEXCOORD_0; + VARYING_NORMAL = vec3(0.0, 1.0, 0.0); + return o; + } + + void frag(Varyings v) { + #define FRAG_UV v.v_uv + #define FRAG_NORMAL v.v_normal + float NdotL = dot(FRAG_NORMAL, u_lightDir); + gl_FragColor = texture2D(u_texture, FRAG_UV) * NdotL; + } + } + } +} diff --git a/tests/src/shader-lab/shaders/frag-return-vec4.shader b/tests/src/shader-lab/shaders/frag-return-vec4.shader new file mode 100644 index 0000000000..9b3ea3d4ca --- /dev/null +++ b/tests/src/shader-lab/shaders/frag-return-vec4.shader @@ -0,0 +1,41 @@ +Shader "frag-return-vec4-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec3 POSITION; vec3 NORMAL; vec2 TEXCOORD_0; }; + struct Varyings { vec3 v_worldPos; vec4 v_normal; vec2 v_uv; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + vec3 u_lightDir; + + vec3 SRGBToLinear(vec3 gamma) { return gamma * gamma; } + vec3 LinearToSRGB(vec3 linear) { return sqrt(linear); } + + vec4 SurfacesFragmentModifyBaseColorAndTransparency(Varyings input) { + vec4 color = texture2D(u_texture, input.v_uv); + color.rgb = SRGBToLinear(color.rgb); + return color; + } + + Varyings vert(Attributes attr) { + Varyings o; + vec4 pos = renderer_MVPMat * vec4(attr.POSITION, 1.0); + o.v_worldPos = pos.xyz; + o.v_normal = vec4(attr.NORMAL, 1.0); + o.v_uv = attr.TEXCOORD_0; + gl_Position = pos; + return o; + } + + vec4 frag(Varyings input) { + vec4 color = SurfacesFragmentModifyBaseColorAndTransparency(input); + color.rgb = LinearToSRGB(color.rgb); + return color; + } + } + } +} diff --git a/tests/src/shader-lab/shaders/generic-return-type.shader b/tests/src/shader-lab/shaders/generic-return-type.shader new file mode 100644 index 0000000000..72e96b7f3d --- /dev/null +++ b/tests/src/shader-lab/shaders/generic-return-type.shader @@ -0,0 +1,52 @@ +Shader "generic-return-type-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec4 POSITION; }; + struct Varyings { vec2 uv; vec3 worldNormal; vec3 worldTangent; }; + + VertexShader = vert; + FragmentShader = frag; + + // Test: user-defined function with concrete parameter types + vec3 CalculateNormalFromTangentSpace(vec3 normalFromTangentSpace, float normalStrength, vec3 normal, vec3 tangent, float mirrorNormal) { + vec3 binormal = cross(normal, tangent) * mirrorNormal; + return (normalFromTangentSpace.x * normalStrength) * normalize(tangent) + + (normalFromTangentSpace.y * normalStrength) * normalize(binormal) + + normalFromTangentSpace.z * normalize(normal); + } + + sampler2D u_normalMap; + vec4 u_scaleAndStrength; + + Varyings vert(Attributes attr) { + Varyings o; + gl_Position = renderer_MVPMat * attr.POSITION; + o.uv = attr.POSITION.xy; + o.worldNormal = attr.POSITION.xyz; + o.worldTangent = attr.POSITION.xyz; + return o; + } + + void frag(Varyings v) { + vec3 normal = v.worldNormal; + + // Test: builtin generic functions (normalize) on swizzled values, + // passed as arguments to a user-defined function. + // normalize(vec3) should not produce EGenType.GenType (200) as return type, + // it should produce TypeAny so overload matching succeeds. + vec3 nmmp = texture(u_normalMap, v.uv).xyz - vec3(0.5); + normal = CalculateNormalFromTangentSpace( + nmmp, + u_scaleAndStrength.w, + normalize(normal.xyz), + normalize(v.worldTangent), + 1.0 + ); + + gl_FragColor = vec4(normalize(normal), 1.0); + } + } + } +} diff --git a/tests/src/shader-lab/shaders/global-varying-var.shader b/tests/src/shader-lab/shaders/global-varying-var.shader new file mode 100644 index 0000000000..bd6a5cf477 --- /dev/null +++ b/tests/src/shader-lab/shaders/global-varying-var.shader @@ -0,0 +1,42 @@ +Shader "global-varying-var-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec3 POSITION; vec3 NORMAL; vec2 TEXCOORD_0; }; + struct Varyings { vec3 v_worldPos; vec4 v_normal; vec2 v_uv; vec4 v_shadowBiasAndProbeId; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + vec3 u_lightDir; + + #define VSOutput_worldPos o.v_worldPos + #define VSOutput_worldNormal o.v_normal.xyz + #define VSOutput_faceSideSign o.v_normal.w + #define VSOutput_texcoord o.v_uv + #define VSOutput_shadowBias o.v_shadowBiasAndProbeId.xy + + Varyings o; + + Varyings vert(Attributes input) { + mat4 matWorld = renderer_MVPMat; + vec4 pos = matWorld * vec4(input.POSITION, 1.0); + VSOutput_worldPos = pos.xyz; + VSOutput_worldNormal = input.NORMAL; + VSOutput_faceSideSign = 1.0; + VSOutput_texcoord = input.TEXCOORD_0; + VSOutput_shadowBias = vec2(0.0, 0.0); + gl_Position = pos; + return o; + } + + void frag(Varyings v) { + vec3 N = normalize(v.v_normal.xyz); + float NdotL = dot(N, u_lightDir); + gl_FragColor = texture2D(u_texture, v.v_uv) * NdotL; + } + } + } +} diff --git a/tests/src/shader-lab/shaders/macro-member-access-builtin-arg.shader b/tests/src/shader-lab/shaders/macro-member-access-builtin-arg.shader new file mode 100644 index 0000000000..70ae36f3bc --- /dev/null +++ b/tests/src/shader-lab/shaders/macro-member-access-builtin-arg.shader @@ -0,0 +1,49 @@ +Shader "macro-member-access-builtin-arg-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec4 POSITION; vec3 NORMAL; vec2 TEXCOORD_0; }; + struct Varyings { vec2 v_uv; vec4 v_normal; vec3 v_worldPos; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + vec3 u_lightDir; + vec3 u_cameraPos; + + // Cocos-style FSInput macros: member access used as builtin function args + #define FSInput_worldNormal v.v_normal.xyz + #define FSInput_faceSideSign v.v_normal.w + #define FSInput_worldPos v.v_worldPos + #define FSInput_texcoord v.v_uv + + Varyings vert(Attributes attr) { + Varyings o; + gl_Position = renderer_MVPMat * attr.POSITION; + o.v_uv = attr.TEXCOORD_0; + o.v_normal = vec4(attr.NORMAL, 1.0); + o.v_worldPos = attr.POSITION.xyz; + return o; + } + + void frag(Varyings v) { + // normalize() with member access macro as arg + vec3 N = normalize(FSInput_worldNormal); + + // dot() with member access macro as arg + float NdotL = dot(N, u_lightDir); + + // texture2D() with member access macro as arg + vec4 albedo = texture2D(u_texture, FSInput_texcoord); + + // mix() with member access macro and scalar macro + vec3 viewDir = normalize(u_cameraPos - FSInput_worldPos); + float rim = 1.0 - dot(N, viewDir); + + gl_FragColor = albedo * NdotL + vec4(vec3(rim), 0.0); + } + } + } +} diff --git a/tests/src/shader-lab/shaders/macro-negate-number.shader b/tests/src/shader-lab/shaders/macro-negate-number.shader new file mode 100644 index 0000000000..a224605393 --- /dev/null +++ b/tests/src/shader-lab/shaders/macro-negate-number.shader @@ -0,0 +1,41 @@ +Shader "macro-negate-number-test" { + SubShader "default" { + Pass "default" { + struct a2v { + vec4 POSITION; + }; + + mat4 renderer_MVPMat; + + #define SCENE_FOG_MODE 1 + + VertexShader = vert; + FragmentShader = frag; + + void vert(a2v v) { + gl_Position = renderer_MVPMat * v.POSITION; + } + + void frag() { + vec4 color = vec4(1.0); + + // Test: !0 should evaluate to true (like C preprocessor) + #if SCENE_FOG_MODE != 4 && (!0 || SCENE_FOG_MODE) + color = vec4(0.0, 1.0, 0.0, 1.0); + #endif + + // Test: !1 should evaluate to false + #if !1 + color = vec4(1.0, 0.0, 0.0, 1.0); + #endif + + // Test: !0 alone + #if !0 + color = vec4(0.0, 0.0, 1.0, 1.0); + #endif + + gl_FragColor = color; + } + } + } +} diff --git a/tests/src/shader-lab/shaders/texture-generic.shader b/tests/src/shader-lab/shaders/texture-generic.shader new file mode 100644 index 0000000000..a27789175c --- /dev/null +++ b/tests/src/shader-lab/shaders/texture-generic.shader @@ -0,0 +1,94 @@ +Shader "texture-generic-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec4 POSITION; }; + struct Varyings { vec2 uv; }; + + VertexShader = vert; + FragmentShader = frag; + + // Test: user-defined function taking vec4, called with texture() return value. + // texture(sampler2D, vec2) returns GVec4 which must resolve to vec4. + float decode32(vec4 rgba) { + rgba = rgba * 255.0; + float Sign = 1.0 - step(128.0, rgba[3] + 0.5) * 2.0; + float Exponent = 2.0 * mod(float(int(rgba[3] + 0.5)), 128.0) + step(128.0, rgba[2] + 0.5) - 127.0; + float Mantissa = mod(float(int(rgba[2] + 0.5)), 128.0) * 65536.0 + rgba[1] * 256.0 + rgba[0] + 8388608.0; + return Sign * exp2(Exponent - 23.0) * Mantissa; + } + + sampler2D u_texture; + + // Test: texture() result passed to user function (GVec4 → vec4 resolve) + mat4 getJointMatrix(float i) { + float x = 4.0 * i; + vec4 v1 = vec4( + decode32(texture(u_texture, vec2((x + 0.5) / 1024.0, 0.5))), + decode32(texture(u_texture, vec2((x + 1.5) / 1024.0, 0.5))), + decode32(texture(u_texture, vec2((x + 2.5) / 1024.0, 0.5))), + decode32(texture(u_texture, vec2((x + 3.5) / 1024.0, 0.5))) + ); + return mat4(v1, v1, v1, vec4(0.0, 0.0, 0.0, 1.0)); + } + + // Test: texture() result used directly in arithmetic (GVec4 → vec4) + vec4 sampleAndScale(sampler2D tex, vec2 coord, float scale) { + vec4 color = texture(tex, coord); + return color * scale; + } + + // Test: textureLod also returns GVec4 + vec4 sampleLod(sampler2D tex, vec2 coord, float lod) { + vec4 color = textureLod(tex, coord, lod); + return color; + } + + // Test: texture2DLod (ES 1.0 function, concrete types) + vec4 sampleLod2D(sampler2D tex, vec2 coord, float lod) { + return texture2DLod(tex, coord, lod); + } + + // Test: texture2DLod result passed to user function + float decodeLod2D(sampler2D tex, vec2 coord) { + return decode32(texture2DLod(tex, coord, 0.0)); + } + + // Test: texture() with samplerCube (GVec4 → vec4 resolve via GSamplerCube) + samplerCube u_cubeMap; + vec4 sampleCube(samplerCube tex, vec3 dir) { + return texture(tex, dir); + } + + // Test: textureLod with samplerCube (GVec4 → vec4 resolve via GSamplerCube) + vec4 sampleCubeLod(samplerCube tex, vec3 dir, float lod) { + return textureLod(tex, dir, lod); + } + + // Test: texture(samplerCube) result passed to user function (vec4 param matching) + float decodeCube(vec4 rgba) { + return rgba.r + rgba.g; + } + float sampleAndDecode(samplerCube tex, vec3 dir) { + return decodeCube(texture(tex, dir)); + } + + Varyings vert(Attributes attr) { + Varyings o; + gl_Position = renderer_MVPMat * attr.POSITION; + o.uv = attr.POSITION.xy; + return o; + } + + void frag(Varyings v) { + vec4 sampled = sampleAndScale(u_texture, v.uv, 1.0); + vec4 lodSampled = sampleLod(u_texture, v.uv, 0.0); + vec4 cubeSampled = sampleCube(u_cubeMap, vec3(v.uv, 1.0)); + vec4 cubeLodSampled = sampleCubeLod(u_cubeMap, vec3(v.uv, 1.0), 0.0); + float decoded = sampleAndDecode(u_cubeMap, vec3(v.uv, 1.0)); + gl_FragColor = sampled + lodSampled + cubeSampled + cubeLodSampled + vec4(decoded); + } + } + } +}