mirror of
https://github.com/galacean/engine.git
synced 2026-06-05 19:27:46 +08:00
Merge remote-tracking branch 'origin/dev/2.0' into feat/gpu-instancing
This commit is contained in:
@@ -113,11 +113,20 @@ animator.speed = 0;
|
||||
animator.speed = 1;
|
||||
```
|
||||
|
||||
If you only want to pause a specific `AnimatorState` , you can do so by setting its speed to 0.
|
||||
If you only want to pause a specific `AnimatorState` on this Animator instance, set the per-instance speed to 0.
|
||||
|
||||
```typescript
|
||||
const state = animator.findAnimatorState("xxx");
|
||||
if (!state) {
|
||||
// State not found in any layer
|
||||
return;
|
||||
}
|
||||
|
||||
// Pause only this Animator instance's playback of the state.
|
||||
state.speed = 0;
|
||||
|
||||
// To resume later, write any non-zero value:
|
||||
// state.speed = 1;
|
||||
```
|
||||
|
||||
### Transition to Specified Animation State
|
||||
@@ -130,26 +139,34 @@ animator.crossFade("OtherStateName", 0.3);
|
||||
|
||||
### Get Current Playing Animation State
|
||||
|
||||
You can use the [getCurrentAnimatorState](/apis/core/#Animator-getCurrentAnimatorState) method to get the currently playing `AnimatorState` . The parameter is the index `layerIndex` of the `AnimatorControllerLayer` where the `AnimatorState` is located. For details, see the [API documentation](/apis/core/#Animator-getCurrentAnimatorState). After obtaining it, you can set the properties of the `AnimatorState` , such as changing the default loop playback to play once.
|
||||
[getCurrentAnimatorState](/apis/core/#Animator-getCurrentAnimatorState) returns the playing `AnimatorStateInstance` on a given layer, or `null` when the layer is missing or nothing is playing.
|
||||
|
||||
```typescript
|
||||
const currentState = animator.getCurrentAnimatorState(0);
|
||||
// Play once
|
||||
currentState.wrapMode = WrapMode.Once;
|
||||
// Loop playback
|
||||
currentState.wrapMode = WrapMode.Loop;
|
||||
const current = animator.getCurrentAnimatorState(0);
|
||||
if (current) {
|
||||
current.speed = 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
### Get Animation State
|
||||
|
||||
You can use the [findAnimatorState](/apis/core/#Animator-findAnimatorState) method to get the specified `AnimatorState` . After obtaining it, you can set the properties of the `AnimatorState` , such as changing the default loop playback to play once.
|
||||
[findAnimatorState](/apis/core/#Animator-findAnimatorState) returns the per-Animator `AnimatorStateInstance` (`AnimatorStateInstance | null`). Writes only affect this Animator; the shared `AnimatorState` on the controller is untouched. Same pattern as `Renderer.getInstanceMaterial`.
|
||||
|
||||
- `speed`, `wrapMode` are per-instance overrideable; unwritten reads return the shared default.
|
||||
- `name`, `clip`, `clipStartTime`, `clipEndTime` read through to the shared asset.
|
||||
|
||||
To mutate the shared asset (broadcasts to every Animator), go through the controller path:
|
||||
|
||||
```typescript
|
||||
const state = animator.findAnimatorState("xxx");
|
||||
// Play once
|
||||
if (!state) return;
|
||||
|
||||
// Per-instance overrides.
|
||||
state.speed = 0.5;
|
||||
state.wrapMode = WrapMode.Once;
|
||||
// Loop playback
|
||||
state.wrapMode = WrapMode.Loop;
|
||||
|
||||
// Broadcast to every Animator using this controller.
|
||||
animator.animatorController.layers[0].stateMachine.findStateByName("xxx").clip = otherClip;
|
||||
```
|
||||
|
||||
### Animation Culling
|
||||
|
||||
@@ -117,11 +117,20 @@ animator.speed = 1;
|
||||
```
|
||||
|
||||
|
||||
如果你只想针对某一个 `动画状态` 进行暂停,可以通过将它的速度设置为 0 来实现。
|
||||
如果你只想针对当前 `Animator` 上的某个 `动画状态` 进行暂停,将它的 per-instance 速度设为 0 即可。
|
||||
|
||||
```typescript
|
||||
const state = animator.findAnimatorState("xxx");
|
||||
if (!state) {
|
||||
// 任何一个动画层都没有该状态
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅暂停当前 Animator 实例的该状态播放。
|
||||
state.speed = 0;
|
||||
|
||||
// 想要恢复时写回任意非零速度即可:
|
||||
// state.speed = 1;
|
||||
```
|
||||
|
||||
### 过渡指定动画状态
|
||||
@@ -135,26 +144,34 @@ animator.crossFade("OtherStateName", 0.3);
|
||||
|
||||
### 获取当前在播放的动画状态
|
||||
|
||||
你可以使用 [getCurrentAnimatorState](/apis/core/#Animator-getCurrentAnimatorState) 方法来获取当前正在播放的 `动画状态`。参数为 `动画状态` 所在 `动画层` 的序号`layerIndex`, 详见[API 文档](/apis/core/#Animator-getCurrentAnimatorState)。获取之后可以设置 `动画状态` 的属性,比如将默认的循环播放改为一次。
|
||||
[getCurrentAnimatorState](/apis/core/#Animator-getCurrentAnimatorState) 返回指定层当前播放的 `AnimatorStateInstance`,层不存在或未在播放时返回 `null`。
|
||||
|
||||
```typescript
|
||||
const currentState = animator.getCurrentAnimatorState(0);
|
||||
// 播放一次
|
||||
currentState.wrapMode = WrapMode.Once;
|
||||
// 循环播放
|
||||
currentState.wrapMode = WrapMode.Loop;
|
||||
const current = animator.getCurrentAnimatorState(0);
|
||||
if (current) {
|
||||
current.speed = 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
### 获取动画状态
|
||||
|
||||
你可以使用 [findAnimatorState](/apis/core/#Animator-findAnimatorState) 方法来获取指定名称的 `动画状态` 。获取之后可以设置动画状态的属性,比如将默认的循环播放改为一次。
|
||||
[findAnimatorState](/apis/core/#Animator-findAnimatorState) 返回当前 `Animator` 独有的 `AnimatorStateInstance`(`AnimatorStateInstance | null`)。对它的写入只影响当前 Animator,共享 `AnimatorState` 资产不受影响。模式与 `Renderer.getInstanceMaterial` 一致。
|
||||
|
||||
- `speed`、`wrapMode` 可逐实例覆盖;未写入前透传到共享默认值。
|
||||
- `name` / `clip` / `clipStartTime` / `clipEndTime` 从共享资产转发。
|
||||
|
||||
要修改共享资产(广播到所有使用该控制器的 Animator),通过控制器路径访问:
|
||||
|
||||
```typescript
|
||||
const state = animator.findAnimatorState("xxx");
|
||||
// 播放一次
|
||||
if (!state) return;
|
||||
|
||||
// 只影响当前 Animator
|
||||
state.speed = 0.5;
|
||||
state.wrapMode = WrapMode.Once;
|
||||
// 循环播放
|
||||
state.wrapMode = WrapMode.Loop;
|
||||
|
||||
// 广播到所有使用该控制器的 Animator
|
||||
animator.animatorController.layers[0].stateMachine.findStateByName("xxx").clip = otherClip;
|
||||
```
|
||||
|
||||
### 动画裁剪
|
||||
|
||||
@@ -56,7 +56,11 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
|
||||
const additivePoseNames = animations.filter((clip) => clip.name.includes("pose")).map((clip) => clip.name);
|
||||
|
||||
additivePoseNames.forEach((name) => {
|
||||
const clip = animator.findAnimatorState(name).clip;
|
||||
const state = animator.findAnimatorState(name);
|
||||
if (!state) {
|
||||
throw new Error(`Animator state not found: ${name}`);
|
||||
}
|
||||
const clip = state.clip;
|
||||
const newState = animatorStateMachine.addState(name);
|
||||
newState.clipStartTime = 1;
|
||||
newState.clip = clip;
|
||||
|
||||
@@ -53,6 +53,9 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
|
||||
const animator = defaultSceneRoot.getComponent(Animator);
|
||||
|
||||
const state = animator.findAnimatorState("walk");
|
||||
if (!state) {
|
||||
throw new Error("Animator state not found: walk");
|
||||
}
|
||||
const clip = state.clip;
|
||||
|
||||
const event0 = new AnimationEvent();
|
||||
|
||||
@@ -33,7 +33,11 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
|
||||
const { defaultSceneRoot } = gltfResource;
|
||||
rootEntity.addChild(defaultSceneRoot);
|
||||
const animator = defaultSceneRoot.getComponent(Animator);
|
||||
animator.findAnimatorState("walk").speed = -1;
|
||||
const walkState = animator.findAnimatorState("walk");
|
||||
if (!walkState) {
|
||||
throw new Error("Animator state not found: walk");
|
||||
}
|
||||
walkState.speed = -1;
|
||||
animator.play("walk");
|
||||
updateForE2E(engine);
|
||||
|
||||
|
||||
@@ -54,9 +54,12 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
|
||||
const animator = defaultSceneRoot.getComponent(Animator)!;
|
||||
animator.animatorController.addParameter("playerSpeed", 1);
|
||||
const stateMachine = animator.animatorController.layers[0].stateMachine;
|
||||
const idleState = animator.findAnimatorState("idle");
|
||||
const walkState = animator.findAnimatorState("walk");
|
||||
const runState = animator.findAnimatorState("run");
|
||||
const idleState = stateMachine.findStateByName("idle");
|
||||
const walkState = stateMachine.findStateByName("walk");
|
||||
const runState = stateMachine.findStateByName("run");
|
||||
if (!idleState || !walkState || !runState) {
|
||||
throw new Error("Required animator states not found: idle/walk/run");
|
||||
}
|
||||
let idleToWalkTime = 0;
|
||||
let walkToRunTime = 0;
|
||||
let runToWalkTime = 0;
|
||||
@@ -70,7 +73,9 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
|
||||
idleState.addTransition(toWalkTransition);
|
||||
idleToWalkTime =
|
||||
//@ts-ignore
|
||||
toWalkTransition.exitTime * idleState._getDuration() + toWalkTransition.duration * walkState._getDuration();
|
||||
toWalkTransition.exitTime * idleState._getDuration() +
|
||||
//@ts-ignore
|
||||
toWalkTransition.duration * walkState._getDuration();
|
||||
|
||||
const exitTransition = idleState.addExitTransition();
|
||||
exitTransition.addCondition("playerSpeed", AnimatorConditionMode.Equals, 0);
|
||||
@@ -97,16 +102,16 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
|
||||
toIdleTransition.duration * idleState._getDuration();
|
||||
|
||||
// to run state
|
||||
const RunToWalkTransition = new AnimatorStateTransition();
|
||||
RunToWalkTransition.destinationState = walkState;
|
||||
RunToWalkTransition.duration = 0.3;
|
||||
RunToWalkTransition.addCondition("playerSpeed", AnimatorConditionMode.Less, 0.5);
|
||||
runState.addTransition(RunToWalkTransition);
|
||||
const runToWalkTransition = new AnimatorStateTransition();
|
||||
runToWalkTransition.destinationState = walkState;
|
||||
runToWalkTransition.duration = 0.3;
|
||||
runToWalkTransition.addCondition("playerSpeed", AnimatorConditionMode.Less, 0.5);
|
||||
runState.addTransition(runToWalkTransition);
|
||||
runToWalkTime =
|
||||
//@ts-ignore
|
||||
(RunToWalkTransition.exitTime - toRunTransition.duration) * runState._getDuration() +
|
||||
(runToWalkTransition.exitTime - toRunTransition.duration) * runState._getDuration() +
|
||||
//@ts-ignore
|
||||
RunToWalkTransition.duration * walkState._getDuration();
|
||||
runToWalkTransition.duration * walkState._getDuration();
|
||||
|
||||
stateMachine.addEntryStateTransition(idleState);
|
||||
|
||||
|
||||
@@ -55,9 +55,12 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
|
||||
rootEntity.addChild(defaultSceneRoot);
|
||||
|
||||
const animator = defaultSceneRoot.getComponent(Animator);
|
||||
const state = animator.findAnimatorState("walk");
|
||||
const walkState = animator.animatorController.layers[0].stateMachine.findStateByName("walk");
|
||||
if (!walkState) {
|
||||
throw new Error("Animator state not found: walk");
|
||||
}
|
||||
|
||||
state.addStateMachineScript(
|
||||
walkState.addStateMachineScript(
|
||||
class extends StateMachineScript {
|
||||
onStateEnter(animator: Animator, animatorState: AnimatorState, layerIndex: number): void {
|
||||
textRenderer.text = "0";
|
||||
|
||||
@@ -45,10 +45,11 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
|
||||
rootEntity.addChild(defaultSceneRoot);
|
||||
const animator = defaultSceneRoot.getComponent(Animator)!;
|
||||
|
||||
animator.play("Right");
|
||||
const state = animator.getCurrentAnimatorState(0);
|
||||
// clipStartTime/clipEndTime are on the shared AnimatorState asset, not the per-Animator instance view.
|
||||
const state = animator.animatorController.layers[0].stateMachine.findStateByName("Right");
|
||||
state.clipStartTime = 1;
|
||||
state.clipEndTime = 1;
|
||||
animator.play("Right");
|
||||
|
||||
updateForE2E(engine);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@galacean/engine-e2e",
|
||||
"private": true,
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"case": "vite serve .dev --config .dev/vite.config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-examples",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"main": "dist/main.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-root",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"packageManager": "pnpm@9.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-core",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -105,6 +105,8 @@ export class Entity extends EngineObject {
|
||||
/** @internal */
|
||||
_scripts: DisorderedArray<Script> = new DisorderedArray<Script>();
|
||||
/** @internal */
|
||||
_scriptsVersion = 0;
|
||||
/** @internal */
|
||||
_children: Entity[] = [];
|
||||
/** @internal */
|
||||
_scene: Scene;
|
||||
@@ -341,7 +343,7 @@ export class Entity extends EngineObject {
|
||||
* @deprecated Please use `children` property instead.
|
||||
* Find child entity by index.
|
||||
* @param index - The index of the child entity
|
||||
* @returns The component which be found
|
||||
* @returns The entity that was found
|
||||
*/
|
||||
getChild(index: number): Entity {
|
||||
return this._children[index];
|
||||
@@ -349,8 +351,8 @@ export class Entity extends EngineObject {
|
||||
|
||||
/**
|
||||
* Find entity by name.
|
||||
* @param name - The name of the entity which want to be found
|
||||
* @returns The component which be found
|
||||
* @param name - The name of the entity to find
|
||||
* @returns The entity that was found
|
||||
*/
|
||||
findByName(name: string): Entity {
|
||||
if (name === this.name) {
|
||||
@@ -369,14 +371,13 @@ export class Entity extends EngineObject {
|
||||
/**
|
||||
* Find the entity by path.
|
||||
* @param path - The path of the entity eg: /entity
|
||||
* @returns The component which be found
|
||||
* @returns The entity that was found
|
||||
*/
|
||||
findByPath(path: string): Entity {
|
||||
const splits = path.split("/").filter(Boolean);
|
||||
if (!splits.length) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return Entity._findChildByName(this, 0, splits, 0);
|
||||
}
|
||||
|
||||
@@ -538,6 +539,7 @@ export class Entity extends EngineObject {
|
||||
_addScript(script: Script) {
|
||||
script._entityScriptsIndex = this._scripts.length;
|
||||
this._scripts.add(script);
|
||||
this._scriptsVersion++;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -547,6 +549,7 @@ export class Entity extends EngineObject {
|
||||
const replaced = this._scripts.deleteByIndex(script._entityScriptsIndex);
|
||||
replaced && (replaced._entityScriptsIndex = script._entityScriptsIndex);
|
||||
script._entityScriptsIndex = -1;
|
||||
this._scriptsVersion++;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,9 @@ import { Utils } from "./Utils";
|
||||
* @internal
|
||||
*/
|
||||
export class UpdateFlagManager {
|
||||
/** @internal */
|
||||
/** Monotonic counter bumped on every `dispatch`; consumers can snapshot it for lazy pull-style cache invalidation. */
|
||||
version = 0;
|
||||
|
||||
_updateFlags: UpdateFlag[] = [];
|
||||
|
||||
private _listeners: ((type?: number, param?: Object) => void)[] = [];
|
||||
@@ -62,6 +64,8 @@ export class UpdateFlagManager {
|
||||
* @param param - Event param
|
||||
*/
|
||||
dispatch(type?: number, param?: Object): void {
|
||||
this.version++;
|
||||
|
||||
const updateFlags = this._updateFlags;
|
||||
for (let i = updateFlags.length - 1; i >= 0; i--) {
|
||||
updateFlags[i].dispatch(type, param);
|
||||
|
||||
@@ -6,11 +6,11 @@ import { Renderer } from "../Renderer";
|
||||
import { Script } from "../Script";
|
||||
import { Logger } from "../base/Logger";
|
||||
import { assignmentClone, ignoreClone } from "../clone/CloneManager";
|
||||
import { ClearableObjectPool } from "../utils/ClearableObjectPool";
|
||||
import { AnimatorController } from "./AnimatorController";
|
||||
import { AnimatorControllerLayer } from "./AnimatorControllerLayer";
|
||||
import { AnimatorControllerParameter, AnimatorControllerParameterValue } from "./AnimatorControllerParameter";
|
||||
import { AnimatorState } from "./AnimatorState";
|
||||
import { AnimatorStateInstance } from "./AnimatorStateInstance";
|
||||
import { AnimatorStateTransition } from "./AnimatorStateTransition";
|
||||
import { AnimatorStateTransitionCollection } from "./AnimatorStateTransitionCollection";
|
||||
import { KeyframeValueType } from "./Keyframe";
|
||||
@@ -31,6 +31,7 @@ import { AnimationCurveOwner } from "./internal/animationCurveOwner/AnimationCur
|
||||
*/
|
||||
export class Animator extends Component {
|
||||
private static _passedTriggerParameterNames = new Array<string>();
|
||||
private static _tempScripts: Script[] = [];
|
||||
|
||||
/** Culling mode of this Animator. */
|
||||
cullingMode: AnimatorCullingMode = AnimatorCullingMode.None;
|
||||
@@ -55,8 +56,6 @@ export class Animator extends Component {
|
||||
@ignoreClone
|
||||
private _curveOwnerPool: Record<number, Record<string, AnimationCurveOwner<KeyframeValueType>>> = Object.create(null);
|
||||
@ignoreClone
|
||||
private _animationEventHandlerPool = new ClearableObjectPool(AnimationEventHandler);
|
||||
@ignoreClone
|
||||
private _parametersValueMap = <Record<string, AnimatorControllerParameterValue>>Object.create(null);
|
||||
|
||||
@ignoreClone
|
||||
@@ -115,9 +114,7 @@ export class Animator extends Component {
|
||||
* @param normalizedTimeOffset - The normalized time offset (between 0 and 1, default 0) to start the state's animation from
|
||||
*/
|
||||
play(stateName: string, layerIndex: number = -1, normalizedTimeOffset: number = 0): void {
|
||||
if (this._controllerUpdateFlag?.flag) {
|
||||
this._reset();
|
||||
}
|
||||
this._resetIfControllerUpdated();
|
||||
|
||||
const stateInfo = this._getAnimatorStateInfo(stateName, layerIndex);
|
||||
const { state } = stateInfo;
|
||||
@@ -192,9 +189,7 @@ export class Animator extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._controllerUpdateFlag?.flag) {
|
||||
this._reset();
|
||||
}
|
||||
this._resetIfControllerUpdated();
|
||||
|
||||
this._updateMark++;
|
||||
|
||||
@@ -206,20 +201,29 @@ export class Animator extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the playing state from the target layerIndex.
|
||||
* Get the state instance currently playing on the target layer.
|
||||
* @param layerIndex - The layer index
|
||||
* @returns The state instance, or null if nothing is playing
|
||||
* @remarks The returned instance is tied to the current controller's layer data. After a controller structure change (layers added or removed), the instance is invalidated; re-call this method to get a fresh one.
|
||||
*/
|
||||
getCurrentAnimatorState(layerIndex: number): AnimatorState {
|
||||
return this._animatorLayersData[layerIndex]?.srcPlayData?.state;
|
||||
getCurrentAnimatorState(layerIndex: number): AnimatorStateInstance | null {
|
||||
this._resetIfControllerUpdated();
|
||||
return this._animatorLayersData[layerIndex]?.srcPlayData?.instance ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state by name.
|
||||
* Get the state instance for a named state on this Animator.
|
||||
* Overrides on the returned instance only affect this Animator.
|
||||
* @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
|
||||
* @param layerIndex - The layer index (default -1, searches all layers)
|
||||
* @returns The state instance, or null if no state matches
|
||||
* @remarks The returned instance is tied to the current controller's layer data. After a controller structure change (layers added or removed), the instance is invalidated; re-call this method to get a fresh one.
|
||||
*/
|
||||
findAnimatorState(stateName: string, layerIndex: number = -1): AnimatorState {
|
||||
return this._getAnimatorStateInfo(stateName, layerIndex).state;
|
||||
findAnimatorState(stateName: string, layerIndex: number = -1): AnimatorStateInstance | null {
|
||||
this._resetIfControllerUpdated();
|
||||
const { state, layerIndex: foundLayer } = this._getAnimatorStateInfo(stateName, layerIndex);
|
||||
if (!state) return null;
|
||||
return this._getAnimatorLayerData(foundLayer).getOrCreateInstance(state);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,13 +328,18 @@ export class Animator extends Component {
|
||||
this._animatorLayersData.length = 0;
|
||||
this._curveOwnerPool = Object.create(null);
|
||||
this._parametersValueMap = Object.create(null);
|
||||
this._animationEventHandlerPool.clear();
|
||||
|
||||
if (this._controllerUpdateFlag) {
|
||||
this._controllerUpdateFlag.flag = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _resetIfControllerUpdated(): void {
|
||||
if (this._controllerUpdateFlag?.flag) {
|
||||
this._reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -358,11 +367,12 @@ export class Animator extends Component {
|
||||
normalizedTimeOffset: number,
|
||||
isFixedDuration: boolean
|
||||
): void {
|
||||
if (this._controllerUpdateFlag?.flag) {
|
||||
this._reset();
|
||||
}
|
||||
this._resetIfControllerUpdated();
|
||||
|
||||
const { state, layerIndex: playLayerIndex } = this._getAnimatorStateInfo(stateName, layerIndex);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const { manuallyTransition } = this._getAnimatorLayerData(playLayerIndex);
|
||||
manuallyTransition.duration = duration;
|
||||
|
||||
@@ -388,8 +398,10 @@ export class Animator extends Component {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if (layerIndex >= 0 && layerIndex < layers.length) {
|
||||
state = layers[layerIndex].stateMachine.findStateByName(stateName);
|
||||
} else {
|
||||
layerIndex = -1;
|
||||
}
|
||||
}
|
||||
stateInfo.layerIndex = layerIndex;
|
||||
@@ -398,19 +410,18 @@ export class Animator extends Component {
|
||||
}
|
||||
|
||||
private _getAnimatorStateData(
|
||||
stateName: string,
|
||||
animatorState: AnimatorState,
|
||||
animatorLayerData: AnimatorLayerData,
|
||||
layerIndex: number
|
||||
): AnimatorStateData {
|
||||
const { animatorStateDataMap } = animatorLayerData;
|
||||
let animatorStateData = animatorStateDataMap[stateName];
|
||||
let animatorStateData = animatorStateDataMap.get(animatorState);
|
||||
if (!animatorStateData) {
|
||||
animatorStateData = new AnimatorStateData();
|
||||
animatorStateDataMap[stateName] = animatorStateData;
|
||||
animatorStateData = new AnimatorStateData(animatorState);
|
||||
animatorStateDataMap.set(animatorState, animatorStateData);
|
||||
this._saveAnimatorStateData(animatorState, animatorStateData, animatorLayerData, layerIndex);
|
||||
this._saveAnimatorEventHandlers(animatorState, animatorStateData);
|
||||
}
|
||||
this._ensureEventHandlers(animatorState, animatorStateData);
|
||||
return animatorStateData;
|
||||
}
|
||||
|
||||
@@ -467,34 +478,41 @@ export class Animator extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
private _saveAnimatorEventHandlers(state: AnimatorState, animatorStateData: AnimatorStateData): void {
|
||||
const eventHandlerPool = this._animationEventHandlerPool;
|
||||
const scripts = [];
|
||||
private _ensureEventHandlers(state: AnimatorState, animatorStateData: AnimatorStateData): void {
|
||||
// state._updateFlagManager dispatches on both clip-swap and clip-events-mutation,
|
||||
// so its version covers every input that affects eventHandlers binding
|
||||
const stateVersion = state._updateFlagManager.version;
|
||||
const scriptsVersion = this._entity._scriptsVersion;
|
||||
if (
|
||||
animatorStateData.eventsBuiltVersion === stateVersion &&
|
||||
animatorStateData.eventsBuiltScriptsVersion === scriptsVersion
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scripts = Animator._tempScripts;
|
||||
this._entity.getComponents(Script, scripts);
|
||||
const scriptCount = scripts.length;
|
||||
const { events } = state.clip;
|
||||
const { eventHandlers } = animatorStateData;
|
||||
eventHandlers.length = 0;
|
||||
for (let i = 0, n = events.length; i < n; i++) {
|
||||
const event = events[i];
|
||||
const eventHandler = new AnimationEventHandler();
|
||||
const funcName = event.functionName;
|
||||
const { handlers } = eventHandler;
|
||||
|
||||
const clipChangedListener = () => {
|
||||
this._entity.getComponents(Script, scripts);
|
||||
const scriptCount = scripts.length;
|
||||
const { events } = state.clip;
|
||||
eventHandlers.length = 0;
|
||||
for (let i = 0, n = events.length; i < n; i++) {
|
||||
const event = events[i];
|
||||
const eventHandler = eventHandlerPool.get();
|
||||
const funcName = event.functionName;
|
||||
const { handlers } = eventHandler;
|
||||
|
||||
eventHandler.event = event;
|
||||
handlers.length = 0;
|
||||
for (let j = scriptCount - 1; j >= 0; j--) {
|
||||
const script = scripts[j];
|
||||
const handler = <Function>script[funcName]?.bind(script);
|
||||
handler && handlers.push(handler);
|
||||
}
|
||||
eventHandlers.push(eventHandler);
|
||||
eventHandler.event = event;
|
||||
for (let j = scriptCount - 1; j >= 0; j--) {
|
||||
const script = scripts[j];
|
||||
const handler = <Function>script[funcName]?.bind(script);
|
||||
handler && handlers.push(handler);
|
||||
}
|
||||
};
|
||||
clipChangedListener();
|
||||
state._updateFlagManager.addListener(clipChangedListener);
|
||||
eventHandlers.push(eventHandler);
|
||||
}
|
||||
scripts.length = 0;
|
||||
animatorStateData.eventsBuiltVersion = stateVersion;
|
||||
animatorStateData.eventsBuiltScriptsVersion = scriptsVersion;
|
||||
}
|
||||
|
||||
private _clearCrossData(animatorLayerData: AnimatorLayerData): void {
|
||||
@@ -521,8 +539,8 @@ export class Animator extends Component {
|
||||
}
|
||||
|
||||
private _prepareStandbyCrossFading(animatorLayerData: AnimatorLayerData): void {
|
||||
// Standby have two sub state, one is never play, one is finished, never play srcPlayData.state is null
|
||||
animatorLayerData.srcPlayData.state && this._prepareSrcCrossData(animatorLayerData, true);
|
||||
// Standby have two sub state, one is never play (srcPlayData is null), one is finished (srcPlayData is non-null)
|
||||
animatorLayerData.srcPlayData && this._prepareSrcCrossData(animatorLayerData, true);
|
||||
// Add dest cross curve data
|
||||
this._prepareDestCrossData(animatorLayerData, true);
|
||||
}
|
||||
@@ -614,9 +632,9 @@ export class Animator extends Component {
|
||||
aniUpdate: boolean
|
||||
): void {
|
||||
const { srcPlayData } = layerData;
|
||||
const { state } = srcPlayData;
|
||||
const state = srcPlayData.instance._state;
|
||||
|
||||
const playSpeed = state.speed * this.speed;
|
||||
const playSpeed = srcPlayData.instance.speed * this.speed;
|
||||
const playDeltaTime = playSpeed * deltaTime;
|
||||
|
||||
srcPlayData.updateOrientation(playDeltaTime);
|
||||
@@ -699,8 +717,9 @@ export class Animator extends Component {
|
||||
);
|
||||
|
||||
if (transition) {
|
||||
// Remove speed factor, use actual cost time
|
||||
const remainDeltaTime = deltaTime - playCostTime / playSpeed;
|
||||
// Remove speed factor, use actual cost time. Per-instance speed=0 means the source
|
||||
// state is paused, so it consumes no time — pass deltaTime through to the destination.
|
||||
const remainDeltaTime = playSpeed === 0 ? deltaTime : deltaTime - playCostTime / playSpeed;
|
||||
remainDeltaTime > 0 && this._updateState(layerData, remainDeltaTime, aniUpdate);
|
||||
}
|
||||
}
|
||||
@@ -711,7 +730,7 @@ export class Animator extends Component {
|
||||
additive: boolean,
|
||||
aniUpdate: boolean
|
||||
): void {
|
||||
const curveBindings = playData.state.clip._curveBindings;
|
||||
const curveBindings = playData.instance.clip._curveBindings;
|
||||
const finished = playData.playState === AnimatorStatePlayState.Finished;
|
||||
|
||||
if (aniUpdate || finished) {
|
||||
@@ -745,20 +764,20 @@ export class Animator extends Component {
|
||||
) {
|
||||
const { srcPlayData, destPlayData, layerIndex } = layerData;
|
||||
const { speed } = this;
|
||||
const { state: srcState } = srcPlayData;
|
||||
const { state: destState } = destPlayData;
|
||||
const srcState = srcPlayData.instance._state;
|
||||
const destState = destPlayData.instance._state;
|
||||
const transitionDuration = layerData.crossFadeTransition._getFixedDuration();
|
||||
|
||||
if (this._tryCrossFadeInterrupt(layerData, transitionDuration, destState, deltaTime, aniUpdate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const srcPlaySpeed = srcState.speed * speed;
|
||||
const dstPlaySpeed = destState.speed * speed;
|
||||
const srcPlaySpeed = srcPlayData.instance.speed * speed;
|
||||
const dstPlaySpeed = destPlayData.instance.speed * speed;
|
||||
const dstPlayDeltaTime = dstPlaySpeed * deltaTime;
|
||||
|
||||
srcPlayData && srcPlayData.updateOrientation(srcPlaySpeed * deltaTime);
|
||||
destPlayData && destPlayData.updateOrientation(dstPlayDeltaTime);
|
||||
srcPlayData.updateOrientation(srcPlaySpeed * deltaTime);
|
||||
destPlayData.updateOrientation(dstPlayDeltaTime);
|
||||
|
||||
const { clipTime: lastSrcClipTime, playState: lastSrcPlayState } = srcPlayData;
|
||||
const { clipTime: lastDestClipTime, playState: lastDstPlayState } = destPlayData;
|
||||
@@ -836,8 +855,8 @@ export class Animator extends Component {
|
||||
aniUpdate: boolean
|
||||
) {
|
||||
const { crossLayerOwnerCollection } = layerData;
|
||||
const { _curveBindings: srcCurves } = srcPlayData.state.clip;
|
||||
const { state: destState } = destPlayData;
|
||||
const { _curveBindings: srcCurves } = srcPlayData.instance.clip;
|
||||
const destState = destPlayData.instance._state;
|
||||
const { _curveBindings: destCurves } = destState.clip;
|
||||
|
||||
const finished = destPlayData.playState === AnimatorStatePlayState.Finished;
|
||||
@@ -876,14 +895,14 @@ export class Animator extends Component {
|
||||
aniUpdate: boolean
|
||||
) {
|
||||
const { destPlayData } = layerData;
|
||||
const { state } = destPlayData;
|
||||
const state = destPlayData.instance._state;
|
||||
const transitionDuration = layerData.crossFadeTransition._getFixedDuration();
|
||||
|
||||
if (this._tryCrossFadeInterrupt(layerData, transitionDuration, state, deltaTime, aniUpdate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playSpeed = state.speed * this.speed;
|
||||
const playSpeed = destPlayData.instance.speed * this.speed;
|
||||
const playDeltaTime = playSpeed * deltaTime;
|
||||
|
||||
destPlayData.updateOrientation(playDeltaTime);
|
||||
@@ -950,7 +969,7 @@ export class Animator extends Component {
|
||||
aniUpdate: boolean
|
||||
) {
|
||||
const { crossLayerOwnerCollection } = layerData;
|
||||
const { state } = destPlayData;
|
||||
const state = destPlayData.instance._state;
|
||||
const { _curveBindings: curveBindings } = state.clip;
|
||||
|
||||
const { clipTime: destClipTime, playState } = destPlayData;
|
||||
@@ -988,8 +1007,8 @@ export class Animator extends Component {
|
||||
aniUpdate: boolean
|
||||
): void {
|
||||
const playData = layerData.srcPlayData;
|
||||
const { state } = playData;
|
||||
const actualSpeed = state.speed * this.speed;
|
||||
const state = playData.instance._state;
|
||||
const actualSpeed = playData.instance.speed * this.speed;
|
||||
const actualDeltaTime = actualSpeed * deltaTime;
|
||||
|
||||
playData.updateOrientation(actualDeltaTime);
|
||||
@@ -1030,7 +1049,7 @@ export class Animator extends Component {
|
||||
}
|
||||
|
||||
const { curveLayerOwner } = playData.stateData;
|
||||
const { _curveBindings: curveBindings } = playData.state.clip;
|
||||
const { _curveBindings: curveBindings } = playData.instance.clip;
|
||||
|
||||
for (let i = curveBindings.length - 1; i >= 0; i--) {
|
||||
const layerOwner = curveLayerOwner[i];
|
||||
@@ -1051,14 +1070,13 @@ export class Animator extends Component {
|
||||
} else {
|
||||
layerData.layerState = LayerState.Playing;
|
||||
}
|
||||
layerData.switchPlayData();
|
||||
layerData.crossFadeTransition = null;
|
||||
layerData.completeCrossFade();
|
||||
}
|
||||
|
||||
private _preparePlayOwner(layerData: AnimatorLayerData, playState: AnimatorState): void {
|
||||
if (layerData.layerState === LayerState.Playing) {
|
||||
const srcPlayData = layerData.srcPlayData;
|
||||
if (srcPlayData.state !== playState) {
|
||||
if (srcPlayData.instance._state !== playState) {
|
||||
const { curveLayerOwner } = srcPlayData.stateData;
|
||||
for (let i = curveLayerOwner.length - 1; i >= 0; i--) {
|
||||
curveLayerOwner[i]?.curveOwner.revertDefaultValue();
|
||||
@@ -1082,7 +1100,7 @@ export class Animator extends Component {
|
||||
deltaTime: number,
|
||||
aniUpdate: boolean
|
||||
): AnimatorStateTransition {
|
||||
const { state } = playData;
|
||||
const state = playData.instance._state;
|
||||
const clipDuration = state.clip.length;
|
||||
let targetTransition: AnimatorStateTransition = null;
|
||||
const startTime = state.clipStartTime * clipDuration;
|
||||
@@ -1299,19 +1317,23 @@ export class Animator extends Component {
|
||||
}
|
||||
|
||||
private _preparePlay(state: AnimatorState, layerIndex: number, normalizedTimeOffset: number = 0): boolean {
|
||||
const name = state.name;
|
||||
if (!state.clip) {
|
||||
Logger.warn(`The state named ${name} has no AnimationClip data.`);
|
||||
Logger.warn(`The state named ${state.name} has no AnimationClip data.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const animatorLayerData = this._getAnimatorLayerData(layerIndex);
|
||||
const animatorStateData = this._getAnimatorStateData(name, state, animatorLayerData, layerIndex);
|
||||
const animatorStateData = this._getAnimatorStateData(state, animatorLayerData, layerIndex);
|
||||
|
||||
this._preparePlayOwner(animatorLayerData, state);
|
||||
|
||||
animatorLayerData.layerState = LayerState.Playing;
|
||||
animatorLayerData.srcPlayData.reset(state, animatorStateData, state._getClipActualEndTime() * normalizedTimeOffset);
|
||||
const playData = animatorLayerData.getOrCreateInstance(state)._playData;
|
||||
playData.reset(animatorStateData, state._getClipActualEndTime() * normalizedTimeOffset);
|
||||
animatorLayerData.srcPlayData = playData;
|
||||
// Drop any dangling cross-fade slot from a previously-interrupted crossFade
|
||||
// so a later crossFade(B) isn't wrongly no-op'd by the active-dest guard.
|
||||
animatorLayerData.clearCrossFadeSlot();
|
||||
animatorLayerData.resetCurrentCheckIndex();
|
||||
|
||||
return true;
|
||||
@@ -1411,13 +1433,21 @@ export class Animator extends Component {
|
||||
}
|
||||
|
||||
const animatorLayerData = this._getAnimatorLayerData(layerIndex);
|
||||
const animatorStateData = this._getAnimatorStateData(crossState.name, crossState, animatorLayerData, layerIndex);
|
||||
|
||||
animatorLayerData.destPlayData.reset(
|
||||
crossState,
|
||||
animatorStateData,
|
||||
transition.offset * crossState._getClipActualEndTime()
|
||||
);
|
||||
// Self/active-dest cross-fade is a no-op: each state has one persistent
|
||||
// instance per layer, so a second concurrent fade has nowhere to live.
|
||||
if (
|
||||
animatorLayerData.srcPlayData?.instance._state === crossState ||
|
||||
animatorLayerData.destPlayData?.instance._state === crossState
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const animatorStateData = this._getAnimatorStateData(crossState, animatorLayerData, layerIndex);
|
||||
|
||||
const destPlayData = animatorLayerData.getOrCreateInstance(crossState)._playData;
|
||||
destPlayData.reset(animatorStateData, transition.offset * crossState._getClipActualEndTime());
|
||||
animatorLayerData.destPlayData = destPlayData;
|
||||
animatorLayerData.resetCurrentCheckIndex();
|
||||
|
||||
switch (animatorLayerData.layerState) {
|
||||
@@ -1452,7 +1482,8 @@ export class Animator extends Component {
|
||||
lastClipTime: number,
|
||||
deltaTime: number
|
||||
): void {
|
||||
const { state, isForward, clipTime } = playData;
|
||||
const { isForward, clipTime } = playData;
|
||||
const state = playData.instance._state;
|
||||
const startTime = state._getClipActualStartTime();
|
||||
const endTime = state._getClipActualEndTime();
|
||||
|
||||
@@ -1564,6 +1595,10 @@ export class Animator extends Component {
|
||||
lastPlayState: AnimatorStatePlayState,
|
||||
deltaTime: number
|
||||
) {
|
||||
// Re-check whether the clip events/scripts changed since the last build —
|
||||
// play()/crossFade() entry points already ensure on enter, but addEvent()
|
||||
// or addComponent(Script) after play() must also flow through.
|
||||
this._ensureEventHandlers(state, playData.stateData);
|
||||
const { eventHandlers } = playData.stateData;
|
||||
eventHandlers.length && this._fireAnimationEvents(playData, eventHandlers, lastClipTime, deltaTime);
|
||||
|
||||
|
||||
76
packages/core/src/animation/AnimatorStateInstance.ts
Normal file
76
packages/core/src/animation/AnimatorStateInstance.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { AnimationClip } from "./AnimationClip";
|
||||
import { AnimatorState } from "./AnimatorState";
|
||||
import { AnimatorStatePlayData } from "./internal/AnimatorStatePlayData";
|
||||
import { WrapMode } from "./enums/WrapMode";
|
||||
|
||||
/**
|
||||
* Per-Animator view of an `AnimatorState`.
|
||||
*
|
||||
* Override fields (speed, wrapMode) are scoped to this Animator; unset fields
|
||||
* fall through to the underlying state asset.
|
||||
*/
|
||||
export class AnimatorStateInstance {
|
||||
/** @internal */
|
||||
readonly _state: AnimatorState;
|
||||
/** @internal */
|
||||
readonly _playData: AnimatorStatePlayData;
|
||||
|
||||
private _speed: number | undefined;
|
||||
private _wrapMode: WrapMode | undefined;
|
||||
|
||||
/**
|
||||
* The name of the underlying state.
|
||||
*/
|
||||
get name(): string {
|
||||
return this._state.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The animation clip of the underlying state.
|
||||
*/
|
||||
get clip(): AnimationClip {
|
||||
return this._state.clip;
|
||||
}
|
||||
|
||||
/**
|
||||
* The normalized clip start time of the underlying state.
|
||||
*/
|
||||
get clipStartTime(): number {
|
||||
return this._state.clipStartTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* The normalized clip end time of the underlying state.
|
||||
*/
|
||||
get clipEndTime(): number {
|
||||
return this._state.clipEndTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playback speed for this Animator; overrides the underlying state when set.
|
||||
*/
|
||||
get speed(): number {
|
||||
return this._speed ?? this._state.speed;
|
||||
}
|
||||
|
||||
set speed(value: number) {
|
||||
this._speed = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap mode for this Animator; overrides the underlying state when set.
|
||||
*/
|
||||
get wrapMode(): WrapMode {
|
||||
return this._wrapMode ?? this._state.wrapMode;
|
||||
}
|
||||
|
||||
set wrapMode(value: WrapMode) {
|
||||
this._wrapMode = value;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
constructor(state: AnimatorState) {
|
||||
this._state = state;
|
||||
this._playData = new AnimatorStatePlayData(this);
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,9 @@ export class AnimatorStateMachine {
|
||||
|
||||
/**
|
||||
* The state will be played automatically.
|
||||
* @remarks When the Animator's AnimatorController changed or the Animator's onEnable be triggered.
|
||||
* @remarks When the Animator's AnimatorController changed or the Animator's onEnable be triggered. Cleared to `null` if the state is removed via `removeState`.
|
||||
*/
|
||||
defaultState: AnimatorState;
|
||||
defaultState: AnimatorState | null = null;
|
||||
|
||||
/** @internal */
|
||||
_entryTransitionCollection = new AnimatorStateTransitionCollection();
|
||||
@@ -68,8 +68,11 @@ export class AnimatorStateMachine {
|
||||
const index = this.states.indexOf(state);
|
||||
if (index > -1) {
|
||||
this.states.splice(index, 1);
|
||||
delete this._statesMap[name];
|
||||
if (this.defaultState === state) {
|
||||
this.defaultState = null;
|
||||
}
|
||||
}
|
||||
delete this._statesMap[name];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ export { Animator } from "./Animator";
|
||||
export { AnimatorController } from "./AnimatorController";
|
||||
export { AnimatorControllerLayer } from "./AnimatorControllerLayer";
|
||||
export { AnimatorState } from "./AnimatorState";
|
||||
export { AnimatorStateInstance } from "./AnimatorStateInstance";
|
||||
export { AnimatorStateMachine } from "./AnimatorStateMachine";
|
||||
export { AnimatorStateTransition } from "./AnimatorStateTransition";
|
||||
export { AnimatorConditionMode } from "./enums/AnimatorConditionMode";
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { IPoolElement } from "../../utils/ObjectPool";
|
||||
import { AnimationEvent } from "../AnimationEvent";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class AnimationEventHandler implements IPoolElement {
|
||||
export class AnimationEventHandler {
|
||||
event: AnimationEvent;
|
||||
handlers: Function[] = [];
|
||||
|
||||
dispose() {}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { AnimatorControllerLayer } from "../AnimatorControllerLayer";
|
||||
import { AnimatorState } from "../AnimatorState";
|
||||
import { AnimatorStateInstance } from "../AnimatorStateInstance";
|
||||
import { AnimatorStateTransition } from "../AnimatorStateTransition";
|
||||
import { LayerState } from "../enums/LayerState";
|
||||
import { AnimationCurveLayerOwner } from "./AnimationCurveLayerOwner";
|
||||
import { AnimatorStateData } from "./AnimatorStateData";
|
||||
import { AnimatorStatePlayData } from "./AnimatorStatePlayData";
|
||||
import type { AnimatorStatePlayData } from "./AnimatorStatePlayData";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -12,20 +14,35 @@ export class AnimatorLayerData {
|
||||
layerIndex: number;
|
||||
layer: AnimatorControllerLayer;
|
||||
curveOwnerPool: Record<number, Record<string, AnimationCurveLayerOwner>> = Object.create(null);
|
||||
animatorStateDataMap: Record<string, AnimatorStateData> = {};
|
||||
srcPlayData: AnimatorStatePlayData = new AnimatorStatePlayData();
|
||||
destPlayData: AnimatorStatePlayData = new AnimatorStatePlayData();
|
||||
animatorStateDataMap: WeakMap<AnimatorState, AnimatorStateData> = new WeakMap();
|
||||
instanceMap: WeakMap<AnimatorState, AnimatorStateInstance> = new WeakMap();
|
||||
srcPlayData: AnimatorStatePlayData | null = null;
|
||||
destPlayData: AnimatorStatePlayData | null = null;
|
||||
layerState: LayerState = LayerState.Standby;
|
||||
crossCurveMark: number = 0;
|
||||
manuallyTransition: AnimatorStateTransition = new AnimatorStateTransition();
|
||||
crossFadeTransition: AnimatorStateTransition;
|
||||
crossLayerOwnerCollection: AnimationCurveLayerOwner[] = [];
|
||||
|
||||
switchPlayData(): void {
|
||||
const srcPlayData = this.destPlayData;
|
||||
const switchTemp = this.srcPlayData;
|
||||
this.srcPlayData = srcPlayData;
|
||||
this.destPlayData = switchTemp;
|
||||
getOrCreateInstance(state: AnimatorState): AnimatorStateInstance {
|
||||
const map = this.instanceMap;
|
||||
let instance = map.get(state);
|
||||
if (!instance) {
|
||||
instance = new AnimatorStateInstance(state);
|
||||
map.set(state, instance);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
completeCrossFade(): void {
|
||||
this.srcPlayData = this.destPlayData;
|
||||
this.destPlayData = null;
|
||||
this.crossFadeTransition = null;
|
||||
}
|
||||
|
||||
clearCrossFadeSlot(): void {
|
||||
this.destPlayData = null;
|
||||
this.crossFadeTransition = null;
|
||||
}
|
||||
|
||||
resetCurrentCheckIndex(): void {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AnimatorState } from "../AnimatorState";
|
||||
import { AnimationCurveLayerOwner } from "./AnimationCurveLayerOwner";
|
||||
import { AnimationEventHandler } from "./AnimationEventHandler";
|
||||
|
||||
@@ -7,4 +8,8 @@ import { AnimationEventHandler } from "./AnimationEventHandler";
|
||||
export class AnimatorStateData {
|
||||
curveLayerOwner: AnimationCurveLayerOwner[] = [];
|
||||
eventHandlers: AnimationEventHandler[] = [];
|
||||
eventsBuiltVersion = -1;
|
||||
eventsBuiltScriptsVersion = -1;
|
||||
|
||||
constructor(readonly state: AnimatorState) {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AnimatorState } from "../AnimatorState";
|
||||
import { AnimatorStateInstance } from "../AnimatorStateInstance";
|
||||
import { AnimatorStatePlayState } from "../enums/AnimatorStatePlayState";
|
||||
import { WrapMode } from "../enums/WrapMode";
|
||||
import { AnimatorStateData } from "./AnimatorStateData";
|
||||
@@ -7,27 +7,30 @@ import { AnimatorStateData } from "./AnimatorStateData";
|
||||
* @internal
|
||||
*/
|
||||
export class AnimatorStatePlayData {
|
||||
state: AnimatorState;
|
||||
stateData: AnimatorStateData;
|
||||
playedTime: number;
|
||||
playState: AnimatorStatePlayState;
|
||||
clipTime: number;
|
||||
currentEventIndex: number;
|
||||
isForward = true;
|
||||
offsetFrameTime: number;
|
||||
|
||||
private _changedOrientation = false;
|
||||
playedTime: number = 0;
|
||||
playState: AnimatorStatePlayState = AnimatorStatePlayState.UnStarted;
|
||||
clipTime: number = 0;
|
||||
currentEventIndex: number = 0;
|
||||
isForward: boolean = true;
|
||||
offsetFrameTime: number = 0;
|
||||
|
||||
reset(state: AnimatorState, stateData: AnimatorStateData, offsetFrameTime: number): void {
|
||||
this.state = state;
|
||||
this.playedTime = 0;
|
||||
this.offsetFrameTime = offsetFrameTime;
|
||||
private _changedOrientation: boolean = false;
|
||||
|
||||
constructor(public readonly instance: AnimatorStateInstance) {}
|
||||
|
||||
reset(stateData: AnimatorStateData, offsetFrameTime: number): void {
|
||||
const state = this.instance._state;
|
||||
this.stateData = stateData;
|
||||
this.offsetFrameTime = offsetFrameTime;
|
||||
this.playedTime = 0;
|
||||
this.playState = AnimatorStatePlayState.UnStarted;
|
||||
this.clipTime = state.clipStartTime * state.clip.length;
|
||||
this.currentEventIndex = 0;
|
||||
this.isForward = true;
|
||||
this.state._transitionCollection.needResetCurrentCheckIndex = true;
|
||||
this._changedOrientation = false;
|
||||
state._transitionCollection.needResetCurrentCheckIndex = true;
|
||||
}
|
||||
|
||||
updateOrientation(deltaTime: number): void {
|
||||
@@ -43,11 +46,12 @@ export class AnimatorStatePlayData {
|
||||
|
||||
update(deltaTime: number): void {
|
||||
this.playedTime += deltaTime;
|
||||
const state = this.state;
|
||||
const instance = this.instance;
|
||||
const state = instance._state;
|
||||
let time = this.playedTime + this.offsetFrameTime;
|
||||
const duration = state._getDuration();
|
||||
this.playState = AnimatorStatePlayState.Playing;
|
||||
if (state.wrapMode === WrapMode.Loop) {
|
||||
if (instance.wrapMode === WrapMode.Loop) {
|
||||
time = duration ? time % duration : 0;
|
||||
} else {
|
||||
if (Math.abs(time) >= duration) {
|
||||
@@ -65,8 +69,9 @@ export class AnimatorStatePlayData {
|
||||
}
|
||||
}
|
||||
|
||||
private _correctTime() {
|
||||
const { state } = this;
|
||||
private _correctTime(): void {
|
||||
const state = this.instance._state;
|
||||
// Reverse playback at clipTime=0 would step into negatives; jump to clipEnd.
|
||||
if (this.clipTime === 0) {
|
||||
this.clipTime = state.clipEndTime * state.clip.length;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export enum AssetType {
|
||||
Font = "Font",
|
||||
/** Source Font, include ttf, otf and woff. */
|
||||
SourceFont = "SourceFont",
|
||||
/** AudioClip, include ogg, wav and mp3. */
|
||||
/** AudioClip, include ogg, wav, mp3, m4a, aac and flac. */
|
||||
Audio = "Audio",
|
||||
/** Project asset. */
|
||||
Project = "project",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-design",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-loader",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ResourceManager,
|
||||
resourceLoader
|
||||
} from "@galacean/engine-core";
|
||||
@resourceLoader(AssetType.Audio, ["mp3", "ogg", "wav"])
|
||||
@resourceLoader(AssetType.Audio, ["mp3", "ogg", "wav", "m4a", "aac", "flac"])
|
||||
class AudioLoader extends Loader<AudioClip> {
|
||||
load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<AudioClip> {
|
||||
return new AssetPromise((resolve, reject) => {
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
|
||||
@resourceLoader(AssetType.Shader, ["shader", "shaderc"])
|
||||
class ShaderLoader extends Loader<Shader> {
|
||||
private static _builtinRegex = /^\s*\/\/\s*@builtin\s+(\w+)/;
|
||||
|
||||
load(item: LoadItem, resourceManager: ResourceManager): AssetPromise<Shader> {
|
||||
const url = item.url!;
|
||||
|
||||
@@ -25,17 +23,7 @@ class ShaderLoader extends Loader<Shader> {
|
||||
|
||||
// @ts-ignore
|
||||
return resourceManager._request<string>(url, { ...item, type: "text" }).then((code: string) => {
|
||||
const builtinShader = this._getBuiltinShader(code);
|
||||
if (builtinShader) {
|
||||
return Shader.find(builtinShader);
|
||||
}
|
||||
|
||||
return Shader.create(code, undefined, url);
|
||||
});
|
||||
}
|
||||
|
||||
private _getBuiltinShader(code: string) {
|
||||
const match = code.match(ShaderLoader._builtinRegex);
|
||||
if (match && match[1]) return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,13 +116,13 @@ export class GLTFParserContext {
|
||||
|
||||
return AssetPromise.all([
|
||||
this.get<void>(GLTFParserType.Validator),
|
||||
this.get<Entity>(GLTFParserType.Scene),
|
||||
this.get<Texture2D>(GLTFParserType.Texture),
|
||||
this.get<Material>(GLTFParserType.Material),
|
||||
this.get<ModelMesh[]>(GLTFParserType.Mesh),
|
||||
this.get<Skin>(GLTFParserType.Skin),
|
||||
this.get<AnimationClip>(GLTFParserType.Animation),
|
||||
this.get<AnimatorController>(GLTFParserType.AnimatorController),
|
||||
this.get<Entity>(GLTFParserType.Scene)
|
||||
this.get<AnimatorController>(GLTFParserType.AnimatorController)
|
||||
]).then(() => {
|
||||
const glTFResource = this.glTFResource;
|
||||
const animatorController = glTFResource.animatorController;
|
||||
|
||||
@@ -34,17 +34,20 @@ export class GLTFSkinParser extends GLTFParser {
|
||||
}
|
||||
skin.bones = bones;
|
||||
|
||||
// Get skeleton
|
||||
// Get skeleton — when `skin.skeleton` is absent, resolve via joints' LCA
|
||||
// LCA falls back to the GLTF_ROOT wrapper only when joints span multiple top-level scene nodes
|
||||
if (skeleton !== undefined) {
|
||||
const rootBone = entities[skeleton];
|
||||
if (!rootBone) {
|
||||
throw `Skin skeleton index ${skeleton} is out of range.`;
|
||||
}
|
||||
skin.rootBone = rootBone;
|
||||
} else {
|
||||
const rootBone = this._findSkeletonRootBone(joints, entities);
|
||||
if (rootBone) {
|
||||
skin.rootBone = rootBone;
|
||||
} else {
|
||||
if (!rootBone) {
|
||||
throw "Failed to find skeleton root bone.";
|
||||
}
|
||||
skin.rootBone = rootBone;
|
||||
}
|
||||
|
||||
return skin;
|
||||
@@ -53,7 +56,11 @@ export class GLTFSkinParser extends GLTFParser {
|
||||
return AssetPromise.resolve(skinPromise);
|
||||
}
|
||||
|
||||
private _findSkeletonRootBone(joints: number[], entities: Entity[]): Entity {
|
||||
/**
|
||||
* Resolve the skeleton rootBone as the lowest common ancestor of the joints' parent chains.
|
||||
* Returns null when joints share no common ancestor.
|
||||
*/
|
||||
private _findSkeletonRootBone(joints: number[], entities: Entity[]): Entity | null {
|
||||
const paths = <Record<number, Entity[]>>{};
|
||||
for (const index of joints) {
|
||||
const path = new Array<Entity>();
|
||||
@@ -65,7 +72,7 @@ export class GLTFSkinParser extends GLTFParser {
|
||||
paths[index] = path;
|
||||
}
|
||||
|
||||
let rootNode = <Entity>null;
|
||||
let rootNode: Entity | null = null;
|
||||
for (let i = 0; ; i++) {
|
||||
let path = paths[joints[0]];
|
||||
if (i >= path.length) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-math",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-physics-lite",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-physics-physx",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-rhi-webgl",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"repository": {
|
||||
"url": "https://github.com/galacean/engine.git"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-shader-compiler",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -30,37 +30,50 @@ export class ParserUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the lexeme of `expr` when it is (transitively) a single bare identifier
|
||||
* wrapped in primary/postfix expression nodes, e.g. `o` → "o". Returns null for
|
||||
* compound expressions (`o.x`, `foo(..)`, `arr[0]`, …). Useful for callers that
|
||||
* want to apply a substitution rule only at the root of an expression, never on
|
||||
* swizzles or nested member access.
|
||||
* Walk single-child precedence-chain wrappers down to a `VariableIdentifier`,
|
||||
* returning the leaf node (or `undefined` for any compound expression).
|
||||
*
|
||||
* `allowParens` chooses between two callers' needs:
|
||||
* - `true` — descend through `( expression )` form, so `(v)` resolves to
|
||||
* `v`. Use when the caller substitutes at the expression root
|
||||
* (aliasing, renaming); user-written parens carry no extra meaning.
|
||||
* - `false` — any 3-child `PrimaryExpression` aborts; `(v)` stays
|
||||
* compound. Use when the caller treats the unwrapped node as a single
|
||||
* token (IO-struct arg drop, alias detection); user-written parens
|
||||
* must not collapse.
|
||||
*/
|
||||
static unwrapBareIdentifier(
|
||||
node: TreeNode,
|
||||
options: { allowParens: boolean }
|
||||
): ASTNode.VariableIdentifier | undefined {
|
||||
let cur: TreeNode = node;
|
||||
while (true) {
|
||||
if (cur instanceof ASTNode.VariableIdentifier) return cur;
|
||||
if (options.allowParens && cur instanceof ASTNode.PrimaryExpression && cur.children.length === 3) {
|
||||
const inner = cur.children[1];
|
||||
if (!(inner instanceof TreeNode)) return undefined;
|
||||
cur = inner;
|
||||
continue;
|
||||
}
|
||||
if (cur instanceof ASTNode.ExpressionAstNode && cur.children.length === 1) {
|
||||
const child = cur.children[0];
|
||||
if (!(child instanceof TreeNode)) return undefined;
|
||||
cur = child;
|
||||
continue;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lexeme variant of `unwrapBareIdentifier({ allowParens: true })` for callers
|
||||
* that already work with strings. Returns `null` for compound expressions.
|
||||
*/
|
||||
static extractDirectIdentLexeme(expr: TreeNode): string | null {
|
||||
let cur: TreeNode | Token = expr;
|
||||
while (cur) {
|
||||
if (cur instanceof ASTNode.VariableIdentifier) {
|
||||
const child = cur.children[0];
|
||||
return child instanceof Token ? child.lexeme : null;
|
||||
}
|
||||
// `( expression )` form on PrimaryExpression — descend through the
|
||||
// wrapped Expression so `(v)` resolves to `v`. All other compound
|
||||
// forms (member access, function call, comma, …) bail out.
|
||||
if (cur instanceof ASTNode.PrimaryExpression && cur.children.length === 3) {
|
||||
cur = cur.children[1];
|
||||
continue;
|
||||
}
|
||||
// Any precedence-chain wrapper with a single child forwards through.
|
||||
// Covers PostfixExpression / PrimaryExpression(len=1) / Assignment /
|
||||
// Conditional / LogicalOr / … all the way down — including the verbose
|
||||
// build's full chain and the release build's elision-flattened chain.
|
||||
if (cur instanceof ASTNode.ExpressionAstNode && cur.children.length === 1) {
|
||||
cur = cur.children[0];
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
const ident = ParserUtils.unwrapBareIdentifier(expr, { allowParens: true });
|
||||
if (!ident) return null;
|
||||
const child = ident.children[0];
|
||||
return child instanceof Token ? child.lexeme : null;
|
||||
}
|
||||
|
||||
// #if _VERBOSE
|
||||
|
||||
@@ -27,11 +27,14 @@ export class ShaderCompilerUtils {
|
||||
source: string,
|
||||
location: ShaderRange | ShaderPosition,
|
||||
file?: string
|
||||
): Error | undefined {
|
||||
): Error {
|
||||
// #if _VERBOSE
|
||||
return new GSError(errorName, message, location, source, file);
|
||||
// #else
|
||||
console.error(message);
|
||||
const err = new Error(message);
|
||||
err.name = errorName;
|
||||
return err;
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,39 +147,26 @@ export abstract class CodeGenVisitor {
|
||||
const astNodes = paramList.paramNodes;
|
||||
const context = VisitorContext.context;
|
||||
|
||||
// `MacroCallFunction` covers two call shapes sharing the same AST:
|
||||
// (a) object-like macro whose value is a function name, used as a call —
|
||||
// `#define FN foo` + `FN(varyings, …)`. The driver expands `FN` to
|
||||
// `foo`, and `foo` is a shader function whose IO-struct params
|
||||
// have been flattened. The call site must drop IO-struct args to
|
||||
// match the flattened signature — same rule as `visitFunctionCall`.
|
||||
// (b) true function-like macro — `#define MAX3(a,b,c) …` + `MAX3(v.x, …)`.
|
||||
// The shader compiler doesn't expand the macro; the driver does, and the
|
||||
// `#define` fixes the parameter count. Args must be preserved
|
||||
// verbatim — a member-access arg like `v.v_uv` unwraps to a root
|
||||
// identifier whose type is an IO struct, but dropping the arg
|
||||
// would change the macro's arity.
|
||||
//
|
||||
// `isFunctionLikeMacro` is set by `MacroCallSymbol.semanticAnalyze` from
|
||||
// `macroDefineList[name][*].isFunction` and carries the definition shape.
|
||||
const params = node.isFunctionLikeMacro
|
||||
? astNodes
|
||||
: astNodes.filter((arg) => {
|
||||
if (arg instanceof ASTNode.AssignmentExpression) {
|
||||
const variableParam = ParserUtils.unwrapNodeByType<ASTNode.VariableIdentifier>(
|
||||
arg,
|
||||
NoneTerminal.variable_identifier
|
||||
);
|
||||
if (
|
||||
variableParam &&
|
||||
typeof variableParam.typeInfo === "string" &&
|
||||
context.getStructRole(variableParam.typeInfo)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// Drop bare IO-struct args only when the macro aliases a user fn (whose
|
||||
// formal was flattened). All other shapes preserve args verbatim.
|
||||
let params: typeof astNodes;
|
||||
if (node.isFunctionLikeMacro || !node.aliasesNonBuiltinIdent) {
|
||||
params = astNodes;
|
||||
} else {
|
||||
params = astNodes.filter((arg) => {
|
||||
if (arg instanceof ASTNode.AssignmentExpression) {
|
||||
const variableParam = ParserUtils.unwrapBareIdentifier(arg, { allowParens: false });
|
||||
if (
|
||||
variableParam &&
|
||||
typeof variableParam.typeInfo === "string" &&
|
||||
context.getStructRole(variableParam.typeInfo)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
let paramsCode = "";
|
||||
for (let i = 0, length = params.length; i < length; i++) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Logger } from "@galacean/engine-core";
|
||||
import { IBaseSymbol } from "./IBaseSymbol";
|
||||
|
||||
export class SymbolTable<T extends IBaseSymbol> {
|
||||
@@ -10,7 +11,7 @@ export class SymbolTable<T extends IBaseSymbol> {
|
||||
for (let i = 0, n = entry.length; i < n; i++) {
|
||||
if (entry[i].isInMacroBranch) continue;
|
||||
if (entry[i].equal(symbol)) {
|
||||
console.warn("Replace symbol:", symbol.ident);
|
||||
Logger.warn("Replace symbol:", symbol.ident);
|
||||
entry[i] = symbol;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Logger } from "@galacean/engine-core";
|
||||
import { ETokenType } from "../common";
|
||||
import { Keyword } from "../common/enums/Keyword";
|
||||
import { Grammar } from "../parser/Grammar";
|
||||
import { GrammarSymbol, NoneTerminal, Terminal } from "../parser/GrammarSymbol";
|
||||
import Production from "./Production";
|
||||
import State from "./State";
|
||||
import StateItem from "./StateItem";
|
||||
import { default as GrammarUtils, default as Utils } from "./Utils";
|
||||
@@ -157,12 +159,17 @@ export class LALR1 {
|
||||
private _addAction(table: ActionTable, terminal: Terminal, action: ActionInfo) {
|
||||
const exist = table.get(terminal);
|
||||
if (exist && !Utils.isActionEqual(exist, action)) {
|
||||
// Resolve dangling else ambiguity
|
||||
if (terminal === Keyword.ELSE && exist.action === EAction.Shift && action.action === EAction.Reduce) {
|
||||
return;
|
||||
// Known shift-preferred conflicts (see TargetParser.y `%expect 2`).
|
||||
// Enforce shift regardless of the order `_inferNextState` registers
|
||||
// actions: when `exist` is already Shift and `action` is Reduce, keep
|
||||
// `exist` (early-return); the reverse order (`exist` Reduce, `action`
|
||||
// Shift) falls through to `table.set` below and Shift overwrites
|
||||
// Reduce. Order-independent by construction.
|
||||
if (LALR1._isKnownShiftPreferred(terminal, exist, action)) {
|
||||
if (exist.action === EAction.Shift && action.action === EAction.Reduce) return;
|
||||
} else {
|
||||
// #if _VERBOSE
|
||||
console.warn(
|
||||
Logger.warn(
|
||||
`conflict detect: <Terminal ${GrammarUtils.toString(terminal)}> \n`,
|
||||
Utils.printAction(exist),
|
||||
"\n",
|
||||
@@ -174,6 +181,28 @@ export class LALR1 {
|
||||
table.set(terminal, action);
|
||||
}
|
||||
|
||||
// Catalog of expected shift/reduce conflicts. Each entry must correspond to
|
||||
// one of TargetParser.y's `%expect`-ed conflicts; any new conflict not in
|
||||
// this list falls through to the verbose `conflict detect` warning so the
|
||||
// grammar/runtime drift is loud rather than silent.
|
||||
// - ELSE: dangling-else, bind to nearest `if`
|
||||
// - '(' + `type_specifier_nonarray → macro_call_symbol`: macro-as-type-alias
|
||||
// (#2974), prefer the expression-position `macro_call_function` over the
|
||||
// type-position reduce
|
||||
private static _isKnownShiftPreferred(terminal: Terminal, exist: ActionInfo, action: ActionInfo): boolean {
|
||||
if (terminal === Keyword.ELSE) return true;
|
||||
if (terminal !== ETokenType.LEFT_PAREN) return false;
|
||||
const reduce = exist.action === EAction.Reduce ? exist : action.action === EAction.Reduce ? action : null;
|
||||
if (!reduce) return false;
|
||||
const prod = Production.pool.get(reduce.target!);
|
||||
return (
|
||||
!!prod &&
|
||||
prod.goal === NoneTerminal.type_specifier_nonarray &&
|
||||
prod.derivation.length === 1 &&
|
||||
prod.derivation[0] === NoneTerminal.macro_call_symbol
|
||||
);
|
||||
}
|
||||
|
||||
// https://people.cs.pitt.edu/~jmisurda/teaching/cs1622/handouts/cs1622-first_and_follow.pdf
|
||||
private computeFirstSet() {
|
||||
for (const production of this.grammar.productions.slice(1)) {
|
||||
|
||||
@@ -1392,36 +1392,14 @@ export namespace ASTNode {
|
||||
const child = this.children[0] as BaseToken | MacroCallSymbol | MacroCallFunction;
|
||||
const referenceGlobalSymbolNames = this.referenceGlobalSymbolNames;
|
||||
const symbols = this._symbols;
|
||||
const lookupSymbol = SemanticAnalyzer._lookupSymbol;
|
||||
let needFindNames: string[];
|
||||
|
||||
// FXAA-style cross-arm shadowing: same name is a macro in one
|
||||
// preprocessor arm and a variable in the mutually-exclusive arm.
|
||||
// At a MACRO_CALL use site, also probe the macro name itself so
|
||||
// the sibling-arm declaration is marked as referenced and codegen
|
||||
// keeps it. Grammar half of the fix is in 87cb2b5f0.
|
||||
let macroNameAsVarLookup: string | null = null;
|
||||
|
||||
if (child instanceof BaseToken) {
|
||||
needFindNames = [child.lexeme];
|
||||
} else {
|
||||
const callSymbol = child as MacroCallSymbol | MacroCallFunction;
|
||||
needFindNames = callSymbol.referenceSymbolNames;
|
||||
const macroName = callSymbol.macroName;
|
||||
if (macroName && needFindNames.indexOf(macroName) === -1) {
|
||||
needFindNames = needFindNames.concat(macroName);
|
||||
macroNameAsVarLookup = macroName;
|
||||
}
|
||||
}
|
||||
// Real references — every name must resolve; miss is an authoring error.
|
||||
const needFindNames = child instanceof BaseToken ? [child.lexeme] : child.referenceSymbolNames;
|
||||
|
||||
for (let i = 0; i < needFindNames.length; i++) {
|
||||
const name = needFindNames[i];
|
||||
|
||||
// `macroDefineList` short-circuit; bypass for the macro name itself
|
||||
// so cross-arm shadowing can resolve the sibling-arm declaration.
|
||||
if (sa.macroDefineList[name] && name !== macroNameAsVarLookup) {
|
||||
continue;
|
||||
}
|
||||
if (sa.macroDefineList[name]) continue;
|
||||
|
||||
// only `macro_call` CFG can reference fnSymbols, others fnSymbols are referenced in `function_call_generic` CFG
|
||||
if (!(child instanceof BaseToken) && BuiltinFunction.isExist(name)) {
|
||||
@@ -1434,37 +1412,86 @@ export namespace ASTNode {
|
||||
continue;
|
||||
}
|
||||
|
||||
lookupSymbol.set(name, ESymbolType.Any);
|
||||
sa.symbolTableStack.lookupAll(lookupSymbol, true, symbols);
|
||||
|
||||
if (!symbols.length) {
|
||||
// #if _VERBOSE
|
||||
sa.reportWarning(this.location, `Please sure the identifier "${name}" will be declared before used.`);
|
||||
// #endif
|
||||
} else {
|
||||
// Expression-style macros have their own value AST; its real type isn't
|
||||
// the type of any single `referenceSymbolNames` entry (`v` in `v.v_uv`
|
||||
// is a `Varyings` struct but the macro call site's type should be the
|
||||
// member type). Skip type inference for those and keep TypeAny.
|
||||
if (child instanceof BaseToken || !child.hasAstValue) {
|
||||
this.typeInfo = symbols[0].dataType?.type;
|
||||
}
|
||||
const currentScopeSymbol = <VarSymbol | FnSymbol>sa.symbolTableStack.scope.getSymbol(lookupSymbol, true);
|
||||
if (currentScopeSymbol) {
|
||||
if (
|
||||
(currentScopeSymbol instanceof FnSymbol || currentScopeSymbol.isGlobalVariable) &&
|
||||
referenceGlobalSymbolNames.indexOf(name) === -1
|
||||
) {
|
||||
referenceGlobalSymbolNames.push(name);
|
||||
}
|
||||
} else if (
|
||||
symbols.some((s) => s instanceof FnSymbol || s.isGlobalVariable) &&
|
||||
referenceGlobalSymbolNames.indexOf(name) === -1
|
||||
) {
|
||||
referenceGlobalSymbolNames.push(name);
|
||||
}
|
||||
const hit = VariableIdentifier._lookupAndMarkGlobalReference(
|
||||
sa,
|
||||
name,
|
||||
symbols,
|
||||
referenceGlobalSymbolNames,
|
||||
this.location
|
||||
);
|
||||
// Expression-style macros have their own value AST; its real type isn't
|
||||
// the type of any single `referenceSymbolNames` entry (`v` in `v.v_uv`
|
||||
// is a `Varyings` struct but the macro call site's type should be the
|
||||
// member type). Skip type inference for those and keep TypeAny.
|
||||
if (hit && (child instanceof BaseToken || !child.hasAstValue)) {
|
||||
this.typeInfo = symbols[0].dataType?.type;
|
||||
}
|
||||
}
|
||||
|
||||
// FXAA-style cross-arm shadowing: at a MACRO_CALL use site, silently
|
||||
// probe the macro name itself so any sibling-arm `var` declaration is
|
||||
// marked as referenced and codegen keeps it. Miss is the common
|
||||
// single-arm case — no warning, no type inference. Grammar half of the
|
||||
// cross-arm fix is in 87cb2b5f0.
|
||||
if (!(child instanceof BaseToken)) {
|
||||
VariableIdentifier._probeCrossArmShadowing(sa, child, needFindNames, symbols, referenceGlobalSymbolNames);
|
||||
}
|
||||
}
|
||||
|
||||
/** Run the cross-arm shadowing probe for a MACRO_CALL site. No-op when the
|
||||
* probe isn't meaningful: no macro name, name already resolved as a real
|
||||
* reference, or name is a builtin (builtins can't be shadowed by a
|
||||
* sibling-arm decl). */
|
||||
private static _probeCrossArmShadowing(
|
||||
sa: SemanticAnalyzer,
|
||||
child: MacroCallSymbol | MacroCallFunction,
|
||||
needFindNames: string[],
|
||||
symbols: (VarSymbol | FnSymbol)[],
|
||||
referenceGlobalSymbolNames: string[]
|
||||
): void {
|
||||
const macroName = child.macroName;
|
||||
if (!macroName) return;
|
||||
if (needFindNames.indexOf(macroName) !== -1) return; // already looked up as a real reference
|
||||
if (BuiltinFunction.isExist(macroName) || BuiltinVariable.getVar(macroName)) return; // builtins can't be shadowed
|
||||
VariableIdentifier._lookupAndMarkGlobalReference(sa, macroName, symbols, referenceGlobalSymbolNames, null);
|
||||
}
|
||||
|
||||
/** Look up `name` in the symbol stack and, if a global var/fn declaration
|
||||
* exists, push it into `referenceGlobalSymbolNames`. Returns `true` iff
|
||||
* the lookup hit (caller can then derive type info). When `missWarnLoc`
|
||||
* is non-null, a miss reports a "declared before used" warning; pass
|
||||
* `null` for silent probes (e.g. FXAA-style cross-arm shadowing).
|
||||
*
|
||||
* Mutation contract: `symbols` is used as scratch storage — `lookupAll`
|
||||
* clears and refills it. On hit, the caller may read `symbols[0]` for
|
||||
* type info before the next call overwrites the contents. */
|
||||
private static _lookupAndMarkGlobalReference(
|
||||
sa: SemanticAnalyzer,
|
||||
name: string,
|
||||
symbols: (VarSymbol | FnSymbol)[],
|
||||
referenceGlobalSymbolNames: string[],
|
||||
missWarnLoc: ShaderRange | null
|
||||
): boolean {
|
||||
const lookupSymbol = SemanticAnalyzer._lookupSymbol;
|
||||
lookupSymbol.set(name, ESymbolType.Any);
|
||||
sa.symbolTableStack.lookupAll(lookupSymbol, true, symbols);
|
||||
|
||||
if (!symbols.length) {
|
||||
// #if _VERBOSE
|
||||
if (missWarnLoc) {
|
||||
sa.reportWarning(missWarnLoc, `Please sure the identifier "${name}" will be declared before used.`);
|
||||
}
|
||||
// #endif
|
||||
return false;
|
||||
}
|
||||
const currentScopeSymbol = <VarSymbol | FnSymbol>sa.symbolTableStack.scope.getSymbol(lookupSymbol, true);
|
||||
const isGlobal = currentScopeSymbol
|
||||
? currentScopeSymbol instanceof FnSymbol || currentScopeSymbol.isGlobalVariable
|
||||
: symbols.some((s) => s instanceof FnSymbol || s.isGlobalVariable);
|
||||
if (isGlobal && referenceGlobalSymbolNames.indexOf(name) === -1) {
|
||||
referenceGlobalSymbolNames.push(name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
override codeGen(visitor: CodeGenVisitor): string {
|
||||
@@ -1661,15 +1688,16 @@ export namespace ASTNode {
|
||||
* a root of `referenceSymbolNames`. Mixed forms across branches → false,
|
||||
* fall back to legacy inference. */
|
||||
hasAstValue: boolean = false;
|
||||
/** True when the macro is defined as function-like (`#define NAME(params) …`).
|
||||
* Used by `MacroCallFunction` codegen to pick between the two call shapes
|
||||
* — object-like-macro-as-function-name vs true function-like macro. */
|
||||
/** `#define NAME(params) …` form — drives function-like vs object-like codegen. */
|
||||
isFunctionLikeMacro: boolean = false;
|
||||
/** Every visible replacement is a non-builtin identifier — assume user fn alias. */
|
||||
aliasesNonBuiltinIdent: boolean = false;
|
||||
|
||||
override init(): void {
|
||||
this.referenceSymbolNames.length = 0;
|
||||
this.hasAstValue = false;
|
||||
this.isFunctionLikeMacro = false;
|
||||
this.aliasesNonBuiltinIdent = false;
|
||||
}
|
||||
|
||||
override semanticAnalyze(sa: SemanticAnalyzer): void {
|
||||
@@ -1689,6 +1717,7 @@ export namespace ASTNode {
|
||||
let visibleCount = 0;
|
||||
let allAst = true;
|
||||
let isFn = false;
|
||||
let allAliasNonBuiltinIdent = true;
|
||||
if (defList) {
|
||||
for (let i = 0, n = defList.length; i < n; i++) {
|
||||
const info = defList[i];
|
||||
@@ -1702,6 +1731,22 @@ export namespace ASTNode {
|
||||
if (info.valueAst) {
|
||||
MacroCallSymbol._collectIdentifierRefs(info.valueAst, info.params, refs);
|
||||
}
|
||||
// aliasesNonBuiltinIdent: macro replacement is a single non-builtin
|
||||
// identifier — best-effort proxy for "this macro call site aliases a
|
||||
// user fn", since uniform/const aliases would surface as a GLSL
|
||||
// compile error later anyway. Keyword replacements (`vec3`, `mat4`)
|
||||
// reach here with `valueAst === undefined` (opaque path), so they
|
||||
// automatically fail without an explicit keyword guard.
|
||||
if (info.isFunction || !info.valueAst) {
|
||||
allAliasNonBuiltinIdent = false;
|
||||
} else {
|
||||
const leadingIdent = ParserUtils.unwrapBareIdentifier(info.valueAst, { allowParens: false });
|
||||
const leadingChild = leadingIdent?.children[0];
|
||||
const leadingId = leadingChild instanceof BaseToken ? leadingChild.lexeme : undefined;
|
||||
if (!leadingId || BuiltinFunction.isExist(leadingId)) {
|
||||
allAliasNonBuiltinIdent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Require *every* visible entry to be AST-form before taking the AST
|
||||
@@ -1710,6 +1755,7 @@ export namespace ASTNode {
|
||||
// instead of polluting the call site with TypeAny.
|
||||
this.hasAstValue = visibleCount > 0 && allAst;
|
||||
this.isFunctionLikeMacro = isFn;
|
||||
this.aliasesNonBuiltinIdent = visibleCount > 0 && allAliasNonBuiltinIdent;
|
||||
}
|
||||
|
||||
/** Push every leaf `VariableIdentifier`'s lexeme into `out`, skipping
|
||||
@@ -1739,12 +1785,14 @@ export namespace ASTNode {
|
||||
macroName: string = "";
|
||||
hasAstValue: boolean = false;
|
||||
isFunctionLikeMacro: boolean = false;
|
||||
aliasesNonBuiltinIdent: boolean = false;
|
||||
|
||||
override init(): void {
|
||||
this.referenceSymbolNames = [];
|
||||
this.macroName = "";
|
||||
this.hasAstValue = false;
|
||||
this.isFunctionLikeMacro = false;
|
||||
this.aliasesNonBuiltinIdent = false;
|
||||
}
|
||||
|
||||
override semanticAnalyze(sa: SemanticAnalyzer): void {
|
||||
@@ -1754,6 +1802,7 @@ export namespace ASTNode {
|
||||
this.macroName = child.macroName;
|
||||
this.hasAstValue = child.hasAstValue;
|
||||
this.isFunctionLikeMacro = child.isFunctionLikeMacro;
|
||||
this.aliasesNonBuiltinIdent = child.aliasesNonBuiltinIdent;
|
||||
}
|
||||
|
||||
override codeGen(visitor: CodeGenVisitor) {
|
||||
@@ -1839,6 +1888,14 @@ export namespace ASTNode {
|
||||
if (entries) entries.push(info);
|
||||
else list[this.macroName] = [info];
|
||||
}
|
||||
|
||||
// Close the form-param scope pushed at `MACRO_DEFINE_PARAMS` shift. By
|
||||
// the time this reduce fires, every `VariableIdentifier` inside the
|
||||
// value expression has already resolved against the params. Object-like
|
||||
// macros never pushed a scope, so nothing to pop.
|
||||
if (this.isFunction) {
|
||||
sa.popScope();
|
||||
}
|
||||
}
|
||||
|
||||
override codeGen(visitor: CodeGenVisitor): string {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Logger } from "@galacean/engine-core";
|
||||
import { ShaderRange } from "../common";
|
||||
import { SymbolTable } from "../common/SymbolTable";
|
||||
import { SymbolTableStack } from "../common/SymbolTableStack";
|
||||
@@ -88,6 +89,6 @@ export default class SemanticAnalyzer {
|
||||
}
|
||||
|
||||
reportWarning(loc: ShaderRange, message: string): void {
|
||||
console.warn(new GSError(GSErrorName.CompilationWarn, message, loc, ShaderCompiler._processingPassText).toString());
|
||||
Logger.warn(new GSError(GSErrorName.CompilationWarn, message, loc, ShaderCompiler._processingPassText).toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ETokenType } from "../common";
|
||||
import { BaseToken } from "../common/BaseToken";
|
||||
import { Keyword } from "../common/enums/Keyword";
|
||||
import { GSError, GSErrorName } from "../GSError";
|
||||
import { LALR1 } from "../lalr";
|
||||
import { addTranslationRule, createGrammar } from "../lalr/CFG";
|
||||
@@ -12,6 +13,7 @@ import { ASTNode, TreeNode } from "./AST";
|
||||
import { Grammar } from "./Grammar";
|
||||
import { GrammarSymbol, NoneTerminal } from "./GrammarSymbol";
|
||||
import SematicAnalyzer from "./SemanticAnalyzer";
|
||||
import { ESymbolType, SymbolInfo } from "./symbolTable";
|
||||
import { TraceStackItem } from "./types";
|
||||
|
||||
/**
|
||||
@@ -74,6 +76,18 @@ export class ShaderTargetParser {
|
||||
const actionInfo = this.stateActionTable.get(token.type);
|
||||
if (actionInfo?.action === EAction.Shift) {
|
||||
traceBackStack.push(token, actionInfo.target!);
|
||||
// Function-like `#define` form params live in a scope wrapping the
|
||||
// value AST, mirroring how `function_header` opens a scope for GLSL
|
||||
// function parameters. Push on shift of `MACRO_DEFINE_PARAMS`; the
|
||||
// matching `popScope` runs when `MacroDefine.semanticAnalyze` reduces
|
||||
// the production (only the function-like alternative needs it, and
|
||||
// it knows that from its own children).
|
||||
if (token.type === Keyword.MACRO_DEFINE_PARAMS) {
|
||||
sematicAnalyzer.pushScope();
|
||||
for (const p of ParserUtils.parseMacroParamList(token.lexeme)) {
|
||||
sematicAnalyzer.symbolTableStack.insert(new SymbolInfo(p, ESymbolType.VAR));
|
||||
}
|
||||
}
|
||||
nextToken = tokens.next();
|
||||
} else if (actionInfo?.action === EAction.Accept) {
|
||||
sematicAnalyzer.acceptRule?.(sematicAnalyzer);
|
||||
|
||||
@@ -1,4 +1,28 @@
|
||||
// For cfg conflict test, used by bison
|
||||
// Bison-format mirror of the runtime grammar in `lalr/CFG.ts`.
|
||||
//
|
||||
// This file is consumed *only* by `bison -r all TargetParser.y` to produce a
|
||||
// human-readable LALR(1) state report — it does NOT generate runtime parser
|
||||
// code. The runtime grammar lives in `lalr/CFG.ts` and is the source of truth.
|
||||
//
|
||||
// Keep this file in sync with `CFG.ts` so the bison conflict report reflects
|
||||
// the actual state machine the runtime LALR1 builder produces. Naming
|
||||
// differences are tolerated (e.g. `eq` here ↔ `EQ_OP` in CFG.ts), but the
|
||||
// *set* of productions per non-terminal and the symbol *structure* of each
|
||||
// alternative must match.
|
||||
|
||||
// Two expected shift/reduce conflicts — bison errors if the count diverges,
|
||||
// catching new unintended ambiguity at grammar-change time:
|
||||
// 1. `ELSE` in `selection_statement` — classic dangling-else, shift binds
|
||||
// the `else` to the nearest `if`. Standard C/GLSL resolution.
|
||||
// 2. `(` in `type_specifier_nonarray → macro_call_symbol` (since #2974) —
|
||||
// at a `macro_call_symbol .` item with `(` lookahead, shift forms a
|
||||
// `macro_call_function` (expression-position macro call) rather than
|
||||
// reducing to `type_specifier_nonarray`. Matches the AST node identity
|
||||
// design intent of #2974.
|
||||
// Both are handled deterministically by `LALR1._isKnownShiftPreferred`
|
||||
// (lalr/LALR1.ts) — runtime resolution is order-independent, not reliant on
|
||||
// bison's implicit shift-wins default.
|
||||
%expect 2
|
||||
|
||||
%token id
|
||||
%token INT_CONSTANT
|
||||
@@ -8,11 +32,48 @@
|
||||
|
||||
%token void
|
||||
%token float
|
||||
%token bool
|
||||
%token int
|
||||
%token uint
|
||||
%token vec2
|
||||
%token vec3
|
||||
%token vec4
|
||||
%token bvec2
|
||||
%token bvec3
|
||||
%token bvec4
|
||||
%token ivec2
|
||||
%token ivec3
|
||||
%token ivec4
|
||||
%token uvec2
|
||||
%token uvec3
|
||||
%token uvec4
|
||||
%token mat2
|
||||
%token mat3
|
||||
%token mat4
|
||||
%token mat2x3
|
||||
%token mat2x4
|
||||
%token mat3x2
|
||||
%token mat3x4
|
||||
%token mat4x2
|
||||
%token mat4x3
|
||||
%token sampler2D
|
||||
%token sampler3D
|
||||
%token samplerCube
|
||||
%token sampler2DShadow
|
||||
%token samplerCubeShadow
|
||||
%token sampler2DArray
|
||||
%token sampler2DArrayShadow
|
||||
%token isampler2D
|
||||
%token isampler3D
|
||||
%token isamplerCube
|
||||
%token isampler2DArray
|
||||
%token usampler2D
|
||||
%token usampler3D
|
||||
%token usamplerCube
|
||||
%token usampler2DArray
|
||||
%token struct
|
||||
%token highp
|
||||
%token mediemp
|
||||
%token mediump
|
||||
%token lowp
|
||||
|
||||
%token const
|
||||
@@ -168,8 +229,45 @@ precision_specifier:
|
||||
ext_builtin_type_specifier_nonarray:
|
||||
void
|
||||
| float
|
||||
| int
|
||||
| bool
|
||||
| int
|
||||
| uint
|
||||
| vec2
|
||||
| vec3
|
||||
| vec4
|
||||
| bvec2
|
||||
| bvec3
|
||||
| bvec4
|
||||
| ivec2
|
||||
| ivec3
|
||||
| ivec4
|
||||
| uvec2
|
||||
| uvec3
|
||||
| uvec4
|
||||
| mat2
|
||||
| mat3
|
||||
| mat4
|
||||
| mat2x3
|
||||
| mat2x4
|
||||
| mat3x2
|
||||
| mat3x4
|
||||
| mat4x2
|
||||
| mat4x3
|
||||
| sampler2D
|
||||
| sampler3D
|
||||
| samplerCube
|
||||
| sampler2DShadow
|
||||
| samplerCubeShadow
|
||||
| sampler2DArray
|
||||
| sampler2DArrayShadow
|
||||
| isampler2D
|
||||
| isampler3D
|
||||
| isamplerCube
|
||||
| isampler2DArray
|
||||
| usampler2D
|
||||
| usampler3D
|
||||
| usamplerCube
|
||||
| usampler2DArray
|
||||
;
|
||||
|
||||
type_specifier_nonarray:
|
||||
@@ -185,8 +283,8 @@ type_specifier_nonarray:
|
||||
;
|
||||
|
||||
struct_specifier:
|
||||
struct id '{' struct_declaration_list '}' ;
|
||||
| struct '{' struct_declaration_list '}' ;
|
||||
struct id '{' struct_declaration_list '}' ';'
|
||||
| struct '{' struct_declaration_list '}' ';'
|
||||
;
|
||||
|
||||
struct_declaration_list:
|
||||
@@ -216,7 +314,7 @@ macro_struct_branch:
|
||||
|
||||
layout_qualifier:
|
||||
layout '(' location '=' INT_CONSTANT ')'
|
||||
| layout '(' location '=' id ')'
|
||||
;
|
||||
|
||||
|
||||
struct_declarator_list:
|
||||
@@ -241,7 +339,7 @@ type_specifier:
|
||||
|
||||
precision_qualifier:
|
||||
highp
|
||||
| mediemp
|
||||
| mediump
|
||||
| lowp
|
||||
;
|
||||
|
||||
@@ -421,7 +519,11 @@ function_call:
|
||||
function_call_generic:
|
||||
function_identifier '(' function_call_parameter_list ')'
|
||||
| function_identifier '(' ')'
|
||||
| function_identifier '(' void ')'
|
||||
// Mirrors CFG.ts:681 verbatim. This alt is unreachable from any legal
|
||||
// GLSL token stream (lexer never produces `f VOID )` without `(` between)
|
||||
// — present since the LALR refactor (#2113, 2024-07). Tracked as latent
|
||||
// bug; fix belongs in a separate PR with a `f(void)` regression test.
|
||||
| function_identifier void ')'
|
||||
;
|
||||
|
||||
function_call_parameter_list:
|
||||
@@ -555,9 +657,9 @@ simple_statement:
|
||||
declaration:
|
||||
function_prototype ';'
|
||||
| init_declarator_list ';'
|
||||
| PRECISION precision_qualifier ext_builtin_type_specifier_nonarray ';'
|
||||
| type_qualifier id ';'
|
||||
| type_qualifier id identifier_list ';'
|
||||
| precision_specifier
|
||||
;
|
||||
|
||||
identifier_list:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-shader",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-ui",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-xr-webxr",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-xr",
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@galacean/engine-tests",
|
||||
"private": true,
|
||||
"version": "2.0.0-alpha.32",
|
||||
"version": "2.0.0-alpha.33",
|
||||
"license": "MIT",
|
||||
"main": "dist/main.js",
|
||||
"module": "dist/module.js",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,117 @@ beforeAll(async function () {
|
||||
@registerGLTFParser(GLTFParserType.Schema)
|
||||
class GLTFCustomJSONParser extends GLTFParser {
|
||||
parse(context: GLTFParserContext) {
|
||||
if (context.glTFResource.url.endsWith("testSkinRoot.gltf")) {
|
||||
context.buffers = [new ArrayBuffer(192)];
|
||||
return Promise.resolve({
|
||||
asset: {
|
||||
version: "2.0"
|
||||
},
|
||||
scene: 0,
|
||||
scenes: [
|
||||
{
|
||||
nodes: [0, 1]
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
{
|
||||
name: "Character_Man"
|
||||
},
|
||||
{
|
||||
name: "mixamorig:Hips",
|
||||
children: [2]
|
||||
},
|
||||
{
|
||||
name: "mixamorig:Spine"
|
||||
}
|
||||
],
|
||||
skins: [
|
||||
{
|
||||
inverseBindMatrices: 0,
|
||||
// Joints span both top-level scene roots: Character_Man (0) and Hips (1)/Spine (2).
|
||||
joints: [0, 1, 2]
|
||||
}
|
||||
],
|
||||
accessors: [
|
||||
{
|
||||
bufferView: 0,
|
||||
byteOffset: 0,
|
||||
componentType: 5126,
|
||||
count: 3,
|
||||
type: "MAT4"
|
||||
}
|
||||
],
|
||||
bufferViews: [
|
||||
{
|
||||
buffer: 0,
|
||||
byteOffset: 0,
|
||||
byteLength: 192
|
||||
}
|
||||
],
|
||||
buffers: [
|
||||
{
|
||||
byteLength: 192
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (context.glTFResource.url.endsWith("testSingleSkeleton.gltf")) {
|
||||
context.buffers = [new ArrayBuffer(128)];
|
||||
return Promise.resolve({
|
||||
asset: {
|
||||
version: "2.0"
|
||||
},
|
||||
scene: 0,
|
||||
scenes: [
|
||||
{
|
||||
// Two top-level roots: a character skeleton and an unrelated sibling (e.g., a light).
|
||||
nodes: [0, 2]
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
{
|
||||
name: "Character_Root",
|
||||
children: [1]
|
||||
},
|
||||
{
|
||||
name: "mixamorig:Hips"
|
||||
},
|
||||
{
|
||||
name: "Light"
|
||||
}
|
||||
],
|
||||
skins: [
|
||||
{
|
||||
inverseBindMatrices: 0,
|
||||
// All joints converge to a single top-level root (Character_Root).
|
||||
joints: [0, 1]
|
||||
}
|
||||
],
|
||||
accessors: [
|
||||
{
|
||||
bufferView: 0,
|
||||
byteOffset: 0,
|
||||
componentType: 5126,
|
||||
count: 2,
|
||||
type: "MAT4"
|
||||
}
|
||||
],
|
||||
bufferViews: [
|
||||
{
|
||||
buffer: 0,
|
||||
byteOffset: 0,
|
||||
byteLength: 128
|
||||
}
|
||||
],
|
||||
buffers: [
|
||||
{
|
||||
byteLength: 128
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const glTF = <any>{
|
||||
buffers: [
|
||||
{
|
||||
@@ -481,6 +592,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 () {
|
||||
@@ -530,6 +652,33 @@ describe("glTF scene root structure", function () {
|
||||
expect(defaultSceneRoot.children.length).to.equal(1);
|
||||
expect(defaultSceneRoot.children[0].name).to.equal("entity1");
|
||||
});
|
||||
|
||||
it("Multi-root skins without skeleton should use the scene wrapper as rootBone", async () => {
|
||||
const glTFResource: GLTFResource = await engine.resourceManager.load({
|
||||
type: AssetType.GLTF,
|
||||
url: "mock/path/testSkinRoot.gltf"
|
||||
});
|
||||
const { defaultSceneRoot, skins } = glTFResource;
|
||||
|
||||
expect(defaultSceneRoot.name).to.equal("GLTF_ROOT");
|
||||
expect(defaultSceneRoot.children.length).to.equal(2);
|
||||
expect(skins[0].rootBone).to.equal(defaultSceneRoot);
|
||||
});
|
||||
|
||||
it("Multi-root scenes whose joints converge to a single top-level root should not use the scene wrapper", async () => {
|
||||
const glTFResource: GLTFResource = await engine.resourceManager.load({
|
||||
type: AssetType.GLTF,
|
||||
url: "mock/path/testSingleSkeleton.gltf"
|
||||
});
|
||||
const { defaultSceneRoot, skins } = glTFResource;
|
||||
|
||||
expect(defaultSceneRoot.name).to.equal("GLTF_ROOT");
|
||||
// Scene has two top-level roots, but all joints converge to "Character_Root".
|
||||
expect(defaultSceneRoot.children.length).to.equal(2);
|
||||
expect(skins[0].rootBone).to.not.equal(defaultSceneRoot);
|
||||
// rootBone should be inside the Character_Root subtree (LCA = Character_Root).
|
||||
expect(skins[0].rootBone.name).to.equal("Character_Root");
|
||||
});
|
||||
});
|
||||
|
||||
describe("glTF instance test", function () {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ShaderMacroProcessor } from "@galacean/engine-core/src/shader/ShaderMac
|
||||
|
||||
import { Logger, WebGLEngine } from "@galacean/engine";
|
||||
import { server } from "@vitest/browser/context";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
const { readFile } = server.commands;
|
||||
Logger.enable();
|
||||
@@ -1185,6 +1185,51 @@ describe("ShaderCompiler Precompile", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
// After LALR enforces shift on `macro_call_symbol . '('`, the visitor at
|
||||
// `K(args)` must distinguish three callee kinds derived from the macro's
|
||||
// replacement and apply IO-struct formal flattening only when the callee is
|
||||
// a user-declared function. Earlier the filter ran for every object-like
|
||||
// macro and silently dropped member-access / literal args (e.g. emitting
|
||||
// `vec3()` from `MyVec3(attr.POSITION.x, attr.POSITION.y, attr.POSITION.z)`).
|
||||
describe("MacroCallFunction calleeKind-aware arg filtering", () => {
|
||||
let evaluated: string;
|
||||
beforeAll(async () => {
|
||||
const source = await readFile("./shaders/macro-as-type-args.shader");
|
||||
const precompiled = shaderCompiler._precompile(source, ShaderLanguage.GLSLES100);
|
||||
const vi = precompiled.subShaders[0].passes[0].vertexShaderInstructions ?? [];
|
||||
evaluated = ShaderMacroProcessor.evaluate(vi as any, new Map());
|
||||
});
|
||||
|
||||
// (1) builtin type alias: constructor call, no formal flattening
|
||||
it("builtin type alias keeps member-access args", () => {
|
||||
// After flatten, `attr.POSITION.x/y/z` → `POSITION.x/y/z`.
|
||||
expect(evaluated).toMatch(/vec3\s*\(\s*POSITION\.x\s*,\s*POSITION\.y\s*,\s*POSITION\.z\s*\)/);
|
||||
});
|
||||
it("builtin type alias keeps literal args", () => {
|
||||
expect(evaluated).toMatch(/vec3\s*\(\s*0\.1\s*,\s*0\.2\s*,\s*0\.3\s*\)/);
|
||||
});
|
||||
it("builtin type alias never emits an empty constructor call", () => {
|
||||
// Defends against the original bug where every arg was filtered.
|
||||
expect(evaluated).not.toMatch(/vec3\s*\(\s*\)/);
|
||||
});
|
||||
|
||||
// (2) builtin function alias: args are values, no flattening
|
||||
it("builtin function alias keeps all args verbatim", () => {
|
||||
expect(evaluated).toMatch(/mix\s*\(\s*v_member\s*,\s*v_literal\s*,\s*0\.5\s*\)/);
|
||||
});
|
||||
|
||||
// (3) user-fn alias: formal flattening DOES apply — bare IO-struct arg
|
||||
// must be dropped so the call site matches the flattened signature.
|
||||
it("user-fn alias drops bare IO-struct arg paired with flattened formal", () => {
|
||||
// `computeColor(Attributes a)` flattens to `computeColor()` and call
|
||||
// becomes `computeColor()` (the `attr` arg drops).
|
||||
expect(evaluated).toMatch(/computeColor\s*\(\s*\)/);
|
||||
// The macro name `MyHelper` should still appear in the macro define
|
||||
// table (the directive itself), but never as `MyHelper(attr)` text.
|
||||
expect(evaluated).not.toMatch(/MyHelper\s*\(\s*attr\s*\)/);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// 6. Shader._createFromPrecompiled()
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -248,6 +248,22 @@ describe("ShaderCompiler", async () => {
|
||||
glslValidate(engine, shaderSource, shaderCompilerRelease);
|
||||
});
|
||||
|
||||
// Regression: function-like macro form params and macro-name-as-var cross-arm
|
||||
// probes used to emit spurious "declared before used" warnings.
|
||||
it("function-like macro form params and macro names don't warn as undeclared", async () => {
|
||||
const src = await readFile("./shaders/macro-form-params-no-warn.shader");
|
||||
const warns: string[] = [];
|
||||
const origWarn = console.warn;
|
||||
console.warn = (...args: any[]) => warns.push(args.join(" "));
|
||||
try {
|
||||
glslValidate(engine, src, shaderCompilerVerbose);
|
||||
} finally {
|
||||
console.warn = origWarn;
|
||||
}
|
||||
const undeclared = warns.filter((w) => /will be declared before used/.test(w));
|
||||
expect(undeclared, `unexpected undeclared warnings:\n${undeclared.join("\n")}`).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("macro-negate-number (!0, !1 in #if expressions)", async () => {
|
||||
const shaderSource = await readFile("./shaders/macro-negate-number.shader");
|
||||
glslValidate(engine, shaderSource, shaderCompilerVerbose);
|
||||
|
||||
46
tests/src/shader-compiler/shaders/macro-as-type-args.shader
Normal file
46
tests/src/shader-compiler/shaders/macro-as-type-args.shader
Normal file
@@ -0,0 +1,46 @@
|
||||
Shader "macro-callee-kinds" {
|
||||
SubShader "Default" {
|
||||
Pass "test" {
|
||||
mat4 renderer_MVPMat;
|
||||
|
||||
// Object-like macros covering the three callee kinds the visitor must
|
||||
// distinguish at a `K(args)` call site (LALR enforces shift to
|
||||
// `macro_call_function`, so this exercises the resulting visitor):
|
||||
|
||||
// (1) builtin type alias — constructor call, no formal flattening
|
||||
#define MyVec3 vec3
|
||||
// (2) builtin function alias — args are values, no flattening
|
||||
#define MyMix mix
|
||||
// (3) user-declared function alias — formals flattened, bare IO-struct
|
||||
// args must be dropped to match flattened signature
|
||||
#define MyHelper computeColor
|
||||
|
||||
struct Attributes { vec3 POSITION; };
|
||||
|
||||
// helper used via macro alias `MyHelper`; its only formal is an IO struct,
|
||||
// so it gets flattened away and its body reads `POSITION.xyz` directly.
|
||||
vec3 computeColor(Attributes a) {
|
||||
return a.POSITION.xyz;
|
||||
}
|
||||
|
||||
void vert(Attributes attr) {
|
||||
// (1) builtin type via member access — args must be preserved
|
||||
vec3 v_member = MyVec3(attr.POSITION.x, attr.POSITION.y, attr.POSITION.z);
|
||||
// (1) builtin type via literals — args must be preserved
|
||||
vec3 v_literal = MyVec3(0.1, 0.2, 0.3);
|
||||
// (2) builtin fn alias — args must be preserved verbatim
|
||||
vec3 v_mix = MyMix(v_member, v_literal, 0.5);
|
||||
// (3) user-fn alias with bare IO struct — formal-paired, MUST be
|
||||
// dropped so codegen emits `MyHelper()` matching flattened decl
|
||||
vec3 v_user = MyHelper(attr);
|
||||
|
||||
gl_Position = renderer_MVPMat * vec4(v_member + v_literal + v_mix + v_user, 1.0);
|
||||
}
|
||||
|
||||
void frag() { gl_FragColor = vec4(1.0); }
|
||||
|
||||
VertexShader = vert;
|
||||
FragmentShader = frag;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
Shader "macro-form-params-no-warn" {
|
||||
SubShader "Default" {
|
||||
Pass "test" {
|
||||
// Form params `a` / `mat` ref'd inside the value AST: must not warn.
|
||||
#define saturate( a ) clamp( a, 0.0, 1.0 )
|
||||
#define INVERSE_MAT(mat) inverseMat(mat)
|
||||
// Object-like macro used as a value: cross-arm probe must not warn.
|
||||
#define HALF_MIN 6.103515625e-5
|
||||
mat2 inverseMat(mat2 m) { return m; }
|
||||
void vert() { gl_Position = vec4(saturate(0.5)); }
|
||||
void frag() {
|
||||
float dp3 = max(float(HALF_MIN), 1.0);
|
||||
gl_FragColor = vec4(dp3);
|
||||
}
|
||||
VertexShader = vert;
|
||||
FragmentShader = frag;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user