From 11cf7503489ac7ea12ddf9badf4607e01b4ef9a4 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Wed, 30 Jun 2021 17:55:33 +0800 Subject: [PATCH 01/35] AnimationCurve --- .vscode/cSpell.json | 4 + .../skeletal-animation-blending.ts | 4 +- .../skeletal-animation-data-hub.ts | 128 +- .../skeletal-animation-state.ts | 21 +- .../skeletal-animation-utils.ts | 8 +- .../skeletal-animation/skeletal-animation.ts | 2 +- cocos/core/algorithm/binary-search.ts | 8 +- cocos/core/animation/animation-clip.ts | 2316 +++++++++++++++-- cocos/core/animation/animation-curve.ts | 76 +- cocos/core/animation/animation-state.ts | 457 +--- cocos/core/animation/animation.ts | 11 +- cocos/core/animation/compression.ts | 96 + cocos/core/animation/cubic-spline-value.ts | 4 + cocos/core/animation/index.ts | 2 +- cocos/core/animation/internal-symbols.ts | 2 + cocos/core/animation/legacy-clip-data.ts | 373 +++ cocos/core/animation/pose-output.ts | 29 + cocos/core/curves/curve-base.ts | 4 + cocos/core/curves/curve.ts | 585 +++++ cocos/core/curves/index.ts | 25 + cocos/core/curves/integer-curve.ts | 55 + cocos/core/curves/keyframe-curve.ts | 193 ++ cocos/core/curves/keys-shared-curves.ts | 285 ++ cocos/core/curves/object-curve.ts | 17 + cocos/core/curves/quat-curve.ts | 298 +++ cocos/core/curves/real-curve-param.ts | 60 + cocos/core/curves/solve-cubic.ts | 63 + cocos/core/data/serialization-symbols.ts | 5 + cocos/core/data/utils/asserts.ts | 2 +- cocos/core/geometry/curve.ts | 256 +- cocos/core/index.ts | 1 + cocos/particle/animator/curve-range.ts | 79 +- .../animaion-clip-migration-3.x.test.ts | 246 ++ tests/animation/animation-clip-3.x.test.ts | 125 + tests/animation/animation-clip.test.ts | 82 +- tests/animation/animation-curve.test.ts | 26 - tests/animation/compression.test.ts | 89 + tests/core/geometry/geometry-curve.test.ts | 196 ++ tests/curves/curve.test.ts | 275 ++ tests/curves/key-shared-curves.test.ts | 244 ++ tests/curves/quat-curve.test.ts | 67 + 41 files changed, 5822 insertions(+), 997 deletions(-) create mode 100644 cocos/core/animation/compression.ts create mode 100644 cocos/core/animation/internal-symbols.ts create mode 100644 cocos/core/animation/legacy-clip-data.ts create mode 100644 cocos/core/animation/pose-output.ts create mode 100644 cocos/core/curves/curve-base.ts create mode 100644 cocos/core/curves/curve.ts create mode 100644 cocos/core/curves/index.ts create mode 100644 cocos/core/curves/integer-curve.ts create mode 100644 cocos/core/curves/keyframe-curve.ts create mode 100644 cocos/core/curves/keys-shared-curves.ts create mode 100644 cocos/core/curves/object-curve.ts create mode 100644 cocos/core/curves/quat-curve.ts create mode 100644 cocos/core/curves/real-curve-param.ts create mode 100644 cocos/core/curves/solve-cubic.ts create mode 100644 cocos/core/data/serialization-symbols.ts create mode 100644 tests/animation/animaion-clip-migration-3.x.test.ts create mode 100644 tests/animation/animation-clip-3.x.test.ts delete mode 100644 tests/animation/animation-curve.test.ts create mode 100644 tests/animation/compression.test.ts create mode 100644 tests/core/geometry/geometry-curve.test.ts create mode 100644 tests/curves/curve.test.ts create mode 100644 tests/curves/key-shared-curves.test.ts create mode 100644 tests/curves/quat-curve.test.ts diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json index 13ac7b70dc5..9ae4dbbdbff 100644 --- a/.vscode/cSpell.json +++ b/.vscode/cSpell.json @@ -10,17 +10,21 @@ "Chukong", "clampf", "COCOSPLAY", + "coeff", "deinterleave", "deserialization", "deserialize", "deserializes", "earcut", "emscripten", + "endregion", "eventify", "eventified", + "extrap", "forin", "glsl", "grayscale", + "interp", "IGFX", "lerp", "lerpable", diff --git a/cocos/3d/skeletal-animation/skeletal-animation-blending.ts b/cocos/3d/skeletal-animation/skeletal-animation-blending.ts index 5d465d09844..99a8b1d994f 100644 --- a/cocos/3d/skeletal-animation/skeletal-animation-blending.ts +++ b/cocos/3d/skeletal-animation/skeletal-animation-blending.ts @@ -41,7 +41,7 @@ export class BlendStateBuffer { property: P, host: BlendStateWriterHost, constants: boolean, - ): Omit, 'node' | 'property'> { + ): BlendStateWriter

{ const propertyBlendState = this.ref(node, property); return new BlendStateWriterInternal

( node, @@ -124,7 +124,7 @@ class BlendStateWriterInternal

implements IBoundTarg export type BlendStateWriter

= Omit, 'node' | 'property'>; -type BlendingProperty = keyof NodeBlendState['_properties']; +export type BlendingProperty = keyof NodeBlendState['_properties']; type BlendingPropertyValue

= NonNullable['blendedValue']; diff --git a/cocos/3d/skeletal-animation/skeletal-animation-data-hub.ts b/cocos/3d/skeletal-animation/skeletal-animation-data-hub.ts index a2dd5c178a7..be34c58924c 100644 --- a/cocos/3d/skeletal-animation/skeletal-animation-data-hub.ts +++ b/cocos/3d/skeletal-animation/skeletal-animation-data-hub.ts @@ -27,41 +27,25 @@ * @packageDocumentation * @module animation */ - -import { - clamp01, Mat4, Quat, Vec3, -} from '../../core/math'; import { DataPoolManager } from './data-pool-manager'; -import { AnimationClip, IObjectCurveData } from '../../core/animation/animation-clip'; -import { HierarchyPath, isCustomPath, isPropertyPath } from '../../core/animation/target-path'; +import type { AnimationClip } from '../../core/animation/animation-clip'; import { legacyCC } from '../../core/global-exports'; +import { BAKE_SKELETON_CURVE_SYMBOL } from '../../core/animation/internal-symbols'; -type CurveData = Vec3[] | Quat[] | Mat4[]; -type ConvertedProps = Record; - -interface IPropertyCurve { - keys: number; - values: CurveData; -} -interface ISkeletalCurveInfo { - frames: number; - sample: number; -} -interface IConvertedData { - info: ISkeletalCurveInfo; - data: Record; -} +type BakeData = ReturnType; /** * 骨骼动画数据转换中心。 */ export class SkelAnimDataHub { - public static getOrExtract (clip: AnimationClip) { + public static getOrExtract (clip: AnimationClip): BakeData { let data = SkelAnimDataHub.pool.get(clip); - if (!data || data.info.sample !== clip.sample) { + if (!data || data.samples !== clip.sample) { // release outdated render data if (data) { (legacyCC.director.root.dataPoolManager as DataPoolManager).releaseAnimationClip(clip); } - data = convertToSkeletalCurves(clip); + const frames = Math.ceil(clip.sample * clip.duration) + 1; + const step = clip.sample; + data = clip[BAKE_SKELETON_CURVE_SYMBOL](0, step, frames); SkelAnimDataHub.pool.set(clip, data); } return data; @@ -71,99 +55,5 @@ export class SkelAnimDataHub { SkelAnimDataHub.pool.delete(clip); } - protected static pool = new Map(); -} - -function convertToSkeletalCurves (clip: AnimationClip): IConvertedData { - const data: Record = {}; - clip.curves.forEach((curve) => { - if (!curve.valueAdapter - && isCustomPath(curve.modifiers[0], HierarchyPath) - && isPropertyPath(curve.modifiers[1])) { - const { path } = curve.modifiers[0]; - let cs = data[path]; - if (!cs) { cs = data[path] = {}; } - const property = curve.modifiers[1] as string; - cs[property] = { values: curve.data.values, keys: curve.data.keys }; // don't use curve.data directly - } - }); - const frames = Math.ceil(clip.sample * clip.duration) + 1; - // lazy eval the conversion due to memory-heavy ops - // many animation paths may not be actually in-use - for (const path of Object.keys(data)) { - const props = data[path]; - if (!props) { continue; } - Object.defineProperty(props, 'worldMatrix', { - get: () => { - if (!props._worldMatrix) { - const { position, rotation, scale } = props; - // fixed step pre-sample - convertToUniformSample(clip, position, frames); - convertToUniformSample(clip, rotation, frames); - convertToUniformSample(clip, scale, frames); - // transform to world space - convertToWorldSpace(data, path, props); - } - return props._worldMatrix; - }, - }); - } - const info: ISkeletalCurveInfo = { - frames, - sample: clip.sample, - }; - return { info, data }; -} - -function convertToUniformSample (clip: AnimationClip, curve: IPropertyCurve, frames: number) { - const keys = clip.keys[curve.keys]; - const values: CurveData = []; - if (!keys || keys.length === 1) { - for (let i = 0; i < frames; i++) { - values[i] = curve.values[0].clone(); // never forget to clone - } - } else { - const isQuat = curve.values[0] instanceof Quat; - for (let i = 0, idx = 0; i < frames; i++) { - let time = i / clip.sample; - while (keys[idx] <= time) { idx++; } - if (idx > keys.length - 1) { idx = keys.length - 1; time = keys[idx]; } else if (idx === 0) { idx = 1; } - const from = curve.values[idx - 1].clone(); - const denom = keys[idx] - keys[idx - 1]; - const ratio = denom ? clamp01((time - keys[idx - 1]) / denom) : 1; - if (isQuat) { - (from as Quat).slerp(curve.values[idx] as Quat, ratio); - } else { - (from as Vec3).lerp(curve.values[idx] as Vec3, ratio); - } - values[i] = from; - } - } - curve.values = values; -} - -function convertToWorldSpace (convertedProps: Record, path: string, props: IObjectCurveData) { - const oPos = props.position.values; - const oRot = props.rotation.values; - const oScale = props.scale.values; - const matrix = oPos.map(() => new Mat4()); - const idx = path.lastIndexOf('/'); - let pMatrix: Mat4[] | null = null; - if (idx > 0) { - const name = path.substring(0, idx); - const data = convertedProps[name]; - if (!data) { console.warn('no data for parent bone?'); return; } - pMatrix = data.worldMatrix.values as Mat4[]; - } - // all props should have the same length now - for (let i = 0; i < oPos.length; i++) { - const oT = oPos[i]; - const oR = oRot[i]; - const oS = oScale[i]; - const m = matrix[i]; - Mat4.fromRTS(m, oR, oT, oS); - if (pMatrix) { Mat4.multiply(m, pMatrix[i], m); } - } - Object.keys(props).forEach((k) => delete props[k]); - props._worldMatrix = { keys: 0, interpolate: false, values: matrix }; + private static pool = new Map(); } diff --git a/cocos/3d/skeletal-animation/skeletal-animation-state.ts b/cocos/3d/skeletal-animation/skeletal-animation-state.ts index a1d31afcf57..32d6414fbfa 100644 --- a/cocos/3d/skeletal-animation/skeletal-animation-state.ts +++ b/cocos/3d/skeletal-animation/skeletal-animation-state.ts @@ -33,7 +33,7 @@ import { SkinnedMeshRenderer } from '../skinned-mesh-renderer'; import { Mat4, Quat, Vec3 } from '../../core/math'; import { IAnimInfo, JointAnimationInfo } from './skeletal-animation-utils'; import { Node } from '../../core/scene-graph/node'; -import { AnimationClip, IRuntimeCurve } from '../../core/animation/animation-clip'; +import { AnimationClip } from '../../core/animation/animation-clip'; import { AnimationState } from '../../core/animation/animation-state'; import { SkeletalAnimation, Socket } from './skeletal-animation'; import { SkelAnimDataHub } from './skeletal-animation-data-hub'; @@ -53,8 +53,6 @@ interface ISocketData { frames: ITransform[]; } -const noCurves: IRuntimeCurve[] = []; - export class SkeletalAnimationState extends AnimationState { protected _frames = 1; @@ -89,12 +87,13 @@ export class SkeletalAnimationState extends AnimationState { } this._parent = root.getComponent('cc.SkeletalAnimation') as SkeletalAnimation; const baked = this._parent.useBakedAnimation; - super.initialize(root, baked ? noCurves : undefined); + this._doNotCreateEval = baked; + super.initialize(root); this._curvesInited = !baked; - const { info } = SkelAnimDataHub.getOrExtract(this.clip); - this._frames = info.frames - 1; + const { frames, samples } = SkelAnimDataHub.getOrExtract(this.clip); + this._frames = frames - 1; this._animInfo = this._animInfoMgr.getData(root.uuid); - this._bakedDuration = this._frames / info.sample; // last key + this._bakedDuration = this._frames / samples; // last key } public onPlay () { @@ -128,13 +127,13 @@ export class SkeletalAnimationState extends AnimationState { if (!socket.target) { continue; } const clipData = SkelAnimDataHub.getOrExtract(this.clip); let animPath = socket.path; - let source = clipData.data[animPath]; + let source = clipData.joints[animPath]; let animNode = targetNode; let downstream: Mat4 | undefined; while (!source) { const idx = animPath.lastIndexOf('/'); animPath = animPath.substring(0, idx); - source = clipData.data[animPath]; + source = clipData.joints[animPath]; if (animNode) { if (!downstream) { downstream = Mat4.identity(m4_2); } Mat4.fromRTS(m4_1, animNode.rotation, animNode.position, animNode.scale); @@ -143,8 +142,8 @@ export class SkeletalAnimationState extends AnimationState { } if (idx < 0) { break; } } - const curveData: Mat4[] | undefined = source && source.worldMatrix.values as Mat4[]; - const { frames } = clipData.info; + const curveData: Mat4[] | undefined = source && source.transforms; + const { frames } = clipData; const transforms: ITransform[] = []; for (let f = 0; f < frames; f++) { let mat: Mat4; diff --git a/cocos/3d/skeletal-animation/skeletal-animation-utils.ts b/cocos/3d/skeletal-animation/skeletal-animation-utils.ts index 70c792f236c..7662a48de98 100644 --- a/cocos/3d/skeletal-animation/skeletal-animation-utils.ts +++ b/cocos/3d/skeletal-animation/skeletal-animation-utils.ts @@ -272,7 +272,7 @@ export class JointTexturePool { if (texture && texture.bounds.has(mesh.hash)) { texture.refCount++; return texture; } const { joints, bindposes } = skeleton; const clipData = SkelAnimDataHub.getOrExtract(clip); - const { frames } = clipData.info; + const { frames } = clipData; let textureBuffer: Float32Array = null!; let buildTexture = false; const jointCount = joints.length; if (!texture) { @@ -394,14 +394,14 @@ export class JointTexturePool { const clipData = SkelAnimDataHub.getOrExtract(clip); for (let j = 0; j < jointCount; j++) { let animPath = joints[j]; - let source = clipData.data[animPath]; + let source = clipData.joints[animPath]; let animNode = skinningRoot.getChildByPath(animPath); let downstream: Mat4 | undefined; let correctionPath: string | undefined; while (!source) { const idx = animPath.lastIndexOf('/'); animPath = animPath.substring(0, idx); - source = clipData.data[animPath]; + source = clipData.joints[animPath]; if (animNode) { if (!downstream) { downstream = new Mat4(); } Mat4.fromRTS(m4_1, animNode.rotation, animNode.position, animNode.scale); @@ -447,7 +447,7 @@ export class JointTexturePool { } } animInfos.push({ - curveData: source && source.worldMatrix.values as Mat4[], downstream, bindposeIdx, bindposeCorrection, + curveData: source && source.transforms, downstream, bindposeIdx, bindposeCorrection, }); } return animInfos; diff --git a/cocos/3d/skeletal-animation/skeletal-animation.ts b/cocos/3d/skeletal-animation/skeletal-animation.ts index 9d1d8b1fce5..938eec21879 100644 --- a/cocos/3d/skeletal-animation/skeletal-animation.ts +++ b/cocos/3d/skeletal-animation/skeletal-animation.ts @@ -174,7 +174,7 @@ export class SkeletalAnimation extends Animation { } public querySockets () { - const animPaths = (this._defaultClip && Object.keys(SkelAnimDataHub.getOrExtract(this._defaultClip).data).sort() + const animPaths = (this._defaultClip && Object.keys(SkelAnimDataHub.getOrExtract(this._defaultClip).joints).sort() .reduce((acc, cur) => (cur.startsWith(acc[acc.length - 1]) ? acc : (acc.push(cur), acc)), [] as string[])) || []; if (!animPaths.length) { return ['please specify a valid default animation clip first']; } const out: string[] = []; diff --git a/cocos/core/algorithm/binary-search.ts b/cocos/core/algorithm/binary-search.ts index db6f1fca0c9..7879cafe7ca 100644 --- a/cocos/core/algorithm/binary-search.ts +++ b/cocos/core/algorithm/binary-search.ts @@ -45,7 +45,7 @@ export function binarySearch (array: number[], value: number) { * otherwise, a negative number that is the bitwise complement of the index of the next element that is large than the searched value or, * if there is no larger element(include the case that the array is empty), the bitwise complement of array's length. */ -export function binarySearchEpsilon (array: number[], value: number, EPSILON = 1e-6) { +export function binarySearchEpsilon (array: readonly number[], value: number, EPSILON = 1e-6) { let low = 0; let high = array.length - 1; let middle = high >>> 1; @@ -71,15 +71,15 @@ export function binarySearchEpsilon (array: number[], value: number, EPSILON = 1 * otherwise, a negative number that is the bitwise complement of the index of the next element that is large than the searched value or, * if there is no larger element(include the case that the array is empty), the bitwise complement of array's length. */ -export function binarySearchBy (array: T[], value: T, lessThan: (lhs: T, rhs: T) => boolean) { +export function binarySearchBy (array: T[], value: U, lessThan: (lhs: T, rhs: U) => number) { let low = 0; let high = array.length - 1; let middle = high >>> 1; for (; low <= high; middle = (low + high) >>> 1) { const test = array[middle]; - if (lessThan(value, test)) { + if (lessThan(test, value) < 0) { high = middle - 1; - } else if (lessThan(test, value)) { + } else if (lessThan(test, value) > 0) { low = middle + 1; } else { return middle; diff --git a/cocos/core/animation/animation-clip.ts b/cocos/core/animation/animation-clip.ts index 24dce5700a6..7c6b0188f0b 100644 --- a/cocos/core/animation/animation-clip.ts +++ b/cocos/core/animation/animation-clip.ts @@ -28,88 +28,479 @@ * @module animation */ -import { EDITOR } from 'internal:constants'; import { ccclass, serializable } from 'cc.decorator'; import { Asset } from '../assets/asset'; import { SpriteFrame } from '../../2d/assets/sprite-frame'; -import { CompactValueTypeArray } from '../data/utils/compact-value-type-array'; -import { errorID } from '../platform/debug'; +import { error, errorID, warn } from '../platform/debug'; import { DataPoolManager } from '../../3d/skeletal-animation/data-pool-manager'; import { binarySearchEpsilon } from '../algorithm/binary-search'; import { murmurhash2_32_gc } from '../utils/murmurhash2_gc'; -import { AnimCurve, IPropertyCurveData, RatioSampler } from './animation-curve'; import { SkelAnimDataHub } from '../../3d/skeletal-animation/skeletal-animation-data-hub'; -import { ComponentPath, HierarchyPath, TargetPath } from './target-path'; -import { WrapMode as AnimationWrapMode } from './types'; +import { ComponentPath, HierarchyPath, TargetPath, evaluatePath, isPropertyPath } from './target-path'; +import { WrapMode as AnimationWrapMode, WrapMode, WrapModeMask } from './types'; import { IValueProxyFactory } from './value-proxy'; import { legacyCC } from '../global-exports'; +import { RealCurve, RealInterpMode } from '../curves'; +import { ObjectCurve } from '../curves/object-curve'; +import { Color, Mat4, Quat, Size, Vec2, Vec3, Vec4 } from '../math'; +import { Node } from '../scene-graph/node'; +import { IntegerCurve } from '../curves/integer-curve'; +import { QuaternionCurve, QuaternionInterpMode } from '../curves/quat-curve'; +import { KeySharedQuaternionCurves, KeySharedRealCurves } from '../curves/keys-shared-curves'; +import { assertIsTrue } from '../data/utils/asserts'; +import type { PoseOutput } from './pose-output'; +import * as legacy from './legacy-clip-data'; +import { BAKE_SKELETON_CURVE_SYMBOL } from './internal-symbols'; +import { RealKeyframeValue } from '../curves/curve'; +import { CubicSplineNumberValue, CubicSplineVec2Value, CubicSplineVec3Value, CubicSplineVec4Value } from './cubic-spline-value'; -export interface IObjectCurveData { - [propertyName: string]: IPropertyCurveData; +export declare namespace AnimationClip { + export interface IEvent { + frame: number; + func: string; + params: string[]; + } + + export type { legacy as _legacy }; } -export interface IComponentsCurveData { - [componentName: string]: IObjectCurveData; +// #region Tracks + +type TrackPath = TargetPath[]; + +interface Range { + min: number; + max: number; } -export interface INodeCurveData { - props?: IObjectCurveData; - comps?: IComponentsCurveData; +const createEvalSymbol = Symbol('CreateEval'); + +const CLASS_NAME_PREFIX_ANIM = 'cc.animation.'; + +// Export for test +export const searchForRootBonePathSymbol = Symbol('SearchForRootBonePath'); + +/** + * A track describes the path of animate a target. + * It's the basic unit of animation clip. + */ +@ccclass(`${CLASS_NAME_PREFIX_ANIM}Track`) +export class Track { + @serializable + public path: TrackPath = []; + + @serializable + public setter!: IValueProxyFactory | undefined; + + public getChannels (): Channel[] { + return []; + } + + public getRange (): Range { + const range: Range = { min: Infinity, max: -Infinity }; + for (const channel of this.getChannels()) { + range.min = Math.min(range.min, channel.curve.rangeMin); + range.max = Math.max(range.max, channel.curve.rangeMax); + } + return range; + } + + public [createEvalSymbol] (runtimeBinding: RuntimeBinding): TrackEval { + throw new Error(`No Impl`); + } } -export type IRuntimeCurve = Pick & { +interface TrackEval { /** - * 属性曲线。 + * Evaluates the track. + * @param time The time. */ - curve: AnimCurve; + evaluate(time: number, runtimeBinding: RuntimeBinding): unknown; +} - /** - * 曲线采样器。 - */ - sampler: RatioSampler | null; -}; +type Curve = RealCurve | IntegerCurve | QuaternionCurve | ObjectCurve; -export interface IAnimationEvent { - functionName: string; - parameters: string[]; -} +@ccclass(`${CLASS_NAME_PREFIX_ANIM}Channel`) +export class Channel { + constructor (curve: T) { + this._curve = curve; + } -export interface IAnimationEventGroup { - events: IAnimationEvent[]; + @serializable + public name = ''; + + get curve () { + return this._curve; + } + + @serializable + private _curve!: T; } -export declare namespace AnimationClip { - export type PropertyCurveData = IPropertyCurveData; +type RealChannel = Channel; + +type IntegerChannel = Channel; + +type QuaternionChannel = Channel; - export interface ICurve { - commonTarget?: number; - modifiers: TargetPath[]; - valueAdapter?: IValueProxyFactory; - data: PropertyCurveData; +@ccclass(`${CLASS_NAME_PREFIX_ANIM}SingleChannelTrack`) +export abstract class SingleChannelTrack extends Track { + constructor () { + super(); + this._channel = new Channel(this.createCurve()); } - export interface ICommonTarget { - modifiers: TargetPath[]; - valueAdapter?: IValueProxyFactory; + get channel () { + return this._channel; } - export interface IEvent { - frame: number; - func: string; - params: any[]; + public getChannels () { + return [this._channel]; } - export namespace _impl { - type MaybeCompactCurve = Omit & { - data: Omit & { - values: any[] | CompactValueTypeArray; - }; + protected createCurve (): TCurve { + throw new Error(`Not impl`); + } + + public [createEvalSymbol] (_runtimeBinding: RuntimeBinding): TrackEval { + const { curve } = this._channel; + return { + evaluate: (time) => curve.evaluate(time), }; + } + + @serializable + private _channel: Channel; +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}RealTrack`) +export class RealTrack extends SingleChannelTrack { + protected createCurve () { + return new RealCurve(); + } +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}IntegerTrack`) +export class IntegerTrack extends SingleChannelTrack { + protected createCurve () { + return new IntegerCurve(); + } +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}QuaternionTrack`) +export class QuaternionTrack extends SingleChannelTrack { + protected createCurve () { + return new QuaternionCurve(); + } + + public [createEvalSymbol] () { + return new QuatTrackEval(this.getChannels()[0].curve); + } +} + +class QuatTrackEval { + constructor (private _curve: QuaternionCurve) { + + } + + public evaluate (time: number) { + this._curve.evaluate(time, this._result); + return this._result; + } + + private _result: Quat = new Quat(); +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}ObjectTrack`) +export class ObjectTrack extends SingleChannelTrack> { + protected createCurve () { + return new ObjectCurve(); + } +} + +function maskIfEmpty (curve: T) { + return curve.empty ? undefined : curve; +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}VectorTrack`) +export class VectorTrack extends Track { + constructor () { + super(); + this._channels = new Array(4) as VectorTrack['_channels']; + for (let i = 0; i < this._channels.length; ++i) { + const channel = new Channel(new RealCurve()); + channel.name = 'X'; + this._channels[i] = channel; + } + } + + get componentsCount () { + return this._nComponents; + } + + set componentsCount (value) { + this._nComponents = value; + } + + public getChannels () { + return this._channels; + } + + public [createEvalSymbol] () { + switch (this._nComponents) { + default: + case 2: + return new Vec2TrackEval( + maskIfEmpty(this._channels[0].curve), + maskIfEmpty(this._channels[1].curve), + ); + case 3: + return new Vec3TrackEval( + maskIfEmpty(this._channels[0].curve), + maskIfEmpty(this._channels[1].curve), + maskIfEmpty(this._channels[2].curve), + ); + case 4: + return new Vec4TrackEval( + maskIfEmpty(this._channels[0].curve), + maskIfEmpty(this._channels[1].curve), + maskIfEmpty(this._channels[2].curve), + maskIfEmpty(this._channels[3].curve), + ); + } + } + + @serializable + private _channels: [RealChannel, RealChannel, RealChannel, RealChannel]; + + @serializable + private _nComponents: 2 | 3 | 4 = 4; +} + +class Vec2TrackEval { + constructor (private _x: RealCurve | undefined, private _y: RealCurve | undefined) { + + } + + public evaluate (time: number, runtimeBinding: RuntimeBinding) { + if ((!this._x || !this._y) && runtimeBinding.getValue) { + Vec2.copy(this._result, runtimeBinding.getValue() as Vec2); + } + + if (this._x) { + this._result.x = this._x.evaluate(time); + } + if (this._y) { + this._result.y = this._y.evaluate(time); + } + + return this._result; + } + + private _result: Vec2 = new Vec2(); +} + +class Vec3TrackEval { + constructor (private _x: RealCurve | undefined, private _y: RealCurve | undefined, private _z: RealCurve | undefined) { + + } + + public evaluate (time: number, runtimeBinding: RuntimeBinding) { + if ((!this._x || !this._y || !this._z) && runtimeBinding.getValue) { + Vec3.copy(this._result, runtimeBinding.getValue() as Vec3); + } + + if (this._x) { + this._result.x = this._x.evaluate(time); + } + if (this._y) { + this._result.y = this._y.evaluate(time); + } + if (this._z) { + this._result.z = this._z.evaluate(time); + } + + return this._result; + } + + private _result: Vec3 = new Vec3(); +} + +class Vec4TrackEval { + constructor ( + private _x: RealCurve | undefined, + private _y: RealCurve | undefined, + private _z: RealCurve | undefined, + private _w: RealCurve | undefined, + ) { + + } + + public evaluate (time: number, runtimeBinding: RuntimeBinding) { + if ((!this._x || !this._y || !this._z || !this._w) && runtimeBinding.getValue) { + Vec4.copy(this._result, runtimeBinding.getValue() as Vec4); + } + + if (this._x) { + this._result.x = this._x.evaluate(time); + } + if (this._y) { + this._result.y = this._y.evaluate(time); + } + if (this._z) { + this._result.z = this._z.evaluate(time); + } + if (this._w) { + this._result.w = this._w.evaluate(time); + } + + return this._result; + } + + private _result: Vec4 = new Vec4(); +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}ColorTrack`) +export class ColorTrack extends Track { + constructor () { + super(); + this._channels = new Array(4) as ColorTrack['_channels']; + for (let i = 0; i < this._channels.length; ++i) { + const channel = new Channel(new IntegerCurve()); + channel.name = 'R'; + this._channels[i] = channel; + } + } + + public getChannels () { + return this._channels; + } + + public [createEvalSymbol] () { + return new ColorTrackEval( + maskIfEmpty(this._channels[0].curve), + maskIfEmpty(this._channels[1].curve), + maskIfEmpty(this._channels[2].curve), + maskIfEmpty(this._channels[3].curve), + ); + } + + @serializable + private _channels: [IntegerChannel, IntegerChannel, IntegerChannel, IntegerChannel]; +} + +class ColorTrackEval { + constructor ( + private _x: TCurve | undefined, + private _y: TCurve | undefined, + private _z: TCurve | undefined, + private _w: TCurve | undefined, + ) { + + } + + public evaluate (time: number, runtimeBinding: RuntimeBinding) { + if ((!this._x || !this._y || !this._z || !this._w) && runtimeBinding.getValue) { + Color.copy(this._result, runtimeBinding.getValue() as Color); + } + + if (this._x) { + this._result.r = this._x.evaluate(time); + } + if (this._y) { + this._result.g = this._y.evaluate(time); + } + if (this._z) { + this._result.b = this._z.evaluate(time); + } + if (this._w) { + this._result.a = this._w.evaluate(time); + } + + return this._result; + } + + private _result: Color = new Color(); +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}UntypedTrackChannel`) +class UntypedTrackChannel extends Channel { + @serializable + public property!: string; + + constructor () { + super(new RealCurve()); + } +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}UntypedTrack`) +class UntypedTrack extends Track { + @serializable + private _channels: UntypedTrackChannel[] = []; + + public getChannels () { + return this._channels; + } + + public [createEvalSymbol] (runtimeBinding: RuntimeBinding) { + if (!runtimeBinding.getValue) { + throw new Error(`Can not decide type for untyped track: runtime binding does not provide a getter.`); + } + const trySearchCurve = (property: string) => this._channels.find((channel) => channel.property === property)?.curve; + const value = runtimeBinding.getValue(); + switch (true) { + case value instanceof Size: + default: + throw new Error(`Can not decide type for untyped track: got a unsupported value from runtime binding.`); + case value instanceof Vec2: + return new Vec2TrackEval( + trySearchCurve('x'), + trySearchCurve('y'), + ); + case value instanceof Vec3: + return new Vec3TrackEval( + trySearchCurve('x'), + trySearchCurve('y'), + trySearchCurve('z'), + ); + case value instanceof Vec4: + return new Vec4TrackEval( + trySearchCurve('x'), + trySearchCurve('y'), + trySearchCurve('z'), + trySearchCurve('w'), + ); + case value instanceof Color: + // TODO: what if x, y, z, w? + return new ColorTrackEval( + trySearchCurve('r'), + trySearchCurve('g'), + trySearchCurve('b'), + trySearchCurve('a'), + ); + } + } - type MaybeCompactKeys = Array; + public addChannel (property: string): UntypedTrackChannel { + const channel = new UntypedTrackChannel(); + channel.property = property; + this._channels.push(channel); + return channel; } } +// #endregion + +interface SkeletonAnimationBakeInfo { + samples: number; + + frames: number; + + joints: Record; +} + /** * @zh 动画剪辑表示一段使用动画编辑器编辑的关键帧动画或是外部美术工具生产的骨骼动画。 * 它的数据主要被分为几层:轨道、关键帧和曲线。 @@ -130,34 +521,14 @@ export class AnimationClip extends Asset { * ``` */ public static createWithSpriteFrames (spriteFrames: SpriteFrame[], sample: number) { - if (!Array.isArray(spriteFrames)) { - errorID(3905); - return null; - } - const clip = new AnimationClip(); clip.sample = sample || clip.sample; - clip.duration = spriteFrames.length / clip.sample; const step = 1 / clip.sample; - const keys = new Array(spriteFrames.length); - const values = new Array(keys.length); - for (let i = 0; i < spriteFrames.length; i++) { - keys[i] = i * step; - values[i] = spriteFrames[i]; - } - clip.keys = [keys]; - clip.curves = [{ - modifiers: [ - new ComponentPath('cc.Sprite'), - 'spriteFrame', - ], - data: { - keys: 0, - values, - }, - }]; - + const track = new ObjectTrack(); + track.path = [new ComponentPath('cc.Sprite'), 'spriteFrame']; + const curve = track.getChannels()[0].curve; + curve.assignSorted(spriteFrames.map((spriteFrame, index) => [step * index, spriteFrame])); return clip; } @@ -183,13 +554,6 @@ export class AnimationClip extends Asset { @serializable public wrapMode = AnimationWrapMode.Normal; - /** - * @zh 动画包含的事件数据。 - * @en Associated event data. - */ - @serializable - public events: AnimationClip.IEvent[] = []; - /** * Sets if node TRS curves in this animation can be blended. * Normally this flag is enabled for model animation and disabled for other case. @@ -198,34 +562,6 @@ export class AnimationClip extends Asset { @serializable public enableTrsBlending = false; - @serializable - private _duration = 0; - - @serializable - private _keys: number[][] = []; - - @serializable - private _stepness = 0; - - @serializable - private _curves: AnimationClip.ICurve[] = []; - - @serializable - private _commonTargets: AnimationClip.ICommonTarget[] = []; - - @serializable - private _hash = 0; - - private frameRate = 0; - private _ratioSamplers: RatioSampler[] = []; - private _runtimeCurves?: IRuntimeCurve[]; - private _runtimeEvents?: { - ratios: number[]; - eventGroups: IAnimationEventGroup[]; - }; - - private _data: Uint8Array | null = null; - /** * @zh 动画的周期。 * @en Animation duration. @@ -239,40 +575,31 @@ export class AnimationClip extends Asset { } /** - * @zh 曲线可引用的所有时间轴。 - * @en Frame keys referenced by curves. + * Gets the count of tracks this animation owns. */ - get keys () { - return this._keys; - } - - set keys (value) { - this._keys = value; + get tracksCount () { + return this._tracks.length; } /** - * @protected + * Gets an iterable to tracks. */ - get eventGroups (): readonly IAnimationEventGroup[] { - if (!this._runtimeEvents) { - this._createRuntimeEvents(); - } - return this._runtimeEvents!.eventGroups; + get tracks (): Iterable { + return this._tracks; } /** - * @protected + * Gets or sets if compression is enabled for this animation. + * When compression is enabled, + * both space and performance may be optimized at production phase. + * The price is that you can not flexible edit the animation at run time. */ - get stepness () { - return this._stepness; + get compressionEnabled () { + return this._compressionEnabled; } - /** - * @protected - */ - set stepness (value) { - this._stepness = value; - this._applyStepness(); + set compressionEnabled (value) { + this._compressionEnabled = value; } get hash () { @@ -283,66 +610,410 @@ export class AnimationClip extends Asset { return this._hash = murmurhash2_32_gc(buffer, 666); } - get curves () { - return this._curves; - } - - set curves (value) { - this._curves = value; - delete this._runtimeCurves; - } - /** - * 此动画的数据。 + * @zh 动画包含的事件数据。 + * @en Associated event data. */ - get data () { - return this._data; + get events () { + return this._events; } - get commonTargets () { - return this._commonTargets; - } + set events (value) { + this._events = value; - set commonTargets (value) { - this._commonTargets = value; + const ratios: number[] = []; + const eventGroups: IAnimationEventGroup[] = []; + const events = this.events.sort((a, b) => a.frame - b.frame); + for (const eventData of events) { + const ratio = eventData.frame / this._duration; + let i = ratios.findIndex((r) => r === ratio); + if (i < 0) { + i = ratios.length; + ratios.push(ratio); + eventGroups.push({ + events: [], + }); + } + eventGroups[i].events.push({ + functionName: eventData.func, + parameters: eventData.params, + }); + } + + this._runtimeEvents = { + ratios, + eventGroups, + }; } public onLoaded () { this.frameRate = this.sample; - this._decodeCVTAs(); } - public getPropertyCurves (): readonly IRuntimeCurve[] { - if (!this._runtimeCurves) { - this._createPropertyCurves(); + /** + * Gets the time range this animation spans. + * @returns The time range. + */ + public getRange () { + const range: Range = { min: Infinity, max: -Infinity }; + for (const track of this._tracks) { + const trackRange = track.getRange(); + range.min = Math.min(range.min, trackRange.min); + range.max = Math.max(range.max, trackRange.max); } - return this._runtimeCurves!; + return range; + } + + /** + * Gets the specified track. + * @param index Index to the track. + * @returns The track. + */ + public getTrack (index: number) { + return this._tracks[index]; + } + + /** + * Adds a track into this animation. + * @param track The track. + * @returns Index to the track. + */ + public addTrack (track: Track) { + const index = this._tracks.length; + this._tracks.push(track); + return index; + } + + /** + * Removes a track from this animation. + * @param index Index to the track. + */ + public removeTrack (index: number) { + this._tracks.splice(index, 1); + } + + /** + * Removes all tracks from this animation. + */ + public clearTracks () { + this._tracks.length = 0; + } + + /** + * Creates an event evaluator for this animation. + * @param targetNode Target node used to fire events. + * @returns @internal Do not use this in your code. + */ + public createEventEvaluator (targetNode: Node) { + return new EventEvaluator( + targetNode, + this._runtimeEvents.ratios, + this._runtimeEvents.eventGroups, + this.wrapMode, + ); + } + + /** + * Creates an evaluator for this animation. + * @param context The context. + * @returns The evaluator. + * @internal Do not use this in your code. + */ + public createEvaluator (context: AnimationClipEvalContext) { + const { + target, + } = context; + + const binder: Binder = (trackPath: TrackPath, setter: IValueProxyFactory | undefined) => { + const trackTarget = createGeneralBinding( + target, + trackPath, + setter ?? undefined, + this.enableTrsBlending ? context.pose : undefined, + false, + ); + // TODO: warning + return trackTarget ?? undefined; + }; + + return this._createEvalWithBinder(target, binder, context.rootMotion); + } + + /** + * Compresses this animation. + * @internal Do not use this in your code. + */ + public compress () { + const compressedData = new CompressedData(); + const compressedTracks = new Set(); + for (const track of this._tracks) { + let mayBeCompressed = false; + if (track instanceof RealTrack) { + mayBeCompressed = compressedData.compressRealTrack(track); + } else if (track instanceof VectorTrack) { + mayBeCompressed = compressedData.compressVectorTrack(track); + } else if (track instanceof QuaternionTrack) { + mayBeCompressed = compressedData.compressQuatTrack(track); + } + if (mayBeCompressed) { + compressedTracks.add(track); + } + } + this._compression = { + data: compressedData, + compressedTracks: Array.from(compressedTracks), + }; + } + + /** + * @internal Do not use this in your code. + */ + public purgeCompressedTracks () { + const { _compression: compression } = this; + if (!compression) { + return; + } + const compressedTracks = compression.compressedTracks; + if (!compressedTracks) { + return; + } + this._tracks = this._tracks.filter((track) => !compressedTracks.includes(track)); + compression.compressedTracks = undefined; + } + + public destroy () { + if (legacyCC.director.root.dataPoolManager) { + (legacyCC.director.root.dataPoolManager as DataPoolManager).releaseAnimationClip(this); + } + SkelAnimDataHub.destroy(this); + return super.destroy(); + } + + public [BAKE_SKELETON_CURVE_SYMBOL] (start: number, samples: number, frames: number): SkeletonAnimationBakeInfo { + const step = 1.0 / samples; + + const animatedJoints = this._collectAnimatedJoints(); + + const jointsBakeInfo: Record = {}; + for (const joint of animatedJoints) { + jointsBakeInfo[joint] = { + transforms: Array.from({ length: frames }, () => new Mat4()), + }; + } + + const skeletonFrames = animatedJoints.reduce((result, joint) => { + result[joint] = new BoneGlobalTransform(); + return result; + }, {} as Record); + for (const joint of Object.keys(skeletonFrames)) { + const skeletonFrame = skeletonFrames[joint]; + const parentJoint = joint.lastIndexOf('/'); + if (parentJoint >= 0) { + const parentJointName = joint.substring(0, parentJoint); + const parentJointFrame = skeletonFrames[parentJointName]; + if (!parentJointFrame) { + warn(`Seems like we have animation for ${joint} but are missing its parent joint ${parentJointName} in animation?`); + } else { + skeletonFrame.parent = parentJointFrame; + } + } + } + + const binder: Binder = (trackPath: TrackPath, setter: IValueProxyFactory | undefined) => { + if (setter || !isTargetingTRS(trackPath)) { + return undefined; + } + + const { path } = trackPath[0]; + const jointFrame = skeletonFrames[path]; + if (!jointFrame) { + return undefined; + } + + return createBoneTransformBinding(jointFrame, trackPath[1]); + }; + + const evaluator = this._createEvalWithBinder(undefined, binder, undefined); + + for (let iFrame = 0; iFrame < frames; ++iFrame) { + const time = start + step * iFrame; + evaluator.evaluate(time); + for (const joint of animatedJoints) { + Mat4.copy( + jointsBakeInfo[joint].transforms[iFrame], + skeletonFrames[joint].globalTransform, + ); + } + for (const joint of animatedJoints) { + skeletonFrames[joint].invalidate(); + } + } + + return { + samples, + + frames, + + joints: jointsBakeInfo, + }; + } + + /** + * Convert all untyped tracks into typed ones and delete the original. + * @param refine How to decide the type on specified path. + * @internal DO NOT USE THIS IN YOUR CODE. + */ + public upgradeUntypedTracks (refine: (path: TrackPath, setter?: IValueProxyFactory) => 'vec2' | 'vec3' | 'vec4' | 'color' | 'size') { + const newTracks: Track[] = []; + for (const track of this._tracks) { + if (!(track instanceof UntypedTrack)) { + continue; + } + const untypedTrack = track; + + const trySearchChannel = (property: string, outChannel: RealChannel) => { + const untypedChannel = untypedTrack.getChannels().find((channel) => channel.property === property); + if (untypedChannel) { + outChannel.name = untypedChannel.name; + outChannel.curve.assignSorted( + Array.from(untypedChannel.curve.times()), + Array.from(untypedChannel.curve.values()), + ); + } + }; + const kind = refine(track.path, track.setter); + switch (kind) { + default: + continue; + case 'vec2': case 'vec3': case 'vec4': { + const track = new VectorTrack(); + newTracks.push(track); + track.componentsCount = kind === 'vec2' ? 2 : kind === 'vec3' ? 3 : 4; + const [x, y, z, w] = track.getChannels(); + switch (kind) { + case 'vec4': + trySearchChannel('w', w); + // fall through + case 'vec3': + trySearchChannel('z', z); + // fall through + default: + case 'vec2': + trySearchChannel('x', x); + trySearchChannel('y', y); + } + break; + } + case 'color': { + const track = new ColorTrack(); + newTracks.push(track); + const [r, g, b, a] = track.getChannels(); + trySearchChannel('r', r); + trySearchChannel('g', g); + trySearchChannel('b', b); + trySearchChannel('a', a); + // TODO: we need float-int conversion if xyzw + trySearchChannel('x', r); + trySearchChannel('y', g); + trySearchChannel('z', b); + trySearchChannel('w', a); + break; + } + case 'size': + break; + } + } + } + + /** + * Export for test. + */ + public [searchForRootBonePathSymbol] () { + return this._searchForRootBonePath(); + } + + // #region Legacy area + // The following are significantly refactored and deprecated since 3.1. + // We deprecates the direct exposure of keys, values, events. + // Instead, we use track to organize them together. + + /** + * @zh 曲线可引用的所有时间轴。 + * @en Frame keys referenced by curves. + * @deprecated Since V3.1. + */ + get keys () { + return this._getLegacyData().keys; + } + + set keys (value) { + this._legacyDataDirty = true; + this._getLegacyData().keys = value; + } + + /** + * @zh 此动画包含的所有曲线。 + * @en Curves this animation contains. + * @deprecated Since V3.1. + */ + get curves () { + this._legacyDataDirty = true; + return this._getLegacyData().curves; + } + + set curves (value) { + this._getLegacyData().curves = value; + } + + /** + * @deprecated Since V3.1. + */ + get commonTargets () { + return this._getLegacyData().commonTargets; + } + + set commonTargets (value) { + this._legacyDataDirty = true; + this._getLegacyData().commonTargets = value; + } + + /** + * 此动画的数据。 + * @deprecated Since V3.1. + */ + get data () { + return this._getLegacyData().data; } /** - * @zh 提交事件数据的修改。 - * 当你修改了 `this.events` 时,必须调用 `this.updateEventDatas()` 使修改生效。 - * @en - * Commit event data update. - * You should call this function after you changed the `events` data to take effect. * @internal + * @deprecated Since V3.1. */ - public updateEventDatas () { - delete this._runtimeEvents; + public getPropertyCurves () { + return this._getLegacyData().getPropertyCurves(); + } + + /** + * @protected + * @deprecated Since V3.1. + */ + get eventGroups (): readonly IAnimationEventGroup[] { + return this._runtimeEvents.eventGroups; } /** - * @en Gets the event group shall be processed at specified ratio. - * @zh 获取事件组应按指定比例处理。 - * @param ratio The ratio. + * @zh 提交事件数据的修改。 + * 当你修改了 `this.events` 时,必须调用 `this.updateEventDatas()` 使修改生效。 + * @en + * Commit event data update. + * You should call this function after you changed the `events` data to take effect. * @internal + * @deprecated Since V3.1. */ - public getEventGroupIndexAtRatio (ratio: number): number { - if (!this._runtimeEvents) { - this._createRuntimeEvents(); - } - const result = binarySearchEpsilon(this._runtimeEvents!.ratios, ratio); - return result; + public updateEventDatas () { + // EMPTY } /** @@ -354,91 +1025,1284 @@ export class AnimationClip extends Asset { return this.events.length !== 0; } - public destroy () { - if (legacyCC.director.root.dataPoolManager) { - (legacyCC.director.root.dataPoolManager as DataPoolManager).releaseAnimationClip(this); + /** + * Migrates legacy data into tracks. + * @internal This method tend to be used as internal purpose or patch. + * DO NOT use it in your code since it might be removed for the future at any time. + * @deprecated Since V3.1. + */ + public syncLegacyData () { + if (this._legacyData) { + this._fromLegacy(this._legacyData); + this._legacyData = undefined; } - SkelAnimDataHub.destroy(this); - return super.destroy(); } - protected _createPropertyCurves () { - this._ratioSamplers = this._keys.map( - (keys) => new RatioSampler( - keys.map( - (key) => key / this._duration, - ), - ), + // #endregion + + @serializable + private _duration = 0; + + @serializable + private _hash = 0; + + private frameRate = 0; + + @serializable + private _tracks: Track[] = []; + + @serializable + private _compressionEnabled = false; + + @serializable + private _compression: { + data: CompressedData; + compressedTracks: Track[] | undefined; + } | undefined = undefined; + + private _legacyData: legacy.AnimationClipLegacyData | undefined = undefined; + + private _legacyDataDirty = false; + + @serializable + private _events: AnimationClip.IEvent[] = []; + + private _runtimeEvents: { + ratios: number[]; + eventGroups: IAnimationEventGroup[]; + } = { + ratios: [], + eventGroups: [], + }; + + private _createEvalWithBinder (target: unknown, binder: Binder, rootMotionOptions: RootMotionOptions | undefined) { + if (this._legacyDataDirty) { + this._legacyDataDirty = false; + this.syncLegacyData(); + } + + const rootMotionTrackExcludes: Track[] = []; + let rootMotionEvaluation: RootMotionEvaluation | undefined; + if (rootMotionOptions) { + rootMotionEvaluation = this._createRootMotionEvaluation( + target, + rootMotionOptions, + rootMotionTrackExcludes, + ); + } + + const trackEvalStatues: TrackEvalStatus[] = []; + let compressedDataEvaluator: CompressedDataEvaluator | undefined; + + for (const track of this._tracks) { + if (this._compression?.compressedTracks?.includes(track)) { + continue; + } + if (rootMotionTrackExcludes.includes(track)) { + continue; + } + const trackTarget = binder(track.path, track.setter); + if (!trackTarget) { + continue; + } + const trackEval = track[createEvalSymbol](trackTarget); + trackEvalStatues.push({ + binding: trackTarget, + trackEval, + }); + } + + if (this._compression) { + compressedDataEvaluator = this._compression.data.createEval(binder); + } + + const evaluation = new AnimationClipEvaluation( + trackEvalStatues, + compressedDataEvaluator, + rootMotionEvaluation, + ); + + return evaluation; + } + + private _createRootMotionEvaluation ( + target: unknown, + rootMotionOptions: RootMotionOptions, + rootMotionTrackExcludes: Track[], + ) { + if (!(target instanceof Node)) { + error(`Current context does not allow root motion.`); + return undefined; + } + + const rootBonePath = this._searchForRootBonePath(); + if (!rootBonePath) { + warn(`Root motion is ignored since root bone could not be located in animation.`); + return undefined; + } + + const rootBone = target.getChildByPath(rootBonePath); + if (!rootBone) { + warn(`Root motion is ignored since the root bone could not be located in scene.`); + return undefined; + } + + // const { } = rootMotionOptions; + + const boneTransform = new BoneTransform(); + const rootMotionsTrackEvaluations: TrackEvalStatus[] = []; + for (const track of this._tracks) { + const { path: trackPath } = track; + if (!isTargetingTRS(trackPath)) { + continue; + } + const bonePath = trackPath[0].path; + if (bonePath !== rootBonePath) { + continue; + } + rootMotionTrackExcludes.push(track); + const property = trackPath[1]; + const trackTarget = createBoneTransformBinding(boneTransform, property); + if (!trackTarget) { + continue; + } + const trackEval = track[createEvalSymbol](trackTarget); + rootMotionsTrackEvaluations.push({ + binding: trackTarget, + trackEval, + }); + } + const rootMotionEvaluation = new RootMotionEvaluation( + rootBone, + this._duration, + boneTransform, + rootMotionsTrackEvaluations, ); - this._runtimeCurves = this._curves.map((targetCurve): IRuntimeCurve => ({ - curve: new AnimCurve(targetCurve.data, this._duration), - modifiers: targetCurve.modifiers, - valueAdapter: targetCurve.valueAdapter, - sampler: this._ratioSamplers[targetCurve.data.keys], - commonTarget: targetCurve.commonTarget, - })); + return rootMotionEvaluation; + } - this._applyStepness(); + private _searchForRootBonePath () { + const paths = this._tracks.map((track) => { + if (isTargetingTRS(track.path)) { + const { path } = track.path[0]; + return { + path, + rank: path.split('/').length, + }; + } else { + return { + path: '', + rank: 0, + }; + } + }); + + paths.sort((a, b) => a.rank - b.rank); + + const iNonEmptyPath = paths.findIndex((p) => p.rank !== 0); + if (iNonEmptyPath < 0) { + return ''; + } + + const nPaths = paths.length; + const firstPath = paths[iNonEmptyPath]; + let highestPathsAreSame = true; + for (let iPath = iNonEmptyPath + 1; iPath < nPaths; ++iPath) { + const path = paths[iPath]; + if (path.rank !== firstPath.rank) { + break; + } + if (path.path !== firstPath.path) { + highestPathsAreSame = false; + break; + } + } + + return highestPathsAreSame ? firstPath.path : ''; } - protected _createRuntimeEvents () { - if (EDITOR && !legacyCC.GAME_VIEW) { - return; + private _getLegacyData () { + if (!this._legacyData) { + this._legacyData = this._toLegacy(); } + return this._legacyData; + } - const ratios: number[] = []; - const eventGroups: IAnimationEventGroup[] = []; - const events = this.events.sort((a, b) => a.frame - b.frame); - for (const eventData of events) { - const ratio = eventData.frame / this._duration; - let i = ratios.findIndex((r) => r === ratio); - if (i < 0) { - i = ratios.length; - ratios.push(ratio); - eventGroups.push({ - events: [], - }); + private _toLegacy (): legacy.AnimationClipLegacyData { + const keys: number[][] = []; + const legacyCurves: legacy.LegacyClipCurve[] = []; + const commonTargets: legacy.LegacyCommonTarget[] = []; + + const legacyClipData = new legacy.AnimationClipLegacyData(this._duration); + legacyClipData.keys = keys; + legacyClipData.curves = legacyCurves; + legacyClipData.commonTargets = commonTargets; + return legacyClipData; + } + + private _fromLegacy (legacyData: legacy.AnimationClipLegacyData) { + const newTracks: Track[] = []; + + const { + keys: legacyKeys, + curves: legacyCurves, + commonTargets: legacyCommonTargets, + } = legacyData; + + const untypedTracks = legacyCommonTargets.map((legacyCommonTarget) => { + const track = new UntypedTrack(); + track.path = legacyCommonTarget.modifiers; + track.setter = legacyCommonTarget.valueAdapter; + newTracks.push(track); + return track; + }); + + for (const legacyCurve of legacyCurves) { + const legacyCurveData = legacyCurve.data; + const legacyValues = legacyCurveData.values; + if (legacyValues.length === 0) { + // Legacy clip did not record type info. + continue; } - eventGroups[i].events.push({ - functionName: eventData.func, - parameters: eventData.params, + const legacyKeysIndex = legacyCurveData.keys; + // Rule: negative index means single frame. + const times = legacyKeysIndex < 0 ? [0.0] : legacyKeys[legacyCurveData.keys]; + const firstValue = legacyValues[0]; + // Rule: default to true. + const interpolate = legacyCurveData ?? true; + // Rule: _arrayLength only used for morph target, internally. + assertIsTrue(typeof legacyCurveData._arrayLength !== 'number' || typeof firstValue === 'number'); + const legacyEasingMethodConverter = new legacy.LegacyEasingMethodConverter(legacyCurveData, times.length); + + const installPathAndSetter = (track: Track) => { + track.path = legacyCurve.modifiers; + track.setter = legacyCurve.valueAdapter; + }; + + let legacyCommonTargetCurve: RealCurve | undefined; + if (typeof legacyCurve.commonTarget === 'number') { + // Rule: common targets should only target Vectors/`Size`/`Color`. + if (!legacyValues.every((value) => typeof value === 'number')) { + warn(`Incorrect curve.`); + continue; + } + // Rule: Each curve that has common target should be numeric curve and targets string property. + if (legacyCurve.valueAdapter || legacyCurve.modifiers.length !== 1 || typeof legacyCurve.modifiers[0] !== 'string') { + warn(`Incorrect curve.`); + continue; + } + const propertyName = legacyCurve.modifiers[0]; + const untypedTrack = untypedTracks[legacyCurve.commonTarget]; + const { curve } = untypedTrack.addChannel(propertyName); + legacyCommonTargetCurve = curve; + } + + const convertCurve = () => { + if (typeof firstValue === 'number') { + if (!legacyValues.every((value) => typeof value === 'number')) { + warn(`Misconfigured curve.`); + return; + } + let realCurve: RealCurve; + if (legacyCommonTargetCurve) { + realCurve = legacyCommonTargetCurve; + } else { + const track = new RealTrack(); + installPathAndSetter(track); + newTracks.push(track); + realCurve = track.channel.curve; + } + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + realCurve.assignSorted(times, (legacyValues as number[]).map((value) => new RealKeyframeValue({ value, interpMode: interpMethod }))); + legacyEasingMethodConverter.convert(realCurve); + return; + } else if (typeof firstValue === 'object') { + switch (true) { + default: + break; + case legacyValues.every((value) => value instanceof Vec2): + case legacyValues.every((value) => value instanceof Vec3): + case legacyValues.every((value) => value instanceof Vec4): { + type Vec4plus = Vec4[]; + type Vec3plus = (Vec3 | Vec4)[]; + type Vec2plus = (Vec2 | Vec3 | Vec4)[]; + const components = firstValue instanceof Vec2 ? 2 : firstValue instanceof Vec3 ? 3 : 4; + const track = new VectorTrack(); + installPathAndSetter(track); + track.componentsCount = components; + const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.getChannels(); + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); + switch (components) { + case 4: + w.assignSorted(times, (legacyValues as Vec4plus).map((value) => valueToFrame(value.w))); + legacyEasingMethodConverter.convert(w); + // falls through + case 3: + z.assignSorted(times, (legacyValues as Vec3plus).map((value) => valueToFrame(value.z))); + legacyEasingMethodConverter.convert(z); + // falls through + default: + x.assignSorted(times, (legacyValues as Vec2plus).map((value) => valueToFrame(value.x))); + legacyEasingMethodConverter.convert(x); + y.assignSorted(times, (legacyValues as Vec2plus).map((value) => valueToFrame(value.y))); + legacyEasingMethodConverter.convert(y); + break; + } + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof Quat): { + assertIsTrue(legacyEasingMethodConverter.nil); + const track = new QuaternionTrack(); + installPathAndSetter(track); + const interpMode = interpolate ? QuaternionInterpMode.SLERP : QuaternionInterpMode.CONSTANT; + track.channel.curve.assignSorted(times, (legacyValues as Quat[]).map((value) => ({ + value: Quat.clone(value), + interpMode, + }))); + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof Color): { + const track = new ColorTrack(); + installPathAndSetter(track); + const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.getChannels(); + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); + r.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.r))); + legacyEasingMethodConverter.convert(r); + g.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.g))); + legacyEasingMethodConverter.convert(g); + b.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.b))); + legacyEasingMethodConverter.convert(b); + a.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.a))); + legacyEasingMethodConverter.convert(a); + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof CubicSplineNumberValue): { + assertIsTrue(legacyEasingMethodConverter.nil); + const track = new RealTrack(); + installPathAndSetter(track); + const interpMethod = interpolate ? RealInterpMode.CUBIC : RealInterpMode.CONSTANT; + track.channel.curve.assignSorted(times, (legacyValues as CubicSplineNumberValue[]).map((value) => new RealKeyframeValue({ + value: value.dataPoint, + startTangent: value.inTangent, + endTangent: value.outTangent, + interpMode: interpMethod, + }))); + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof CubicSplineVec2Value): + case legacyValues.every((value) => value instanceof CubicSplineVec3Value): + case legacyValues.every((value) => value instanceof CubicSplineVec4Value): { + assertIsTrue(legacyEasingMethodConverter.nil); + type Vec4plus = CubicSplineVec4Value[]; + type Vec3plus = (CubicSplineVec3Value | CubicSplineVec4Value)[]; + type Vec2plus = (CubicSplineVec2Value | CubicSplineVec3Value | CubicSplineVec4Value)[]; + const components = firstValue instanceof CubicSplineVec2Value ? 2 : firstValue instanceof CubicSplineVec3Value ? 3 : 4; + const track = new VectorTrack(); + installPathAndSetter(track); + track.componentsCount = components; + const [x, y, z, w] = track.getChannels(); + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const valueToFrame = (value: number, startTangent: number, endTangent: number): RealKeyframeValue => new RealKeyframeValue({ + value, + startTangent, + endTangent, + interpMode: interpMethod, + }); + switch (components) { + case 4: + w.curve.assignSorted(times, (legacyValues as Vec4plus).map( + (value) => valueToFrame(value.dataPoint.w, value.inTangent.w, value.outTangent.w), + )); + // falls through + case 3: + z.curve.assignSorted(times, (legacyValues as Vec3plus).map( + (value) => valueToFrame(value.dataPoint.z, value.inTangent.z, value.outTangent.z), + )); + // falls through + default: + x.curve.assignSorted(times, (legacyValues as Vec2plus).map( + (value) => valueToFrame(value.dataPoint.y, value.inTangent.y, value.outTangent.y), + )); + y.curve.assignSorted(times, (legacyValues as Vec2plus).map( + (value) => valueToFrame(value.dataPoint.x, value.inTangent.x, value.outTangent.x), + )); + break; + } + newTracks.push(track); + return; + } + } // End switch + } + + const objectTrack = new ObjectTrack(); + installPathAndSetter(objectTrack); + objectTrack.channel.curve.assignSorted(times, legacyValues); + newTracks.push(objectTrack); + }; + + convertCurve(); + } + + for (const track of newTracks) { + this.addTrack(track); + } + } + + private _collectAnimatedJoints () { + const joints = new Set(); + + for (const track of this._tracks) { + if (!track.setter && isTargetingTRS(track.path)) { + const { path } = track.path[0]; + joints.add(path); + } + } + + if (this._compression) { + for (const joint of this._compression.data.collectAnimatedJoints()) { + joints.add(joint); + } + } + + return Array.from(joints); + } +} + +legacyCC.AnimationClip = AnimationClip; + +type Binder = (path: TrackPath, setter: IValueProxyFactory | undefined) => undefined | RuntimeBinding; + +type RuntimeBinding = { + setValue(value: unknown): void; + + getValue?(): unknown; +}; + +// #region Data compression + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}CompressedData`) +class CompressedData { + public compressRealTrack (track: RealTrack) { + const curve = track.channel.curve; + const mayBeCompressed = KeySharedRealCurves.allowedForCurve(curve); + if (!mayBeCompressed) { + return false; + } + this._tracks.push({ + type: CompressedDataTrackType.FLOAT, + path: track.path, + setter: track.setter, + components: [this._addRealCurve(curve)], + }); + return true; + } + + public compressVectorTrack (vectorTrack: VectorTrack) { + const nComponents = vectorTrack.componentsCount; + const channels = vectorTrack.getChannels(); + const mayBeCompressed = channels.every(({ curve }) => KeySharedRealCurves.allowedForCurve(curve)); + if (!mayBeCompressed) { + return false; + } + const components = new Array(nComponents); + for (let i = 0; i < nComponents; ++i) { + const channel = channels[i]; + components[i] = this._addRealCurve(channel.curve); + } + this._tracks.push({ + type: + nComponents === 2 + ? CompressedDataTrackType.VEC2 + : nComponents === 3 + ? CompressedDataTrackType.VEC3 + : CompressedDataTrackType.VEC4, + path: vectorTrack.path, + setter: vectorTrack.setter, + components, + }); + return true; + } + + public compressQuatTrack (track: QuaternionTrack) { + const curve = track.channel.curve; + const mayBeCompressed = KeySharedQuaternionCurves.allowedForCurve(curve); + if (!mayBeCompressed) { + return false; + } + this._quatTracks.push({ + path: track.path, + setter: track.setter, + pointer: this._addQuaternionCurve(curve), + }); + return true; + } + + public createEval (binder: Binder) { + const compressedDataEvalStatus: CompressedDataEvalStatus = { + keySharedCurvesEvalStatuses: [], + trackEvalStatuses: [], + keysSharedQuatCurvesEvalStatues: [], + quatTrackEvalStatuses: [], + }; + + const { + keySharedCurvesEvalStatuses, + trackEvalStatuses, + keysSharedQuatCurvesEvalStatues, + quatTrackEvalStatuses, + } = compressedDataEvalStatus; + + for (const curves of this._curves) { + keySharedCurvesEvalStatuses.push({ + curves, + result: new Array(curves.curveCount).fill(0.0), }); } - this._runtimeEvents = { - ratios, - eventGroups, + for (const track of this._tracks) { + const trackTarget = binder(track.path, track.setter); + if (!trackTarget) { + continue; + } + let immediate: CompressedTrackImmediate | undefined; + switch (track.type) { + default: + case CompressedDataTrackType.FLOAT: + break; + case CompressedDataTrackType.VEC2: + immediate = new Vec2(); + break; + case CompressedDataTrackType.VEC3: + immediate = new Vec3(); + break; + case CompressedDataTrackType.VEC4: + immediate = new Vec4(); + break; + } + trackEvalStatuses.push({ + type: track.type, + target: trackTarget, + curves: track.components, + immediate, + }); + } + + for (const curves of this._quatCurves) { + keysSharedQuatCurvesEvalStatues.push({ + curves, + result: Array.from({ length: curves.curveCount }, () => new Quat()), + }); + } + + for (const track of this._quatTracks) { + const trackTarget = binder(track.path, track.setter); + if (!trackTarget) { + continue; + } + quatTrackEvalStatuses.push({ + target: trackTarget, + curve: track.pointer, + }); + } + + return new CompressedDataEvaluator(compressedDataEvalStatus); + } + + public collectAnimatedJoints () { + const joints: string[] = []; + + for (const track of this._tracks) { + if (!track.setter && isTargetingTRS(track.path)) { + const { path } = track.path[0]; + joints.push(path); + } + } + + return joints; + } + + @serializable + private _curves: KeySharedRealCurves[] = []; + + @serializable + private _tracks: CompressedTrack[] = []; + + @serializable + private _quatCurves: KeySharedQuaternionCurves[] = []; + + @serializable + private _quatTracks: CompressedQuatTrack[] = []; + + private _addRealCurve (curve: RealCurve): CompressedCurvePointer { + const times = Array.from(curve.times()); + let iKeySharedCurves = this._curves.findIndex((shared) => shared.matchCurve(curve)); + if (iKeySharedCurves < 0) { + iKeySharedCurves = this._curves.length; + const keySharedCurves = new KeySharedRealCurves(times); + this._curves.push(keySharedCurves); + } + const iCurve = this._curves[iKeySharedCurves].curveCount; + this._curves[iKeySharedCurves].addCurve(curve); + return { + shared: iKeySharedCurves, + component: iCurve, }; } - protected _applyStepness () { - // for (const propertyCurve of this._propertyCurves) { - // propertyCurve.curve.stepfy(this._stepness); - // } + public _addQuaternionCurve (curve: QuaternionCurve): CompressedQuatCurvePointer { + const times = Array.from(curve.times()); + let iKeySharedCurves = this._quatCurves.findIndex((shared) => shared.matchCurve(curve)); + if (iKeySharedCurves < 0) { + iKeySharedCurves = this._quatCurves.length; + const keySharedCurves = new KeySharedQuaternionCurves(times); + this._quatCurves.push(keySharedCurves); + } + const iCurve = this._quatCurves[iKeySharedCurves].curveCount; + this._quatCurves[iKeySharedCurves].addCurve(curve); + return { + shared: iKeySharedCurves, + curve: iCurve, + }; } - private _decodeCVTAs () { - const binaryBuffer: ArrayBuffer = ArrayBuffer.isView(this._nativeAsset) ? this._nativeAsset.buffer : this._nativeAsset; - if (!binaryBuffer) { - return; + public validate () { + return this._tracks.length > 0; + } +} + +class CompressedDataEvaluator { + constructor (compressedDataEvalStatus: CompressedDataEvalStatus) { + this._compressedDataEvalStatus = compressedDataEvalStatus; + } + + public evaluate (time: number) { + const { + keySharedCurvesEvalStatuses, + trackEvalStatuses: compressedTrackEvalStatuses, + keysSharedQuatCurvesEvalStatues, + quatTrackEvalStatuses, + } = this._compressedDataEvalStatus; + + const getPreEvaluated = (pointer: CompressedCurvePointer) => keySharedCurvesEvalStatuses[pointer.shared].result[pointer.component]; + + for (const { curves, result } of keySharedCurvesEvalStatuses) { + curves.evaluate(time, result); } - const maybeCompressedKeys = this._keys as AnimationClip._impl.MaybeCompactKeys; - for (let iKey = 0; iKey < maybeCompressedKeys.length; ++iKey) { - const keys = maybeCompressedKeys[iKey]; - if (keys instanceof CompactValueTypeArray) { - maybeCompressedKeys[iKey] = keys.decompress(binaryBuffer); + for (const { type, target, immediate, curves } of compressedTrackEvalStatuses) { + let value: unknown = immediate; + switch (type) { + default: + break; + case CompressedDataTrackType.FLOAT: + value = getPreEvaluated(curves[0]); + break; + case CompressedDataTrackType.VEC2: + Vec2.set( + value as Vec2, + getPreEvaluated(curves[0]), + getPreEvaluated(curves[1]), + ); + break; + case CompressedDataTrackType.VEC3: + Vec3.set( + value as Vec3, + getPreEvaluated(curves[0]), + getPreEvaluated(curves[1]), + getPreEvaluated(curves[2]), + ); + break; + case CompressedDataTrackType.VEC4: + Vec4.set( + value as Vec4, + getPreEvaluated(curves[0]), + getPreEvaluated(curves[1]), + getPreEvaluated(curves[2]), + getPreEvaluated(curves[4]), + ); + break; } + target.setValue(value); + } + + for (const { curves, result } of keysSharedQuatCurvesEvalStatues) { + curves.evaluate(time, result); + } + + for (const { target, curve } of quatTrackEvalStatuses) { + target.setValue(keysSharedQuatCurvesEvalStatues[curve.shared].result[curve.curve]); } + } + + private _compressedDataEvalStatus: CompressedDataEvalStatus; +} + +interface CompressedTrack { + path: TrackPath; + setter: IValueProxyFactory | undefined; + type: CompressedDataTrackType; + components: CompressedCurvePointer[]; +} + +interface CompressedQuatTrack { + path: TrackPath; + setter: IValueProxyFactory | undefined; + pointer: CompressedQuatCurvePointer; +} + +enum CompressedDataTrackType { + FLOAT, + VEC2, + VEC3, + VEC4, +} - for (let iCurve = 0; iCurve < this._curves.length; ++iCurve) { - const curve = this._curves[iCurve] as AnimationClip._impl.MaybeCompactCurve; - if (curve.data.values instanceof CompactValueTypeArray) { - curve.data.values = curve.data.values.decompress(binaryBuffer); +interface TrackEvalStatus { + binding: RuntimeBinding; + trackEval: TrackEval; +} + +type CompressedTrackImmediate = Vec2 | Vec3 | Vec4; + +interface CompressedDataEvalStatus { + keySharedCurvesEvalStatuses: Array<{ + curves: KeySharedRealCurves; + result: number[]; + }>; + + trackEvalStatuses: Array<{ + type: CompressedDataTrackType; + target: RuntimeBinding; + immediate: CompressedTrackImmediate | undefined; + curves: CompressedCurvePointer[]; + }>; + + keysSharedQuatCurvesEvalStatues: Array<{ + curves: KeySharedQuaternionCurves; + result: Quat[]; + }>; + + quatTrackEvalStatuses: Array<{ + target: RuntimeBinding; + curve: CompressedQuatCurvePointer; + }>; +} + +interface CompressedCurvePointer { + shared: number; + component: number; +} + +interface CompressedQuatCurvePointer { + shared: number; + curve: number; +} + +// #endregion +interface AnimationClipEvalContext { + /** + * The output pose. + */ + pose?: PoseOutput; + + /** + * The root animating target(should be scene node now). + */ + target: unknown; + + /** + * Path to the root bone. + */ + rootMotion?: RootMotionOptions; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface RootMotionOptions { +} + +class AnimationClipEvaluation { + /** + * @internal + * @param trackEvalStatuses + * @param compressedDataEvaluator + */ + constructor ( + trackEvalStatuses: TrackEvalStatus[], + compressedDataEvaluator: CompressedDataEvaluator | undefined, + rootMotionEvaluation: RootMotionEvaluation | undefined, + ) { + this._trackEvalStatues = trackEvalStatuses; + this._compressedDataEvaluator = compressedDataEvaluator; + this._rootMotionEvaluation = rootMotionEvaluation; + } + + /** + * Evaluates this animation. + * @param time The time. + */ + public evaluate (time: number) { + const { + _trackEvalStatues: trackEvalStatuses, + _compressedDataEvaluator: compressedDataEvaluator, + } = this; + + for (const trackEvalStatus of trackEvalStatuses) { + const value = trackEvalStatus.trackEval.evaluate(time, trackEvalStatus.binding); + trackEvalStatus.binding.setValue(value); + } + + if (compressedDataEvaluator) { + compressedDataEvaluator.evaluate(time); + } + } + + /** + * Gets the root bone motion. + * @param startTime Start time. + * @param endTime End time. + */ + public evaluateRootMotion (time: number, motionLength: number) { + const { _rootMotionEvaluation: rootMotionEvaluation } = this; + if (rootMotionEvaluation) { + rootMotionEvaluation.evaluate(time, motionLength); + } + } + + private _compressedDataEvaluator: CompressedDataEvaluator | undefined; + private _trackEvalStatues:TrackEvalStatus[] = []; + private _rootMotionEvaluation: RootMotionEvaluation | undefined = undefined; +} + +class BoneTransform { + public position = new Vec3(); + public scale = new Vec3(1.0, 1.0, 1.0); + public rotation = new Quat(); + public eulerAngles = new Vec3(); + + public getTransform (out: Mat4) { + Mat4.fromRTS(out, this.rotation, this.position, this.scale); + } +} + +class BoneGlobalTransform extends BoneTransform { + public parent: BoneGlobalTransform | null = null; + + public get globalTransform (): Readonly { + const transform = this._transform; + if (this._dirty) { + this._dirty = false; + Mat4.fromRTS(transform, this.rotation, this.position, this.scale); + if (this.parent) { + Mat4.multiply(transform, this.parent.globalTransform, transform); } } + return this._transform; } + + public invalidate () { + this._dirty = true; + } + + private _dirty = true; + private _transform = new Mat4(); } -legacyCC.AnimationClip = AnimationClip; +const motionTransformCache = new Mat4(); + +class RootMotionEvaluation { + constructor ( + private _rootBone: Node, + private _duration: number, + private _boneTransform: BoneTransform, + private _trackEvalStatuses: TrackEvalStatus[], + ) { + + } + + public evaluate (time: number, motionLength: number) { + const motionTransform = this._calcMotionTransform(time, motionLength, this._motionTransformCache); + + const { + _translationMotionCache: translationMotion, + _rotationMotionCache: rotationMotion, + _scaleMotionCache: scaleMotion, + _rootBone: rootBone, + } = this; + + Mat4.toRTS(motionTransform, rotationMotion, translationMotion, scaleMotion); + + Vec3.add(translationMotion, translationMotion, rootBone.position); + rootBone.setPosition(translationMotion); + + Quat.multiply(rotationMotion, rotationMotion, rootBone.rotation); + rootBone.setRotation(rotationMotion); + + Vec3.multiply(scaleMotion, scaleMotion, rootBone.scale); + rootBone.setScale(scaleMotion); + } + + private _calcMotionTransform (time: number, motionLength: number, outTransform: Mat4) { + const { _duration: duration } = this; + const remainLength = duration - time; + assertIsTrue(remainLength >= 0); + const startTransform = this._evaluateAt(time, this._startTransformCache); + if (motionLength < remainLength) { + const endTransform = this._evaluateAt(time + motionLength, this._endTransformCache); + relativeTransform(outTransform, startTransform, endTransform); + } else { + Mat4.identity(outTransform); + + const accumulateMotionTransform = (from: Mat4, to: Mat4) => { + relativeTransform(motionTransformCache, from, to); + Mat4.multiply(outTransform, outTransform, motionTransformCache); + }; + + const diff = motionLength - remainLength; + const repeatCount = Math.floor(diff / duration); + const lastRemainTime = diff - repeatCount * duration; + const clipStartTransform = this._evaluateAt(0, this._initialTransformCache); + const clipEndTransform = this._evaluateAt(duration, this._clipEndTransformCache); + const endTransform = this._evaluateAt(lastRemainTime, this._endTransformCache); + + // Start -> Clip End + accumulateMotionTransform(startTransform, clipEndTransform); + + // Whole clip x Repeat Count + relativeTransform(motionTransformCache, clipStartTransform, clipEndTransform); + for (let i = 0; i < repeatCount; ++i) { + Mat4.multiply(outTransform, outTransform, motionTransformCache); + } + + // Clip Start -> End + accumulateMotionTransform(clipStartTransform, endTransform); + } + return outTransform; + } + + private _evaluateAt (time: number, outTransform: Mat4) { + const { + _trackEvalStatuses: trackEvalStatuses, + } = this; + + for (const trackEvalStatus of trackEvalStatuses) { + const value = trackEvalStatus.trackEval.evaluate(time, trackEvalStatus.binding); + trackEvalStatus.binding.setValue(value); + } + + this._boneTransform.getTransform(outTransform); + return outTransform; + } + + private _initialTransformCache = new Mat4(); + private _clipEndTransformCache = new Mat4(); + private _startTransformCache = new Mat4(); + private _endTransformCache = new Mat4(); + private _motionTransformCache = new Mat4(); + private _translationMotionCache = new Vec3(); + private _rotationMotionCache = new Quat(); + private _scaleMotionCache = new Vec3(); +} + +function relativeTransform (out: Mat4, from: Mat4, to: Mat4) { + Mat4.invert(out, from); + Mat4.multiply(out, to, out); +} + +function createBoneTransformBinding (boneTransform: BoneTransform, property: TrsTrackPath[1]) { + switch (property) { + default: + return undefined; + case 'position': + return { + setValue (value: Vec3) { + Vec3.copy(boneTransform.position, value); + }, + }; + case 'rotation': + return { + setValue (value: Quat) { + Quat.copy(boneTransform.rotation, value); + }, + }; + case 'scale': + return { + setValue (value: Vec3) { + Vec3.copy(boneTransform.scale, value); + }, + }; + case 'eulerAngles': + return { + setValue (value: Vec3) { + Vec3.copy(boneTransform.eulerAngles, value); + }, + }; + } +} + +/** + * Bind runtime target. Especially optimized for skeletal case. + */ +function createGeneralBinding ( + rootTarget: unknown, + path: TrackPath, + setter: IValueProxyFactory | undefined, + poseOutput: PoseOutput | undefined, + isConstant: boolean, +): RuntimeBinding | null { + if (!isTargetingTRS(path) || !poseOutput) { + return createRuntimeBinding(rootTarget, path, setter); + } else { + const targetNode = evaluatePath(rootTarget, ...path.slice(0, path.length - 1)); + if (targetNode !== null && targetNode instanceof Node) { + const propertyName = path[path.length - 1] as 'position' | 'rotation' | 'scale' | 'eulerAngles'; + const blendStateWriter = poseOutput.createPoseWriter(targetNode, propertyName, isConstant); + return blendStateWriter; + } + } + return null; +} + +type TrsTrackPath = [HierarchyPath, 'position' | 'rotation' | 'scale' | 'eulerAngles']; + +function isTargetingTRS (path: TargetPath[]): path is TrsTrackPath { + let prs: string | undefined; + if (path.length === 1 && typeof path[0] === 'string') { + prs = path[0]; + } else if (path.length > 1) { + for (let i = 0; i < path.length - 1; ++i) { + if (!(path[i] instanceof HierarchyPath)) { + return false; + } + } + prs = path[path.length - 1] as string; + } + switch (prs) { + case 'position': + case 'scale': + case 'rotation': + case 'eulerAngles': + return true; + default: + return false; + } +} + +function createRuntimeBinding (target: unknown, trackPath: TrackPath, setter?: IValueProxyFactory): null | RuntimeBinding { + const lastPath = trackPath[trackPath.length - 1]; + if (trackPath.length !== 0 && isPropertyPath(lastPath) && !setter) { + const resultTarget = evaluatePath(target, ...trackPath.slice(0, trackPath.length - 1)); + if (resultTarget === null) { + return null; + } + return { + setValue: (value) => { + resultTarget[lastPath] = value; + }, + // eslint-disable-next-line arrow-body-style + getValue: () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return resultTarget[lastPath]; + }, + }; + } else if (!setter) { + error( + `You provided a ill-formed track path.` + + `The last component of track path should be property key, or the setter should not be empty.`, + ); + return null; + } else { + const resultTarget = evaluatePath(target, ...trackPath); + if (resultTarget === null) { + return null; + } + const proxy = setter.forTarget(resultTarget); + const binding: RuntimeBinding = { + setValue: (value) => { + proxy.set(value); + }, + }; + const proxyGet = proxy.get; + if (proxyGet) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + binding.getValue = () => proxyGet.call(proxy); + } + return binding; + } +} + +// #region Events + +interface IAnimationEvent { + functionName: string; + parameters: string[]; +} + +interface IAnimationEventGroup { + events: IAnimationEvent[]; +} + +const InvalidIndex = -1; + +class EventEvaluator { + constructor ( + private _targetNode: Node, + private _ratios: readonly number[], + private _eventGroups: readonly IAnimationEventGroup[], + private _wrapMode: WrapMode, + ) { + + } + + public setWrapMode (wrapMode: WrapMode) { + this._wrapMode = wrapMode; + } + + public ignore (ratio: number, direction: number) { + this._ignoreIndex = InvalidIndex; + this._sampled = false; + + let frameIndex = getEventGroupIndexAtRatio(ratio, this._ratios); + + // only ignore when time not on a frame index + if (frameIndex < 0) { + frameIndex = ~frameIndex - 1; + + // if direction is inverse, then increase index + if (direction < 0) { frameIndex += 1; } + + this._ignoreIndex = frameIndex; + } + } + + public sample (ratio: number, direction: number, iterations: number) { + const length = this._eventGroups.length; + let eventIndex = getEventGroupIndexAtRatio(ratio, this._ratios); + if (eventIndex < 0) { + eventIndex = ~eventIndex - 1; + // If direction is inverse, increase index. + if (direction < 0) { + eventIndex += 1; + } + } + + if (this._ignoreIndex !== eventIndex) { + this._ignoreIndex = InvalidIndex; + } + + if (!this._sampled) { + this._sampled = true; + this._doFire(eventIndex, false); + this._lastFrameIndex = eventIndex; + this._lastIterations = iterations; + this._lastDirection = direction; + return; + } + + const wrapMode = this._wrapMode; + const currentIterations = wrapIterations(iterations); + + let lastIterations = wrapIterations(this._lastIterations); + let lastIndex = this._lastFrameIndex; + const lastDirection = this._lastDirection; + + const iterationsChanged = lastIterations !== -1 && currentIterations !== lastIterations; + + if (lastIndex === eventIndex && iterationsChanged && length === 1) { + this._doFire(0, false); + } else if (lastIndex !== eventIndex || iterationsChanged) { + direction = lastDirection; + + do { + if (lastIndex !== eventIndex) { + if (direction === -1 && lastIndex === 0 && eventIndex > 0) { + if ((wrapMode & WrapModeMask.PingPong) === WrapModeMask.PingPong) { + direction *= -1; + } else { + lastIndex = length; + } + lastIterations++; + } else if (direction === 1 && lastIndex === length - 1 && eventIndex < length - 1) { + if ((wrapMode & WrapModeMask.PingPong) === WrapModeMask.PingPong) { + direction *= -1; + } else { + lastIndex = -1; + } + lastIterations++; + } + + if (lastIndex === eventIndex) { + break; + } + if (lastIterations > currentIterations) { + break; + } + } + + lastIndex += direction; + this._doFire(lastIndex, true); + } while (lastIndex !== eventIndex && lastIndex > -1 && lastIndex < length); + } + + this._lastFrameIndex = eventIndex; + this._lastIterations = iterations; + this._lastDirection = direction; + } + + private _lastFrameIndex = -1; + private _lastIterations = 0.0; + private _lastDirection = 0; + private _ignoreIndex = InvalidIndex; + private _sampled = false; + + private _doFire (eventIndex: number, delay: boolean) { + if (delay) { + legacyCC.director.getAnimationManager().pushDelayEvent(this._checkAndFire, this, [eventIndex]); + } else { + this._checkAndFire(eventIndex); + } + } + + private _checkAndFire (eventIndex: number) { + if (!this._targetNode || !this._targetNode.isValid) { + return; + } + + const { _eventGroups: eventGroups } = this; + if (eventIndex < 0 || eventIndex >= eventGroups.length || this._ignoreIndex === eventIndex) { + return; + } + + const eventGroup = eventGroups[eventIndex]; + const components = this._targetNode.components; + for (const event of eventGroup.events) { + const { functionName } = event; + for (const component of components) { + const fx = component[functionName]; + if (typeof fx === 'function') { + fx.apply(component, event.parameters); + } + } + } + } +} + +function wrapIterations (iterations: number) { + if (iterations - (iterations | 0) === 0) { + iterations -= 1; + } + return iterations | 0; +} + +function getEventGroupIndexAtRatio (ratio: number, ratios: readonly number[]) { + const result = binarySearchEpsilon(ratios, ratio); + return result; +} + +// #endregion diff --git a/cocos/core/animation/animation-curve.ts b/cocos/core/animation/animation-curve.ts index b3ddf63b78b..2ab3926e5a7 100644 --- a/cocos/core/animation/animation-curve.ts +++ b/cocos/core/animation/animation-curve.ts @@ -36,67 +36,7 @@ import { bezierByTime, BezierControlPoints } from './bezier'; import * as easing from './easing'; import { ILerpable, isLerpable } from './types'; import { legacyCC } from '../global-exports'; - -/** - * 表示曲线值,曲线值可以是任意类型,但必须符合插值方式的要求。 - */ -export type CurveValue = any; - -/** - * 表示曲线的目标对象。 - */ -export type CurveTarget = Record; - -/** - * 内置帧时间渐变方式名称。 - */ -export type EasingMethodName = keyof (typeof easing); - -/** - * 帧时间渐变方式。可能为内置帧时间渐变方式的名称或贝塞尔控制点。 - */ -export type EasingMethod = EasingMethodName | BezierControlPoints; - -type LerpFunction = (from: T, to: T, t: number, dt: number) => T; - -type CompressedEasingMethods = Record; - -/** - * 曲线数据。 - */ -export interface IPropertyCurveData { - /** - * 曲线使用的时间轴。 - * @see {AnimationClip.keys} - */ - keys: number; - - /** - * 曲线值。曲线值的数量应和 `keys` 所引用时间轴的帧数相同。 - */ - values: CurveValue[]; - - /** - * 曲线任意两帧时间的渐变方式。仅当 `easingMethods === undefined` 时本字段才生效。 - */ - easingMethod?: EasingMethod; - - /** - * 描述了每一帧时间到下一帧时间之间的渐变方式。 - */ - easingMethods?: EasingMethod[] | CompressedEasingMethods; - - /** - * 是否进行插值。 - * @default true - */ - interpolate?: boolean; - - /** - * For internal usage only. - */ - _arrayLength?: number; -} +import type * as legacy from './legacy-clip-data'; export class RatioSampler { public ratios: number[]; @@ -138,14 +78,14 @@ export class AnimCurve { return controlPoints as BezierControlPoints; } - public types?: Array<(EasingMethod | null)> = undefined; + public types?: Array<(legacy.LegacyEasingMethod | null)> = undefined; - public type?: EasingMethod | null = null; + public type?: legacy.LegacyEasingMethod | null = null; /** * The values of the keyframes. (y) */ - private _values: CurveValue[] = []; + private _values: legacy.LegacyCurveValue[] = []; /** * Lerp function used. If undefined, no lerp is performed. @@ -156,13 +96,13 @@ export class AnimCurve { private _array?: any[]; - constructor (propertyCurveData: Omit, duration: number) { + constructor (propertyCurveData: Omit, duration: number) { this._duration = duration; // Install values. this._values = propertyCurveData.values; - const getCurveType = (easingMethod: EasingMethod) => { + const getCurveType = (easingMethod: legacy.LegacyEasingMethod) => { if (typeof easingMethod === 'string') { return easingMethod; } else if (Array.isArray(easingMethod)) { @@ -323,7 +263,7 @@ legacyCC.sampleAnimationCurve = sampleAnimationCurve; * @param type - If it's Array, then ratio will be computed with bezierByTime. * If it's string, then ratio will be computed with cc.easing function */ -export function computeRatioByType (ratio: number, type: EasingMethod) { +export function computeRatioByType (ratio: number, type: legacy.LegacyEasingMethod) { if (typeof type === 'string') { const func = easing[type]; if (func) { @@ -389,7 +329,7 @@ const selectLerpFx = (() => { return (from: Quat, to: Quat, t: number, dt: number) => Quat.slerp(tempValue, from, to, t); } - return (value: any): LerpFunction | undefined => { + return (value: any): legacy.LegacyLerpFunction | undefined => { if (value === null) { return undefined; } diff --git a/cocos/core/animation/animation-state.ts b/cocos/core/animation/animation-state.ts index 949e1cfd3ec..70dd0411d55 100644 --- a/cocos/core/animation/animation-state.ts +++ b/cocos/core/animation/animation-state.ts @@ -30,18 +30,14 @@ import { EDITOR } from 'internal:constants'; import { Node } from '../scene-graph/node'; -import { AnimationClip, IRuntimeCurve } from './animation-clip'; -import { AnimCurve, RatioSampler } from './animation-curve'; -import { createBoundTarget, createBufferedTarget, IBufferedTarget, IBoundTarget } from './bound-target'; +import { AnimationClip } from './animation-clip'; import { Playable } from './playable'; import { WrapMode, WrapModeMask, WrappedInfo } from './types'; -import { HierarchyPath, evaluatePath, TargetPath } from './target-path'; -import { BlendStateBuffer, BlendStateWriter } from '../../3d/skeletal-animation/skeletal-animation-blending'; import { legacyCC } from '../global-exports'; import { ccenum } from '../value-types/enum'; -import { IValueProxyFactory } from './value-proxy'; import { assertIsNonNullable, assertIsTrue } from '../data/utils/asserts'; import { debug } from '../platform/debug'; +import { PoseOutput } from './pose-output'; /** * @en The event type supported by Animation @@ -82,81 +78,6 @@ export enum EventType { FINISHED = 'finished', } ccenum(EventType); -export class ICurveInstance { - public commonTargetIndex = -1; - - private _curve: AnimCurve; - private _boundTarget: IBoundTarget; - private _curveDetail: Omit; - private declare _shouldLerp: boolean; - - constructor ( - runtimeCurve: Omit, - target: any, - boundTarget: IBoundTarget, - ) { - this._curve = runtimeCurve.curve; - this._curveDetail = runtimeCurve; - - this._boundTarget = boundTarget; - this._shouldLerp = runtimeCurve.curve.hasLerp(); - } - - public applySample (ratio: number, index: number, inBetween: boolean, samplerResultCache, weight: number) { - let value: any; - if (!this._shouldLerp || !inBetween) { - value = this._curve.valueAt(index); - } else { - value = this._curve.valueBetween( - ratio, - samplerResultCache.from, - samplerResultCache.fromRatio, - samplerResultCache.to, - samplerResultCache.toRatio, - ); - } - this._setValue(value, weight); - } - - private _setValue (value: any, weight: number) { - this._boundTarget.setValue(value); - } - - get propertyName () { return ''; } - - get curveDetail () { - return this._curveDetail; - } -} - -/** - * The curves in ISamplerSharedGroup share a same keys. - */ -interface ISamplerSharedGroup { - sampler: RatioSampler | null; - curves: ICurveInstance[]; - samplerResultCache: { - from: number; - fromRatio: number; - to: number; - toRatio: number; - }; -} - -function makeSamplerSharedGroup (sampler: RatioSampler | null): ISamplerSharedGroup { - return { - sampler, - curves: [], - samplerResultCache: { - from: 0, - fromRatio: 0, - to: 0, - toRatio: 0, - }, - }; -} - -const InvalidIndex = -1; /** * @en @@ -204,8 +125,6 @@ export class AnimationState extends Playable { set wrapMode (value: WrapMode) { this._wrapMode = value; - if (EDITOR && !legacyCC.GAME_VIEW) { return; } - // dynamic change wrapMode will need reset time to 0 this.time = 0; @@ -214,6 +133,8 @@ export class AnimationState extends Playable { } else { this.repeatCount = 1; } + + this._clipEventEval?.setWrapMode(value); } /** @@ -331,7 +252,9 @@ export class AnimationState extends Playable { set weight (value) { this._weight = value; - this._blendStateWriterHost.weight = value; + if (this._poseOutput) { + this._poseOutput.weight = value; + } } public frameRate = 0; @@ -348,16 +271,7 @@ export class AnimationState extends Playable { private _clip: AnimationClip; private _useSimpleProcess = false; - private _samplerSharedGroups: ISamplerSharedGroup[] = []; private _target: Node | null = null; - private _ignoreIndex = InvalidIndex; - /** - * May be `null` due to failed to initialize. - */ - private _commonTargetStatuses: (null | { - target: IBufferedTarget; - changed: boolean; - })[] = []; private _wrapMode = WrapMode.Normal; private _repeatCount = 1; private _delay = 0.0; @@ -367,13 +281,10 @@ export class AnimationState extends Playable { * When set new time to animation state, we should ensure the frame at the specified time being played at next update. */ private _currentFramePlayed = false; - private declare _name: string; + private _name: string; private _lastIterations = NaN; private _lastWrapInfo: WrappedInfo | null = null; - private _lastWrapInfoEvent: WrappedInfo | null = null; private _wrappedInfo = new WrappedInfo(); - private _blendStateBuffer: BlendStateBuffer | null = null; - private _blendStateWriters: BlendStateWriter[] = []; private _allowLastFrame = false; private _blendStateWriterHost = { weight: 0.0, @@ -381,8 +292,14 @@ export class AnimationState extends Playable { private declare _playbackRange: { min: number; max: number; }; private _playbackDuration = 0.0; private _invDuration = 1.0; + private _poseOutput: PoseOutput | null = null; private _weight = 0.0; - private _clipHasEvent = false; + private _clipEval: ReturnType | undefined; + private _clipEventEval: ReturnType | undefined; + /** + * For internal usage. Really hack... + */ + protected _doNotCreateEval = false; constructor (clip: AnimationClip, name = '') { super(); @@ -405,12 +322,17 @@ export class AnimationState extends Playable { return this._curveLoaded; } - public initialize (root: Node, propertyCurves?: readonly IRuntimeCurve[]) { + public initialize (root: Node) { if (this._curveLoaded) { return; } this._curveLoaded = true; - this._destroyBlendStateWriters(); - this._samplerSharedGroups.length = 0; - this._blendStateBuffer = legacyCC.director.getAnimationManager()?.blendState ?? null; + if (this._poseOutput) { + this._poseOutput.destroy(); + this._poseOutput = null; + } + if (this._clipEval) { + // TODO: destroy? + this._clipEval = undefined; + } this._targetNode = root; const clip = this._clip; @@ -423,106 +345,33 @@ export class AnimationState extends Playable { this._playbackRange.max = clip.duration; this._playbackDuration = clip.duration; - this._clipHasEvent = clip.hasEvents(); - if ((this.wrapMode & WrapModeMask.Loop) === WrapModeMask.Loop) { this.repeatCount = Infinity; } else { this.repeatCount = 1; } - /** - * Create the bound target. Especially optimized for skeletal case. - */ - const createBoundTargetOptimized = ( - rootTarget: any, - path: TargetPath[], - valueAdapter: IValueProxyFactory | undefined, - isConstant: boolean, - ): IBoundTarget | null => { - if (!clip.enableTrsBlending || !isTargetingTRS(path) || !this._blendStateBuffer) { - return createBoundTarget(rootTarget, path, valueAdapter); - } else { - const targetNode = evaluatePath(rootTarget, ...path.slice(0, path.length - 1)); - if (targetNode !== null && targetNode instanceof Node) { - const propertyName = path[path.length - 1] as 'position' | 'rotation' | 'scale' | 'eulerAngles'; - const blendStateWriter = this._blendStateBuffer.createWriter( - targetNode, - propertyName, - this._blendStateWriterHost, - isConstant, - ); - this._blendStateWriters.push(blendStateWriter); - return blendStateWriter; - } - } - return null; - }; - - this._commonTargetStatuses = clip.commonTargets.map((commonTarget, index) => { - const boundTarget = createBoundTargetOptimized(root, commonTarget.modifiers, commonTarget.valueAdapter, false); - if (!boundTarget) { - return null; - } - const target = createBufferedTarget(boundTarget); - if (target === null) { - return null; - } else { - return { - target, - changed: false, - }; + if (!this._doNotCreateEval) { + const pose = legacyCC.director.getAnimationManager()?.blendState ?? null; + if (pose) { + this._poseOutput = new PoseOutput(pose); } - }); - - if (!propertyCurves) { - propertyCurves = clip.getPropertyCurves(); + this._clipEval = clip.createEvaluator({ + target: root, + pose: this._poseOutput ?? undefined, + }); } - for (let iPropertyCurve = 0; iPropertyCurve < propertyCurves.length; ++iPropertyCurve) { - const propertyCurve = propertyCurves[iPropertyCurve]; - if (propertyCurve.curve.empty()) { - continue; - } - let samplerSharedGroup = this._samplerSharedGroups.find((value) => value.sampler === propertyCurve.sampler); - if (!samplerSharedGroup) { - samplerSharedGroup = makeSamplerSharedGroup(propertyCurve.sampler); - this._samplerSharedGroups.push(samplerSharedGroup); - } - - let rootTarget: any; - if (typeof propertyCurve.commonTarget === 'undefined') { - rootTarget = root; - } else { - const commonTargetStatus = this._commonTargetStatuses[propertyCurve.commonTarget]; - if (!commonTargetStatus) { - continue; - } - rootTarget = commonTargetStatus.target.peek(); - } - const boundTarget = createBoundTargetOptimized( - rootTarget, - propertyCurve.modifiers, - propertyCurve.valueAdapter, - propertyCurve.curve.constant(), - ); - - if (boundTarget === null) { - // warn(`Failed to bind "${root.name}" to curve in clip ${clip.name}: ${err}`); - } else { - const curveInstance = new ICurveInstance( - propertyCurve, - rootTarget, - boundTarget, - ); - curveInstance.commonTargetIndex = propertyCurve.commonTarget ?? -1; - samplerSharedGroup.curves.push(curveInstance); - } + if (!(EDITOR && !legacyCC.GAME_VIEW)) { + this._clipEventEval = clip.createEventEvaluator(this._targetNode); } } public destroy () { - this._destroyBlendStateWriters(); + if (this._poseOutput) { + this._poseOutput.destroy(); + this._poseOutput = null; + } } /** @@ -595,22 +444,8 @@ export class AnimationState extends Playable { this.time = time || 0.0; if (!EDITOR || legacyCC.GAME_VIEW) { - this._lastWrapInfoEvent = null; - this._ignoreIndex = InvalidIndex; - const info = this.getWrappedInfo(time, this._wrappedInfo); - const direction = info.direction; - let frameIndex = this._clip.getEventGroupIndexAtRatio(info.ratio); - - // only ignore when time not on a frame index - if (frameIndex < 0) { - frameIndex = ~frameIndex - 1; - - // if direction is inverse, then increase index - if (direction < 0) { frameIndex += 1; } - - this._ignoreIndex = frameIndex; - } + this._clipEventEval?.ignore(info.ratio, info.direction); } } @@ -671,68 +506,11 @@ export class AnimationState extends Playable { } protected _sampleCurves (ratio: number) { - const weight = this.weight; - - const commonTargetStatuses = this._commonTargetStatuses; - // Before we sample, we pull values of common targets. - for (let iCommonTarget = 0, length = commonTargetStatuses.length; iCommonTarget < length; ++iCommonTarget) { - const commonTargetStatus = commonTargetStatuses[iCommonTarget]; - if (!commonTargetStatus) { - continue; - } - commonTargetStatus.target.pull(); - commonTargetStatus.changed = false; + if (this._poseOutput) { + this._poseOutput.weight = this.weight; } - - const samplerSharedGroups = this._samplerSharedGroups; - samplerSharedGroups.forEach((samplerSharedGroup) => { - const { sampler, samplerResultCache } = samplerSharedGroup; - let index = 0; - let lerpRequired = false; - if (!sampler) { - index = 0; - } else { - index = sampler.sample(ratio); - if (index < 0) { - index = ~index; - if (index <= 0) { - index = 0; - } else if (index >= sampler.ratios.length) { - index = sampler.ratios.length - 1; - } else { - lerpRequired = true; - samplerResultCache.from = index - 1; - samplerResultCache.fromRatio = sampler.ratios[samplerResultCache.from]; - samplerResultCache.to = index; - samplerResultCache.toRatio = sampler.ratios[samplerResultCache.to]; - index = samplerResultCache.from; - } - } - } - - const curves = samplerSharedGroup.curves; - for (let iCurveInstance = 0, szCurves = curves.length; - iCurveInstance < szCurves; ++iCurveInstance) { - const curveInstance = curves[iCurveInstance]; - curveInstance.applySample(ratio, index, lerpRequired, samplerResultCache, weight); - if (curveInstance.commonTargetIndex >= 0) { - const commonTargetStatus = commonTargetStatuses[curveInstance.commonTargetIndex]; - if (commonTargetStatus) { - commonTargetStatus.changed = true; - } - } - } - }); - - // After sample, we push values of common targets. - for (let iCommonTarget = 0, length = commonTargetStatuses.length; iCommonTarget < length; ++iCommonTarget) { - const commonTargetStatus = commonTargetStatuses[iCommonTarget]; - if (!commonTargetStatus) { - continue; - } - if (commonTargetStatus.changed) { - commonTargetStatus.target.push(); - } + if (this._clipEval) { + this._clipEval.evaluate(this.current); } } @@ -779,9 +557,7 @@ export class AnimationState extends Playable { this._sampleCurves(ratio); if (!EDITOR || legacyCC.GAME_VIEW) { - if (this._clipHasEvent) { - this._sampleEvents(this.getWrappedInfo(this.time, this._wrappedInfo)); - } + this._sampleEvents(this.getWrappedInfo(this.time, this._wrappedInfo)); } if (this._allowLastFrame) { @@ -797,9 +573,6 @@ export class AnimationState extends Playable { } } - private cache (frames: number) { - } - private _needReverse (currentIterations: number) { const wrapMode = this.wrapMode; let needReverse = false; @@ -885,77 +658,11 @@ export class AnimationState extends Playable { } private _sampleEvents (wrapInfo: WrappedInfo) { - const length = this._clip.eventGroups.length; - let direction = wrapInfo.direction; - let eventIndex = this._clip.getEventGroupIndexAtRatio(wrapInfo.ratio); - if (eventIndex < 0) { - eventIndex = ~eventIndex - 1; - // If direction is inverse, increase index. - if (direction < 0) { - eventIndex += 1; - } - } - - if (this._ignoreIndex !== eventIndex) { - this._ignoreIndex = InvalidIndex; - } - - wrapInfo.frameIndex = eventIndex; - - if (!this._lastWrapInfoEvent) { - this._fireEvent(eventIndex); - this._lastWrapInfoEvent = new WrappedInfo(wrapInfo); - return; - } - - const wrapMode = this.wrapMode; - const currentIterations = wrapIterations(wrapInfo.iterations); - - const lastWrappedInfo = this._lastWrapInfoEvent; - let lastIterations = wrapIterations(lastWrappedInfo.iterations); - let lastIndex = lastWrappedInfo.frameIndex; - const lastDirection = lastWrappedInfo.direction; - - const iterationsChanged = lastIterations !== -1 && currentIterations !== lastIterations; - - if (lastIndex === eventIndex && iterationsChanged && length === 1) { - this._fireEvent(0); - } else if (lastIndex !== eventIndex || iterationsChanged) { - direction = lastDirection; - - do { - if (lastIndex !== eventIndex) { - if (direction === -1 && lastIndex === 0 && eventIndex > 0) { - if ((wrapMode & WrapModeMask.PingPong) === WrapModeMask.PingPong) { - direction *= -1; - } else { - lastIndex = length; - } - lastIterations++; - } else if (direction === 1 && lastIndex === length - 1 && eventIndex < length - 1) { - if ((wrapMode & WrapModeMask.PingPong) === WrapModeMask.PingPong) { - direction *= -1; - } else { - lastIndex = -1; - } - lastIterations++; - } - - if (lastIndex === eventIndex) { - break; - } - if (lastIterations > currentIterations) { - break; - } - } - - lastIndex += direction; - - legacyCC.director.getAnimationManager().pushDelayEvent(this._fireEvent, this, [lastIndex]); - } while (lastIndex !== eventIndex && lastIndex > -1 && lastIndex < length); - } - - this._lastWrapInfoEvent.set(wrapInfo); + this._clipEventEval?.sample( + wrapInfo.ratio, + wrapInfo.direction, + wrapInfo.iterations, + ); } private _emit (type, state) { @@ -964,29 +671,6 @@ export class AnimationState extends Playable { } } - private _fireEvent (index: number) { - if (!this._targetNode || !this._targetNode.isValid) { - return; - } - - const { eventGroups } = this._clip; - if (index < 0 || index >= eventGroups.length || this._ignoreIndex === index) { - return; - } - - const eventGroup = eventGroups[index]; - const components = this._targetNode.components; - for (const event of eventGroup.events) { - const { functionName } = event; - for (const component of components) { - const fx = component[functionName]; - if (typeof fx === 'function') { - fx.apply(component, event.parameters); - } - } - } - } - private _onReplayOrResume () { legacyCC.director.getAnimationManager().addAnimation(this); } @@ -994,49 +678,6 @@ export class AnimationState extends Playable { private _onPauseOrStop () { legacyCC.director.getAnimationManager().removeAnimation(this); } - - private _destroyBlendStateWriters () { - if (this._blendStateWriters.length) { - assertIsNonNullable(this._blendStateBuffer); - } - for (let iBlendStateWriter = 0; iBlendStateWriter < this._blendStateWriters.length; ++iBlendStateWriter) { - this._blendStateBuffer!.destroyWriter(this._blendStateWriters[iBlendStateWriter]); - } - this._blendStateWriters.length = 0; - if (this._blendStateBuffer) { - this._blendStateBuffer = null; - } - } -} - -function isTargetingTRS (path: TargetPath[]) { - let prs: string | undefined; - if (path.length === 1 && typeof path[0] === 'string') { - prs = path[0]; - } else if (path.length > 1) { - for (let i = 0; i < path.length - 1; ++i) { - if (!(path[i] instanceof HierarchyPath)) { - return false; - } - } - prs = path[path.length - 1] as string; - } - switch (prs) { - case 'position': - case 'scale': - case 'rotation': - case 'eulerAngles': - return true; - default: - return false; - } -} - -function wrapIterations (iterations: number) { - if (iterations - (iterations | 0) === 0) { - iterations -= 1; - } - return iterations | 0; } legacyCC.AnimationState = AnimationState; diff --git a/cocos/core/animation/animation.ts b/cocos/core/animation/animation.ts index 1f5ef6df239..9887a9192bb 100644 --- a/cocos/core/animation/animation.ts +++ b/cocos/core/animation/animation.ts @@ -31,5 +31,14 @@ export * from './target-path'; export * from './value-proxy'; export { UniformProxyFactory } from './value-proxy-factories/uniform'; -export { MorphWeightsValueProxy, MorphWeightsAllValueProxy } from './value-proxy-factories/morph-weights'; +export { MorphWeightValueProxy, MorphWeightsValueProxy, MorphWeightsAllValueProxy } from './value-proxy-factories/morph-weights'; export * from './cubic-spline-value'; +export { + Track, + RealTrack, + IntegerTrack, + VectorTrack, + QuaternionTrack, + ColorTrack, + ObjectTrack, +} from './animation-clip'; diff --git a/cocos/core/animation/compression.ts b/cocos/core/animation/compression.ts new file mode 100644 index 00000000000..7c04ce4b9a9 --- /dev/null +++ b/cocos/core/animation/compression.ts @@ -0,0 +1,96 @@ +import { approx } from '../../core/math/utils'; + +/** + * Removes keys which are linear interpolations of surrounding keys. + * @param keys Input keys. + * @param values Input values. + * @param maxDiff Max error. + * @returns The new keys `keys` and new values `values`. + */ +export function removeLinearKeys (keys: number[], values: number[], maxDiff = 1e-3) { + const nKeys = keys.length; + + if (nKeys < 3) { + return { + keys: keys.slice(), + values: values.slice(), + }; + } + + const removeFlags = new Array(nKeys).fill(false); + // We may choose to use different key selection policy? + // http://nfrechette.github.io/2016/12/07/anim_compression_key_reduction/ + const iLastKey = nKeys - 1; + for (let iKey = 1; iKey< iLastKey; ++iKey) { + // Should we select previous non-removed key? + const iPrevious = iKey - 1; + const iNext = iKey + 1; + const { [iPrevious]: previousKey, [iKey]: currentKey, [iNext]: nextKey } = keys; + const { [iPrevious]: previousValue, [iKey]: currentValue, [iNext]: nextValue } = values; + const alpha = (currentKey - previousKey) / (nextKey - previousKey); + const expectedValue = (nextValue - previousValue) * alpha + previousValue; + if (approx(expectedValue, currentValue, maxDiff)) { + removeFlags[iKey] = true; + } + } + + return filterFromRemoveFlags(keys, values, removeFlags); +} + +/** + * Removes trivial frames. + * @param keys Input keys. + * @param values Input values. + * @param maxDiff Max error. + * @returns The new keys `keys` and new values `values`. + */ +export function removeTrivialKeys (keys: number[], values: number[], maxDiff = 1e-3) { + const nKeys = keys.length; + + if (nKeys < 2) { + return { + keys: keys.slice(), + values: values.slice(), + }; + } + + const removeFlags = new Array(nKeys).fill(false); + for (let iKey = 1; iKey< nKeys; ++iKey) { + // Should we select previous non-removed key? + const iPrevious = iKey - 1; + const { [iPrevious]: previousValue, [iKey]: currentValue } = values; + if (approx(previousValue, currentValue, maxDiff)) { + removeFlags[iKey] = true; + } + } + + return filterFromRemoveFlags(keys, values, removeFlags); +} + +function filterFromRemoveFlags (keys: number[], values: number[], removeFlags: boolean[]) { + const nKeys = keys.length; + + const nRemovals = removeFlags.reduce((n, removeFlag) => removeFlag ? n + 1 : n, 0); + if (!nRemovals) { + return { + keys: keys.slice(), + values: values.slice(), + }; + } + + const nNewKeyframes = nKeys - nRemovals; + const newKeys = new Array(nNewKeyframes).fill(0.0); + const newValues = new Array(nNewKeyframes).fill(0.0); + for (let iNewKeys = 0, iKey = 0; iKey < nKeys; ++iKey) { + if (!removeFlags[iKey]) { + newKeys[iNewKeys] = keys[iKey]; + newValues[iNewKeys] = values[iKey]; + ++iNewKeys; + } + } + + return { + keys: newKeys, + values: newValues, + }; +} \ No newline at end of file diff --git a/cocos/core/animation/cubic-spline-value.ts b/cocos/core/animation/cubic-spline-value.ts index bb1fac42dba..6733e436cfd 100644 --- a/cocos/core/animation/cubic-spline-value.ts +++ b/cocos/core/animation/cubic-spline-value.ts @@ -112,16 +112,20 @@ function makeCubicSplineValueConstructor ( export const CubicSplineVec2Value = makeCubicSplineValueConstructor( 'cc.CubicSplineVec2Value', Vec2, Vec2.multiplyScalar, Vec2.scaleAndAdd, ); + +export type CubicSplineVec2Value = ICubicSplineValue; legacyCC.CubicSplineVec2Value = CubicSplineVec2Value; export const CubicSplineVec3Value = makeCubicSplineValueConstructor( 'cc.CubicSplineVec3Value', Vec3, Vec3.multiplyScalar, Vec3.scaleAndAdd, ); +export type CubicSplineVec3Value = ICubicSplineValue; legacyCC.CubicSplineVec3Value = CubicSplineVec3Value; export const CubicSplineVec4Value = makeCubicSplineValueConstructor( 'cc.CubicSplineVec4Value', Vec4, Vec4.multiplyScalar, Vec4.scaleAndAdd, ); +export type CubicSplineVec4Value = ICubicSplineValue; legacyCC.CubicSplineVec4Value = CubicSplineVec4Value; export const CubicSplineQuatValue = makeCubicSplineValueConstructor( diff --git a/cocos/core/animation/index.ts b/cocos/core/animation/index.ts index 8549766fd4d..fc83b6b815f 100644 --- a/cocos/core/animation/index.ts +++ b/cocos/core/animation/index.ts @@ -41,7 +41,7 @@ legacyCC.easing = easing; export * from './bezier'; export { easing }; export * from './animation-curve'; -export * from './animation-clip'; +export { AnimationClip } from './animation-clip'; export * from './animation-manager'; export { AnimationState, diff --git a/cocos/core/animation/internal-symbols.ts b/cocos/core/animation/internal-symbols.ts new file mode 100644 index 00000000000..37aa2847753 --- /dev/null +++ b/cocos/core/animation/internal-symbols.ts @@ -0,0 +1,2 @@ + +export const BAKE_SKELETON_CURVE_SYMBOL = Symbol('BakeNodeCurves'); diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts new file mode 100644 index 00000000000..abcff042f33 --- /dev/null +++ b/cocos/core/animation/legacy-clip-data.ts @@ -0,0 +1,373 @@ +import { TargetPath } from './target-path'; +import { IValueProxyFactory } from './value-proxy'; +import * as easing from './easing'; +import { BezierControlPoints } from './bezier'; +import { CompactValueTypeArray } from '../data/utils/compact-value-type-array'; +import { serializable } from '../data/decorators'; +import { AnimCurve, RatioSampler } from './animation-curve'; +import { RealCurve, RealInterpMode, TangentWeightMode } from '../curves'; +import { assertIsTrue } from '../data/utils/asserts'; + +/** + * 表示曲线值,曲线值可以是任意类型,但必须符合插值方式的要求。 + */ +export type LegacyCurveValue = any; + +/** + * 表示曲线的目标对象。 + */ +export type LegacyCurveTarget = Record; + +/** + * 内置帧时间渐变方式名称。 + */ +export type LegacyEasingMethodName = keyof (typeof easing); + +/** + * 帧时间渐变方式。可能为内置帧时间渐变方式的名称或贝塞尔控制点。 + */ +export type LegacyEasingMethod = LegacyEasingMethodName | BezierControlPoints; + +export type LegacyCompressedEasingMethods = Record; + +export type LegacyLerpFunction = (from: T, to: T, t: number, dt: number) => T; + +export interface LegacyClipCurveData { + /** + * 曲线使用的时间轴。 + * @see {AnimationClip.keys} + */ + keys: number; + + /** + * 曲线值。曲线值的数量应和 `keys` 所引用时间轴的帧数相同。 + */ + values: LegacyCurveValue[]; + + /** + * 曲线任意两帧时间的渐变方式。仅当 `easingMethods === undefined` 时本字段才生效。 + */ + easingMethod?: LegacyEasingMethod; + + /** + * 描述了每一帧时间到下一帧时间之间的渐变方式。 + */ + easingMethods?: LegacyEasingMethod[] | LegacyCompressedEasingMethods; + + /** + * 是否进行插值。 + * @default true + */ + interpolate?: boolean; + + /** + * For internal usage only. + */ + _arrayLength?: number; +} + +export interface LegacyClipCurve { + commonTarget?: number; + + modifiers: TargetPath[]; + + valueAdapter?: IValueProxyFactory; + + data: LegacyClipCurveData; +} + +export interface LegacyCommonTarget { + modifiers: TargetPath[]; + valueAdapter?: IValueProxyFactory; +} + +export type LegacyMaybeCompactCurve = Omit & { + data: Omit & { + values: any[] | CompactValueTypeArray; + }; +}; + +export type LegacyMaybeCompactKeys = Array; + +export type LegacyRuntimeCurve = Pick & { + /** + * 属性曲线。 + */ + curve: AnimCurve; + + /** + * 曲线采样器。 + */ + sampler: RatioSampler | null; +} + +export class AnimationClipLegacyData { + constructor (duration: number) { + this._duration = duration; + } + + get keys () { + return this._keys; + } + + set keys (value) { + this._keys = value; + } + + get curves () { + return this._curves; + } + + set curves (value) { + this._curves = value; + delete this._runtimeCurves; + } + + get commonTargets () { + return this._commonTargets; + } + + set commonTargets (value) { + this._commonTargets = value; + } + + /** + * 此动画的数据。 + */ + get data () { + return this._data; + } + + public getPropertyCurves (): readonly LegacyRuntimeCurve[] { + if (!this._runtimeCurves) { + this._createPropertyCurves(); + } + return this._runtimeCurves!; + } + + @serializable + private _keys: number[][] = []; + + @serializable + private _curves: LegacyClipCurve[] = []; + + @serializable + private _commonTargets: LegacyCommonTarget[] = []; + + private _ratioSamplers: RatioSampler[] = []; + + private _runtimeCurves?: LegacyRuntimeCurve[]; + + private _data: Uint8Array | null = null; + + private _duration: number; + + protected _createPropertyCurves () { + this._ratioSamplers = this._keys.map( + (keys) => new RatioSampler( + keys.map( + (key) => key / this._duration, + ), + ), + ); + + this._runtimeCurves = this._curves.map((targetCurve): LegacyRuntimeCurve => ({ + curve: new AnimCurve(targetCurve.data, this._duration), + modifiers: targetCurve.modifiers, + valueAdapter: targetCurve.valueAdapter, + sampler: this._ratioSamplers[targetCurve.data.keys], + commonTarget: targetCurve.commonTarget, + })); + } + + // private _decodeCVTAs () { + // const binaryBuffer: ArrayBuffer = ArrayBuffer.isView(this._nativeAsset) ? this._nativeAsset.buffer : this._nativeAsset; + // if (!binaryBuffer) { + // return; + // } + + // const maybeCompressedKeys = this._keys; + // for (let iKey = 0; iKey < maybeCompressedKeys.length; ++iKey) { + // const keys = maybeCompressedKeys[iKey]; + // if (keys instanceof CompactValueTypeArray) { + // maybeCompressedKeys[iKey] = keys.decompress(binaryBuffer); + // } + // } + + // for (let iCurve = 0; iCurve < this._curves.length; ++iCurve) { + // const curve = this._curves[iCurve]; + // if (curve.data.values instanceof CompactValueTypeArray) { + // curve.data.values = curve.data.values.decompress(binaryBuffer); + // } + // } + // } +} + +// #region Legacy data structures prior to 1.2 + +export interface LegacyObjectCurveData { + [propertyName: string]: LegacyClipCurveData; +} + +export interface LegacyComponentsCurveData { + [componentName: string]: LegacyObjectCurveData; +} + +export interface LegacyNodeCurveData { + props?: LegacyObjectCurveData; + comps?: LegacyComponentsCurveData; +} + +// #endregion + +export class LegacyEasingMethodConverter { + constructor (legacyCurveData: LegacyClipCurveData, keyframesCount: number) { + const { easingMethods } = legacyCurveData; + if (Array.isArray(easingMethods)) { + // Different + this._easingMethods = easingMethods; + } else if (easingMethods === undefined) { + // Same + this._easingMethods = new Array(keyframesCount).fill(legacyCurveData.easingMethod); + } else { + // Compressed as record + this._easingMethods = Array.from({ length: keyframesCount }, (_, index) => easingMethods[index]); + } + } + + get nil () { + return !this._easingMethods; + } + + public convert (curve: RealCurve) { + const { _easingMethods: easingMethods } = this; + if (!easingMethods) { + return; + } + + const nKeyframes = curve.keyFramesCount; + if (curve.keyFramesCount < 2) { + return; + } + + if (Array.isArray(easingMethods)) { + assertIsTrue(nKeyframes === easingMethods.length); + } + + const iLastKeyframe = nKeyframes - 1; + for (let iKeyframe = 0; iKeyframe < iLastKeyframe; ++iKeyframe) { + const easingMethod = easingMethods[iKeyframe]; + if (!easingMethod) { + continue; + } + if (Array.isArray(easingMethod)) { + // Time bezier points + const previousKeyframeValue = curve.getKeyframeValue(iKeyframe); + const nextKeyframeValue = curve.getKeyframeValue(iKeyframe + 1); + const [endTangent, endTangentWeight, startTangent, startTangentWeight] = timeBezierToTangents( + easingMethod, + curve.getKeyframeTime(iKeyframe), + previousKeyframeValue.value, + curve.getKeyframeTime(iKeyframe + 1), + nextKeyframeValue.value, + ); + previousKeyframeValue.interpMode = RealInterpMode.CUBIC; + previousKeyframeValue.endTangent = endTangent; + previousKeyframeValue.endTangentWeight = endTangentWeight; + nextKeyframeValue.startTangent = startTangent; + nextKeyframeValue.startTangentWeight = startTangentWeight; + nextKeyframeValue.tangentWeightMode = TangentWeightMode.BOTH; + } else { + const bernstein = new Array(4).fill(0); + // Easing methods in `easing` + switch (easingMethod) { + case 'constant': + case 'linear': + break; + case 'quadIn': + // k * k + powerToBernstein([0.0, 0.0, 1.0, 0.0], bernstein); + break; + case 'quadOut': + // k * (2 - k) + powerToBernstein([0.0, 2.0, -1.0, 0.0], bernstein); + break; + case 'cubicIn': + // k * k * k + powerToBernstein([0.0, 0.0, 0.0, 1.0], bernstein); + break; + case 'cubicOut': + // --k * k * k + 1; + powerToBernstein([0.0, 0.0, 0.0, 1.0], bernstein); + break; + case 'backIn': { + // k * k * ((s + 1) * k - s) + const s = 1.70158; + powerToBernstein([1.0, 0.0, -s, s + 1.0], bernstein); + break; + } + case 'backOut': { + // k * k * ((s + 1) * k - s) + const s = 1.70158; + powerToBernstein([1.0, 0.0, s, s + 1.0], bernstein); + break; + } + case 'smooth': { + // k * k * (3 - 2 * k) + powerToBernstein([0.0, 0.0, 3.0, -2.0], bernstein); + break; + } + default: + // TODO: do sample + assertIsTrue(false); + } + } + } + } + + private _easingMethods: LegacyEasingMethod[] | undefined; +} + +/** + * Legacy curve uses time based bezier curve interpolation. + * That's, interpolate time 'x'(time ratio between two frames, eg.[0, 1]) + * and then use the interpolated time to sample curve. + * Now we need to compute the the end tangent of previous frame and the start tangent of the next frame. + * @param timeBezierPoints Bezier points used for legacy time interpolation. + * @param previousTime Time of the previous keyframe. + * @param previousValue Value of the previous keyframe. + * @param nextTime Time of the next keyframe. + * @param nextValue Value of the next keyframe. + */ +export function timeBezierToTangents ( + timeBezierPoints: BezierControlPoints, + previousTime: number, + previousValue: number, + nextTime: number, + nextValue: number, +): [previousTangent: number, previousTangentWeight: number, nextTangent: number, nextTangentWeight: number] { + const [p1X, p1Y, p2X, p2Y] = timeBezierPoints; + const dValue = nextValue - previousValue; + const dTime = nextTime - previousTime; + const fx = 3 * dTime; + const fy = 3 * dValue; + const t1x = p1X * fx; + const t1y = p1Y * fy; + const t2x = (1.0 - p2X) * fx; + const t2y = (1.0 - p2Y) * fy; + const ONE_THIRD = 1.0 / 3.0; + return [ + t1y / t1x, + Math.sqrt(t1x * t1x + t1y * t1y) * ONE_THIRD, + t2y / t2x, + Math.sqrt(t2x * t2x + t2y * t2y) * ONE_THIRD, + ]; +} + +function powerToBernstein ([p0, p1, p2, p3]: [number, number, number, number], bernstein: number[]) { + // https://stackoverflow.com/questions/33859199/convert-polynomial-curve-to-bezier-curve-control-points + bernstein[0] = p0 + p1 + p2 + p3; + bernstein[1] = p1 / 3.0 + p2 * 2.0 / 3.0 + p3; + bernstein[2] = p2 / 3.0 + p3; + bernstein[3] = p3; +} diff --git a/cocos/core/animation/pose-output.ts b/cocos/core/animation/pose-output.ts new file mode 100644 index 00000000000..1b2dc5a80a8 --- /dev/null +++ b/cocos/core/animation/pose-output.ts @@ -0,0 +1,29 @@ +import { BlendStateBuffer, BlendingProperty, BlendStateWriter } from '../../3d/skeletal-animation/skeletal-animation-blending'; +import type { Node } from '../scene-graph'; + +export type Pose = BlendStateBuffer; + +export class PoseOutput { + public weight = 0.0; + + constructor (pose: Pose) { + this._pose = pose; + } + + public destroy () { + for (let iBlendStateWriter = 0; iBlendStateWriter < this._blendStateWriters.length; ++iBlendStateWriter) { + this._pose.destroyWriter(this._blendStateWriters[iBlendStateWriter]); + } + this._blendStateWriters.length = 0; + } + + public createPoseWriter (node: Node, property: BlendingProperty, constants: boolean) { + const writer = this._pose.createWriter(node, property, this, constants); + this._blendStateWriters.push(writer); + return writer; + } + + private _pose: Pose; + + private _blendStateWriters: BlendStateWriter[] = []; +} diff --git a/cocos/core/curves/curve-base.ts b/cocos/core/curves/curve-base.ts new file mode 100644 index 00000000000..841b4fefb4d --- /dev/null +++ b/cocos/core/curves/curve-base.ts @@ -0,0 +1,4 @@ +export interface CurveBase { + readonly rangeMin: number; + readonly rangeMax: number; +} diff --git a/cocos/core/curves/curve.ts b/cocos/core/curves/curve.ts new file mode 100644 index 00000000000..1b245c474ea --- /dev/null +++ b/cocos/core/curves/curve.ts @@ -0,0 +1,585 @@ +import { assertIsTrue } from '../data/utils/asserts'; +import { approx, lerp, pingPong, repeat } from '../math'; +import { KeyframeCurve } from './keyframe-curve'; +import { ccclass, serializable } from '../data/decorators'; +import { deserializeSymbol, serializeSymbol } from '../data/serialization-symbols'; +import { RealInterpMode, ExtrapMode, TangentWeightMode } from './real-curve-param'; +import { binarySearchEpsilon } from '../algorithm/binary-search'; +import { solveCubic } from './solve-cubic'; +import { EditorExtendableMixin } from '../data/editor-extendable'; + +export { RealInterpMode, ExtrapMode, TangentWeightMode }; + +@ccclass('cc.RealKeyframeValue') +export class RealKeyframeValue { + constructor ({ + interpMode, + tangentWeightMode, + value, + startTangent, + startTangentWeight, + endTangent, + endTangentWeight, + }: Partial = { }) { + this.value = value ?? this.value; + this.startTangent = startTangent ?? this.startTangent; + this.startTangentWeight = startTangentWeight ?? this.startTangentWeight; + this.endTangent = endTangent ?? this.endTangent; + this.endTangentWeight = endTangentWeight ?? this.endTangentWeight; + this.interpMode = interpMode ?? this.interpMode; + this.tangentWeightMode = tangentWeightMode ?? this.tangentWeightMode; + } + + /** + * Interpolation method used for this keyframe. + */ + @serializable + public interpMode = RealInterpMode.LINEAR; + + /** + * Tangent weight mode. + */ + @serializable + public tangentWeightMode = TangentWeightMode.NONE; + + /** + * Value of the keyframe. + */ + @serializable + public value = 0.0; + + /** + * The (y component of) tangent of this keyframe + * when it's used as starting point during cubic interpolation. + * Meaningless otherwise. + */ + @serializable + public startTangent = 0.0; + + /** + * The x component tangent of this keyframe + * when it's used as starting point during cubic interpolation. + * Meaningless otherwise. + */ + @serializable + public startTangentWeight = 0.0; + + /** + * The (y component of) tangent of this keyframe + * when it's used as ending point during cubic interpolation. + * Meaningless otherwise. + */ + @serializable + public endTangent = 0.0; + + /** + * The x component of tangent of this keyframe + * when it's used as starting point during cubic interpolation. + * Meaningless otherwise. + */ + @serializable + public endTangentWeight = 0.0; +} + +/** + * Curve. + */ +@ccclass('cc.RealCurve') +export class RealCurve extends EditorExtendableMixin>(KeyframeCurve) { + /** + * Gets or sets the operation should be taken + * if input time is less than the time of first keyframe when evaluating this curve. + * Defaults to `ExtrapMode.CLAMP`. + */ + get preExtrap () { + return this._preExtrap; + } + + set preExtrap (value) { + this._preExtrap = value; + } + + /** + * Gets or sets the operation should be taken + * if input time is greater than the time of last keyframe when evaluating this curve. + * Defaults to `ExtrapMode.CLAMP`. + */ + get postExtrap () { + return this._postExtrap; + } + + set postExtrap (value) { + this._postExtrap = value; + } + + /** + * Evaluates this curve at specified time. + * @param time Input time. + * @returns Result value. + */ + public evaluate (time: number): number { + const { + _times: times, + _values: values, + } = this; + + const nFrames = times.length; + + if (nFrames === 0) { + return 0.0; + } + + const firstTime = times[0]; + const lastTime = times[nFrames - 1]; + if (time < firstTime) { + // Underflow + const { _preExtrap: preExtrap } = this; + const preValue = values[0]; + if (preExtrap === ExtrapMode.CLAMP || nFrames < 2) { + return preValue.value; + } + switch (preExtrap) { + case ExtrapMode.LINEAR: + return linearTrend(firstTime, values[0].value, times[1], values[1].value, time); + case ExtrapMode.REPEAT: + time = wrapRepeat(time, firstTime, lastTime); + break; + case ExtrapMode.PING_PONG: + time = wrapPingPong(time, firstTime, lastTime); + break; + default: + return preValue.value; + } + } else if (time > lastTime) { + // Overflow + const { _postExtrap: postExtrap } = this; + const preFrame = values[nFrames - 1]; + if (postExtrap === ExtrapMode.CLAMP || nFrames < 2) { + return preFrame.value; + } + switch (postExtrap) { + case ExtrapMode.LINEAR: + return linearTrend(lastTime, preFrame.value, times[nFrames - 2], values[nFrames - 2].value, time); + case ExtrapMode.REPEAT: + time = wrapRepeat(time, firstTime, lastTime); + break; + case ExtrapMode.PING_PONG: + time = wrapPingPong(time, firstTime, lastTime); + break; + default: + return preFrame.value; + } + } + + const index = binarySearchEpsilon(times, time); + if (index >= 0) { + return values[index].value; + } + + const iNext = ~index; + assertIsTrue(iNext !== 0 && iNext !== nFrames && nFrames > 1); + + const iPre = iNext - 1; + const preTime = times[iPre]; + const preValue = values[iPre]; + const nextTime = times[iNext]; + const nextValue = values[iNext]; + assertIsTrue(nextTime > time && time > preTime); + const dt = nextTime - preTime; + + const ratio = (time - preTime) / dt; + return evalBetweenTwoKeyFrames(preTime, preValue, nextTime, nextValue, ratio); + } + + /** + * Adds a keyframe into this curve. + * @param time Time of the keyframe. + * @param value Value of the keyframe. + * @returns The index to the new keyframe. + */ + public addKeyFrame (time: number, value: number | RealKeyframeValue): number { + const keyframeValue = typeof value === 'number' ? new RealKeyframeValue({ value }) : value; + return super.addKeyFrame(time, keyframeValue); + } + + /** + * Returns if this curve is constant. + * @param tolerance The tolerance. + * @returns Whether it is constant. + */ + public isConstant (tolerance: number) { + if (this._values.length <= 1) { + return true; + } + const firstVal = this._values[0].value; + return this._values.every((frame) => approx(frame.value, firstVal, tolerance)); + } + + public [serializeSymbol] () { + const { + _times: times, + _values: keyframeValues, + } = this; + + const nKeyframes = times.length; + + const dataSize = 0 + + OVERFLOW_BYTES + OVERFLOW_BYTES + + FRAME_COUNT_BYTES + + TIME_BYTES * nKeyframes + + REAL_KEY_FRAME_VALUE_MAX_SIZE * nKeyframes; + + const dataView = new DataView(new ArrayBuffer(dataSize)); + let currentOffset = 0; + + // Overflow operations + dataView.setUint8(currentOffset, this._preExtrap); currentOffset += OVERFLOW_BYTES; + dataView.setUint8(currentOffset, this._postExtrap); currentOffset += OVERFLOW_BYTES; + + // Frame count + dataView.setUint32(currentOffset, nKeyframes, true); currentOffset += FRAME_COUNT_BYTES; + + // Times + times.forEach((time, index) => dataView.setFloat32(currentOffset + TIME_BYTES * index, time, true)); + currentOffset += TIME_BYTES * nKeyframes; + + // Frame values + for (const keyframeValue of keyframeValues) { + currentOffset = saveRealKeyFrameValue(dataView, keyframeValue, currentOffset); + } + + return new Uint8Array(dataView.buffer, 0, currentOffset); + } + + public [deserializeSymbol] (serialized: ReturnType) { + const dataView = new DataView(serialized.buffer, serialized.byteOffset, serialized.byteLength); + let currentOffset = 0; + + // Overflow operations + this._preExtrap = dataView.getUint8(currentOffset); currentOffset += OVERFLOW_BYTES; + this._postExtrap = dataView.getUint8(currentOffset); currentOffset += OVERFLOW_BYTES; + + // Frame count + const nKeyframes = dataView.getUint32(currentOffset, true); currentOffset += FRAME_COUNT_BYTES; + + // Times + const times = Array.from({ length: nKeyframes }, + (_, index) => dataView.getFloat32(currentOffset + TIME_BYTES * index, true)); + currentOffset += TIME_BYTES * nKeyframes; + + // Frame values + const keyframeValues = new Array(nKeyframes); + for (let iKeyFrame = 0; iKeyFrame < nKeyframes; ++iKeyFrame) { + const keyframeValue = new RealKeyframeValue({}); + currentOffset = loadRealKeyFrameValue(dataView, keyframeValue, currentOffset); + keyframeValues[iKeyFrame] = keyframeValue; + } + + assertIsTrue(currentOffset === serialized.byteLength); + + this._times = times; + this._values = keyframeValues; + } + + // Always sorted by time + @serializable + private _preExtrap: ExtrapMode = ExtrapMode.CLAMP; + + @serializable + private _postExtrap: ExtrapMode = ExtrapMode.CLAMP; +} + +enum KeyframeValueFlagMask { + VALUE = 1 << 0, + INTERP_MODE = 1 << 1, + TANGENT_WEIGHT_MODE = 1 << 2, + START_TANGENT = 1 << 3, + START_TANGENT_WEIGHT = 1 << 4, + END_TANGENT = 1 << 5, + END_TANGENT_WEIGHT = 1 << 6, +} + +const OVERFLOW_BYTES = 1; +const FRAME_COUNT_BYTES = 4; +const TIME_BYTES = 4; +const KEY_FRAME_VALUE_FLAGS_BYTES = 4; +const VALUE_BYTES = 4; +const INTERP_MODE_BYTES = 1; +const TANGENT_WEIGHT_MODE_BYTES = 1; +const START_TANGENT_BYTES = 4; +const START_TANGENT_WEIGHT_BYTES = 4; +const END_TANGENT_BYTES = 4; +const END_TANGENT_WEIGHT_BYTES = 4; + +const { + interpMode: DEFAULT_INTERP_MODE, + tangentWeightMode: DEFAULT_TANGENT_WEIGHT_MODE, + startTangent: DEFAULT_START_TANGENT, + startTangentWeight: DEFAULT_START_TANGENT_WEIGHT, + endTangent: DEFAULT_END_TANGENT, + endTangentWeight: DEFAULT_END_TANGENT_WEIGHT, +} = new RealKeyframeValue({}); + +const REAL_KEY_FRAME_VALUE_MAX_SIZE = KEY_FRAME_VALUE_FLAGS_BYTES + + VALUE_BYTES + + INTERP_MODE_BYTES + + INTERP_MODE_BYTES + + TANGENT_WEIGHT_MODE_BYTES + + START_TANGENT_WEIGHT_BYTES + + END_TANGENT_BYTES + + END_TANGENT_WEIGHT_BYTES + + 0; + +function saveRealKeyFrameValue (dataView: DataView, keyframeValue: RealKeyframeValue, offset: number) { + let flags = 0; + + let currentOffset = offset; + + const pFlags = currentOffset; // Place holder for flags + currentOffset += KEY_FRAME_VALUE_FLAGS_BYTES; + + const { + value, + interpMode, + tangentWeightMode, + startTangent, + startTangentWeight, + endTangent, + endTangentWeight, + } = keyframeValue; + + dataView.setFloat32(currentOffset, value, true); + currentOffset += VALUE_BYTES; + + if (interpMode !== DEFAULT_INTERP_MODE) { + flags |= KeyframeValueFlagMask.INTERP_MODE; + dataView.setUint8(currentOffset, interpMode); + currentOffset += INTERP_MODE_BYTES; + } + + if (tangentWeightMode !== DEFAULT_TANGENT_WEIGHT_MODE) { + flags |= KeyframeValueFlagMask.TANGENT_WEIGHT_MODE; + dataView.setUint8(currentOffset, tangentWeightMode); + currentOffset += TANGENT_WEIGHT_MODE_BYTES; + } + + if (startTangent !== DEFAULT_START_TANGENT) { + flags |= KeyframeValueFlagMask.START_TANGENT; + dataView.setFloat32(currentOffset, startTangent, true); + currentOffset += START_TANGENT_BYTES; + } + + if (startTangentWeight !== DEFAULT_START_TANGENT_WEIGHT) { + flags |= KeyframeValueFlagMask.START_TANGENT_WEIGHT; + dataView.setFloat32(currentOffset, startTangentWeight, true); + currentOffset += START_TANGENT_WEIGHT_BYTES; + } + + if (endTangent !== DEFAULT_END_TANGENT) { + flags |= KeyframeValueFlagMask.END_TANGENT; + dataView.setFloat32(currentOffset, endTangent, true); + currentOffset += END_TANGENT_BYTES; + } + + if (endTangentWeight !== DEFAULT_END_TANGENT_WEIGHT) { + flags |= KeyframeValueFlagMask.END_TANGENT_WEIGHT; + dataView.setFloat32(currentOffset, endTangentWeight, true); + currentOffset += END_TANGENT_WEIGHT_BYTES; + } + + dataView.setUint32(pFlags, flags, true); + + return currentOffset; +} + +function loadRealKeyFrameValue (dataView: DataView, keyframeValue: RealKeyframeValue, offset: number) { + let currentOffset = offset; + + const flags = dataView.getUint32(currentOffset, true); + currentOffset += KEY_FRAME_VALUE_FLAGS_BYTES; + + keyframeValue.value = dataView.getFloat32(currentOffset, true); + currentOffset += VALUE_BYTES; + + if (flags & KeyframeValueFlagMask.INTERP_MODE) { + keyframeValue.interpMode = dataView.getUint8(currentOffset); + currentOffset += INTERP_MODE_BYTES; + } + + if (flags & KeyframeValueFlagMask.TANGENT_WEIGHT_MODE) { + keyframeValue.tangentWeightMode = dataView.getUint8(currentOffset); + currentOffset += TANGENT_WEIGHT_MODE_BYTES; + } + + if (flags & KeyframeValueFlagMask.START_TANGENT) { + keyframeValue.startTangent = dataView.getFloat32(currentOffset, true); + currentOffset += START_TANGENT_BYTES; + } + + if (flags & KeyframeValueFlagMask.START_TANGENT_WEIGHT) { + keyframeValue.startTangentWeight = dataView.getFloat32(currentOffset, true); + currentOffset += START_TANGENT_WEIGHT_BYTES; + } + + if (flags & KeyframeValueFlagMask.END_TANGENT) { + keyframeValue.endTangent = dataView.getFloat32(currentOffset, true); + currentOffset += END_TANGENT_BYTES; + } + + if (flags & KeyframeValueFlagMask.END_TANGENT_WEIGHT) { + keyframeValue.endTangentWeight = dataView.getFloat32(currentOffset, true); + currentOffset += END_TANGENT_WEIGHT_BYTES; + } + + return currentOffset; +} + +function wrapRepeat (time: number, prevTime: number, nextTime: number) { + return prevTime + repeat(time - prevTime, nextTime - prevTime); +} + +function wrapPingPong (time: number, prevTime: number, nextTime: number) { + return prevTime + pingPong(time - prevTime, nextTime - prevTime); +} + +function linearTrend ( + prevTime: number, + prevValue: number, + nextTime: number, + nextValue: number, + time: number, +) { + const slope = (nextValue - prevValue) / (nextTime - prevTime); + return prevValue + (time - prevTime) * slope; +} + +function evalBetweenTwoKeyFrames ( + prevTime: number, + prevValue: RealKeyframeValue, + nextTime: number, + nextValue: RealKeyframeValue, + ratio: number, +) { + const dt = nextTime - prevTime; + switch (prevValue.interpMode) { + default: + case RealInterpMode.CONSTANT: + return prevValue.value; + case RealInterpMode.LINEAR: + return lerp(prevValue.value, nextValue.value, ratio); + case RealInterpMode.CUBIC: { + const ONE_THIRD = 1.0 / 3.0; + const { + endTangent: prevTangent, + endTangentWeight: prevTangentWeightSpecified, + } = prevValue; + const prevTangentWeightEnabled = isEndTangentWeightEnabled(prevValue.tangentWeightMode); + const { + startTangent: nextTangent, + startTangentWeight: nextTangentWeightSpecified, + } = nextValue; + const nextTangentWeightEnabled = isStartTangentWeightEnabled(nextValue.tangentWeightMode); + + if (!prevTangentWeightEnabled && !nextTangentWeightEnabled) { + // Optimize for the case when both x components of tangents are 1. + // See below. + const p1 = prevValue.value + ONE_THIRD * prevTangent * dt; + const p2 = nextValue.value - ONE_THIRD * nextTangent * dt; + return bezierInterp(prevValue.value, p1, p2, nextValue.value, ratio); + } else { + let prevTangentWeight = 0.0; + if (prevTangentWeightEnabled) { + prevTangentWeight = prevTangentWeightSpecified; + } else { + const x = dt; + const y = dt * prevTangent; + prevTangentWeight = Math.sqrt(x * x + y * y) * ONE_THIRD; + } + const angle0 = Math.atan(prevTangent); + const tx0 = Math.cos(angle0) * prevTangentWeight + prevTime; + const ty0 = Math.sin(angle0) * prevTangentWeight + prevValue.value; + + let nextTangentWeight = 0.0; + if (nextTangentWeightEnabled) { + nextTangentWeight = nextTangentWeightSpecified; + } else { + const x = dt; + const y = dt * nextTangent; + nextTangentWeight = Math.sqrt(x * x + y * y) * ONE_THIRD; + } + const angle1 = Math.atan(nextTangent); + const tx1 = -Math.cos(angle1) * nextTangentWeight + nextTime; + const ty1 = -Math.sin(angle1) * nextTangentWeight + nextValue.value; + + const dx = dt; + // Hermite to Bezier + const u0x = (tx0 - prevTime) / dx; + const u1x = (tx1 - prevTime) / dx; + const u0y = ty0; + const u1y = ty1; + // Converts from Bernstein Basis to Power Basis. + // Formula: [1, 0, 0, 0; -3, 3, 0, 0; 3, -6, 3, 0; -1, 3, -3, 1] * [p_0; p_1; p_2; p_3] + // -------------------------------------- + // | Basis | Coeff + // | t^3 | 3 * p_1 - p_0 - 3 * p_2 + p_3 + // | t^2 | 3 * p_0 - 6 * p_1 + 3 * p_2 + // | t^1 | 3 * p_1 - 3 * p_0 + // | t^0 | p_0 + // -------------------------------------- + // where: p_0 = 0, p_1 = u0x, p_2 = u1x, p_3 = 1 + // Especially, when both tangents are 1, we will have u0x = 1/3 and u1x = 2/3 + // and then: ratio = t, eg. the ratios are + // 1-1 corresponding to param t. That's why we can do optimization like above. + const coeff0 = 0.0; // 0 + const coeff1 = 3.0 * u0x; // 1 + const coeff2 = 3.0 * u1x - 6.0 * u0x; // -1 + const coeff3 = 3.0 * (u0x - u1x) + 1.0; // 1 + // Solves the param t from equation X(t) = ratio. + const solutions = [0.0, 0.0, 0.0] as [number, number, number]; + const nSolutions = solveCubic(coeff0 - ratio, coeff1, coeff2, coeff3, solutions); + const param = getParamFromCubicSolution(solutions, nSolutions, ratio); + // Solves Y. + const y = bezierInterp(prevValue.value, u0y, u1y, nextValue.value, param); + return y; + } + } + } +} + +function isStartTangentWeightEnabled (tangentWeightMode: TangentWeightMode) { + return (tangentWeightMode & TangentWeightMode.START) !== 0; +} + +function isEndTangentWeightEnabled (tangentWeightMode: TangentWeightMode) { + return (tangentWeightMode & TangentWeightMode.END) !== 0; +} + +function bezierInterp (p0: number, p1: number, p2: number, p3: number, t: number) { + const u = 1 - t; + const coeff0 = u * u * u; + const coeff1 = 3 * u * u * t; + const coeff2 = 3 * u * t * t; + const coeff3 = t * t * t; + return coeff0 * p0 + coeff1 * p1 + coeff2 * p2 + coeff3 * p3; +} + +function getParamFromCubicSolution (solutions: readonly [number, number, number], solutionsCount: number, x: number) { + let param = x; + if (solutionsCount === 1) { + param = solutions[0]; + } else { + param = -Infinity; + for (let iSolution = 0; iSolution < solutionsCount; ++iSolution) { + const solution = solutions[iSolution]; + if (solution >= 0.0 && solution <= 1.0) { + if (solution > param) { + param = solution; + } + } + } + if (param === -Infinity) { + param = 0.0; + } + } + return param; +} diff --git a/cocos/core/curves/index.ts b/cocos/core/curves/index.ts new file mode 100644 index 00000000000..cf45cad4b78 --- /dev/null +++ b/cocos/core/curves/index.ts @@ -0,0 +1,25 @@ +export { + RealCurve, + RealKeyframeValue, + RealInterpMode, + ExtrapMode, + TangentWeightMode, +} from './curve'; + +export { + IntegerCurve, +} from './integer-curve'; + +export { + QuaternionCurve, + QuaternionKeyframeValue, + QuaternionInterpMode, +} from './quat-curve'; + +export { + ObjectCurve, +} from './object-curve'; + +export type { + ObjectCurveKeyframe, +} from './object-curve'; diff --git a/cocos/core/curves/integer-curve.ts b/cocos/core/curves/integer-curve.ts new file mode 100644 index 00000000000..906f12770a5 --- /dev/null +++ b/cocos/core/curves/integer-curve.ts @@ -0,0 +1,55 @@ +import { ccclass, serializable } from '../data/decorators'; +import { RealCurve } from './curve'; +import { deserializeSymbol, serializeSymbol } from '../data/serialization-symbols'; + +export enum RoundType { + /** + * Returns the integer part of the result by removing any fractional digits. + */ + TRUNC, + + /** + * Returns the largest integer less than or equal to the result. + */ + FLOOR, + + /** + * Rounds the result up to the next largest integer. + */ + CEIL, + + /** + * Returns the result rounded to the nearest integer. + */ + ROUND, +} + +@ccclass('cc.IntegerCurve') +export class IntegerCurve extends RealCurve { + @serializable + public truncType: RoundType = RoundType.TRUNC; + + public evaluate (time: number) { + const value = super.evaluate(time); + switch (this.truncType) { + default: + case RoundType.TRUNC: return Math.trunc(value); + case RoundType.FLOOR: return Math.floor(value); + case RoundType.CEIL: return Math.ceil(value); + case RoundType.ROUND: return Math.round(value); + } + } + + public [serializeSymbol] () { + const baseSerialized = super[serializeSymbol](); + const buffer = new Uint8Array(baseSerialized.byteLength + 1); + buffer[0] = this.truncType; + buffer.set(baseSerialized, 1); + return buffer; + } + + public [deserializeSymbol] (serialized: ReturnType) { + this.truncType = serialized[0]; + super[deserializeSymbol](new Uint8Array(serialized, 1)); + } +} diff --git a/cocos/core/curves/keyframe-curve.ts b/cocos/core/curves/keyframe-curve.ts new file mode 100644 index 00000000000..f8128ee9858 --- /dev/null +++ b/cocos/core/curves/keyframe-curve.ts @@ -0,0 +1,193 @@ +import { binarySearchEpsilon } from '../algorithm/binary-search'; +import { ccclass, serializable } from '../data/decorators'; +import { assertIsTrue } from '../data/utils/asserts'; +import { approx } from '../math'; +import type { CurveBase } from './curve-base'; + +type KeyFrame = [number, TKeyframeValue]; + +/** + * Curve. + */ +@ccclass('cc.KeyframeCurve') +export class KeyframeCurve implements CurveBase { + /** + * Gets the count of keyframes. + */ + get keyFramesCount () { + return this._times.length; + } + + /** + * Indicates if this curve has no any key frame. + */ + get empty () { + return this._times.length === 0; + } + + /** + * Gets the minimal time. + */ + get rangeMin () { + return this._times[0]; + } + + /** + * Gets the maximum time. + */ + get rangeMax () { + return this._times[this._values.length - 1]; + } + + /** + * Returns an iterator to keyframe pairs. + */ + public* keyframes (): Iterable> { + for (let i = 0; i < this._times.length; ++i) { + yield [this._times[i], this._values[i]]; + } + } + + public times (): Iterable { + return this._times; + } + + public values (): Iterable { + return this._values; + } + + /** + * Gets the time of specified keyframe. + * @param index Index to the keyframe. + * @returns The keyframe 's time. + */ + public getKeyframeTime (index: number): number { + return this._times[index]; + } + + /** + * Gets the value of specified keyframe. + * @param index Index to the keyframe. + * @returns The keyframe 's value. + */ + public getKeyframeValue (index: number): TKeyframeValue { + return this._values[index]; + } + + /** + * Adds a keyframe into this curve. + * @param time Time of the keyframe. + * @param value Value of the keyframe. + * @returns The index to the new keyframe. + */ + public addKeyFrame (time: number, keyframeValue: TKeyframeValue): number { + return this._insertNewKeyframe(time, keyframeValue); + } + + /** + * Removes a keyframe from this curve. + * @param index Index to the keyframe. + */ + public removeKeyframe (index: number) { + this._times.splice(index, 1); + this._values.splice(index, 1); + } + + /** + * Searches for the keyframe at specified time. + * @param time Time to search. + * @returns Index to the keyframe or negative number if not found. + */ + public indexOfKeyframe (time: number) { + return binarySearchEpsilon(this._times, time); + } + + /** + * Updates the time of a keyframe. + * @param index Index to the keyframe. + * @param time New time. + */ + public updateTime (index: number, time: number) { + const value = this._values[index]; + this.removeKeyframe(index); + this._insertNewKeyframe(time, value); + } + + /** + * Assigns all keyframes. + * @param keyframes An iterable to keyframes. The keyframes should be sorted by their time. + */ + public assignSorted (keyframes: Iterable<[number, TKeyframeValue]>): void; + + /** + * Assigns all keyframes. + * @param times Times array. Should be sorted. + * @param values Values array. Corresponding to each time in `times`. + */ + public assignSorted (times: readonly number[], values: TKeyframeValue[]): void; + + public assignSorted (times: Iterable<[number, TKeyframeValue]> | readonly number[], values?: readonly TKeyframeValue[]) { + if (values !== undefined) { + assertIsTrue(Array.isArray(times)); + assertIsTrue(times.length === values.length); + this._times = times.slice(); + this._values = values.map((value) => value); + } else { + const keyframes = Array.from(times as Iterable<[number, TKeyframeValue]>); + this._times = keyframes.map(([time]) => time); + this._values = keyframes.map(([, value]) => value); + } + + assertIsTrue(isSorted(this._times)); + } + + /** + * Removes all key frames. + */ + public clear () { + this._times.length = 0; + this._values.length = 0; + } + + protected searchKeyframe (time: number) { + return binarySearchEpsilon(this._times, time); + } + + private _insertNewKeyframe (time: number, value: TKeyframeValue) { + const times = this._times; + const values = this._values; + const nFrames = times.length; + + const index = binarySearchEpsilon(times, time); + if (index >= 0) { + return index; + } + const iNext = ~index; + if (iNext === 0) { + times.unshift(time); + values.unshift(value); + } else if (iNext === nFrames) { + times.push(time); + values.push(value); + } else { + assertIsTrue(nFrames > 1); + times.splice(iNext - 1, 0, time); + values.splice(iNext - 1, 0, value); + } + return iNext; + } + + // Times are always sorted and 1-1 correspond to values. + @serializable + protected _times: number[] = []; + + @serializable + protected _values: TKeyframeValue[] = []; +} + +function isSorted (values: number[]) { + return values.every( + (value, index, array) => index === 0 + || value > array[index - 1] || approx(value, array[index - 1], 1e-6), + ); +} diff --git a/cocos/core/curves/keys-shared-curves.ts b/cocos/core/curves/keys-shared-curves.ts new file mode 100644 index 00000000000..61ff32b549d --- /dev/null +++ b/cocos/core/curves/keys-shared-curves.ts @@ -0,0 +1,285 @@ +import { binarySearchEpsilon } from '../algorithm/binary-search'; +import { ccclass, serializable } from '../data/decorators'; +import { assertIsTrue } from '../data/utils/asserts'; +import { approx, IQuatLike, lerp, Quat } from '../math'; +import { RealKeyframeValue, ExtrapMode, RealCurve } from './curve'; +import { QuaternionCurve, QuaternionInterpMode } from './quat-curve'; +import { RealInterpMode } from './real-curve-param'; + +const DEFAULT_EPSILON = 1e-5; + +/** + * Considering most of model animations are baked and most of its curves share same times, + * we do not have to do time searching for many times. + */ +@ccclass('cc.KeySharedCurves') +class KeysSharedCurves { + /** + * Only for internal(serialization) usage. + */ + constructor (); + + constructor (times: number[]); + + constructor (times?: number[]) { + if (!times) { + this._times = []; + return; + } + + const nKeyframes = times.length; + + this._keyframesCount = nKeyframes; + this._times = times; + + if (nKeyframes > 1) { + const EPSILON = 1e-6; + let lastDiff = 0.0; + let mayBeOptimized = false; + for (let iFrame = 1; iFrame < nKeyframes; iFrame++) { + const curDiff = times[iFrame] - times[iFrame - 1]; + if (iFrame === 1) { + lastDiff = curDiff; + } else if (Math.abs(curDiff - lastDiff) > EPSILON) { + mayBeOptimized = false; + break; + } + } + if (mayBeOptimized) { + this._optimized = true; + this._times = [this._times[0], this._times[1]]; + } + } + } + + get keyframesCount () { + return this._keyframesCount; + } + + protected matchTimes (times: readonly number[], EPSILON = DEFAULT_EPSILON) { + if (this._optimized) { + const firstTime = this._times[0]; + const diff = this._times[1] - firstTime; + return times.every( + (t, iKeyframe) => approx(t, firstTime + diff * iKeyframe, EPSILON), + ); + } else { + return times.every( + (t, iKeyframe) => approx(t, this._times[iKeyframe], EPSILON), + ); + } + } + + protected getFirstTime () { + return this._times[0]; + } + + protected getLastTime () { + if (!this._optimized) { + return this._times[this._times.length - 1]; + } else { + const diff = this._times[1] - this._times[0]; + return this._times[0] + diff * this._keyframesCount; + } + } + + protected calculateLocation (time: number, out: TimeLocation) { + const { _times: times, _optimized: optimized, keyframesCount: nKeyframes } = this; + if (optimized) { + const firstTime = times[0]; + const diff = times[1] - firstTime; + const div = (time - firstTime) / diff; + const previous = Math.floor(div); + out.previous = previous; + out.ratio = div - previous; + } else { + const index = binarySearchEpsilon(times, time); + if (index >= 0) { + // Exactly matched + out.previous = index; + out.ratio = 0.0; + } else { + const iNext = ~index; + assertIsTrue(iNext >= 1 && iNext < nKeyframes); + const iPrev = iNext - 1; + const prevTime = times[iPrev]; + out.ratio = (time - prevTime) / (times[iNext] - prevTime); + out.previous = iPrev; + } + } + return out; + } + + @serializable + private _times: number[]; + + @serializable + private _optimized = false; + + @serializable + private _keyframesCount = 0; +} + +interface TimeLocation { + previous: number; + ratio: number; +} + +const globalLocation: TimeLocation = { + previous: 0, + ratio: 0, +}; + +@ccclass('cc.KeySharedRealCurves') +export class KeySharedRealCurves extends KeysSharedCurves { + public static allowedForCurve (curve: RealCurve) { + return curve.postExtrap === ExtrapMode.CLAMP + && curve.preExtrap === ExtrapMode.CLAMP + && Array.from(curve.values()).every((value) => value.interpMode === RealInterpMode.LINEAR); + } + + get curveCount () { + return this._curves.length; + } + + public matchCurve (curve: RealCurve, EPSILON = DEFAULT_EPSILON) { + if (curve.keyFramesCount !== this.keyframesCount) { + return false; + } + const times = Array.from(curve.times()); + return super.matchTimes(times, EPSILON); + } + + public addCurve (curve: RealCurve) { + assertIsTrue(curve.keyFramesCount === this.keyframesCount); + this._curves.push({ + values: Array.from(curve.values()).map(({ value }) => value), + }); + } + + public evaluate (time: number, values: number[]) { + const { + _curves: curves, + keyframesCount: nKeyframes, + } = this; + + const nCurves = curves.length; + assertIsTrue(values.length === nCurves); + + if (nKeyframes === 0) { + return; + } + + const firstTime = super.getFirstTime(); + if (time <= firstTime) { + for (let iCurve = 0; iCurve < nCurves; ++iCurve) { + values[iCurve] = this._curves[iCurve].values[0]; + } + return; + } + + const lastTime = super.getLastTime(); + if (time >= lastTime) { + const iLastFrame = nKeyframes - 1; + for (let iCurve = 0; iCurve < nCurves; ++iCurve) { + values[iCurve] = this._curves[iCurve].values[iLastFrame]; + } + return; + } + + const { previous, ratio } = super.calculateLocation(time, globalLocation); + if (ratio !== 0.0) { + for (let iCurve = 0; iCurve < nCurves; ++iCurve) { + const { values: curveValues } = this._curves[iCurve]; + values[iCurve] = lerp(curveValues[previous], curveValues[previous + 1], ratio); + } + } else { + for (let iCurve = 0; iCurve < nCurves; ++iCurve) { + const { values: curveValues } = this._curves[iCurve]; + values[iCurve] = curveValues[previous]; + } + } + } + + @serializable + private _curves: { + values: number[]; + }[] = []; +} + +@ccclass('cc.KeySharedQuaternionCurves') +export class KeySharedQuaternionCurves extends KeysSharedCurves { + public static allowedForCurve (curve: QuaternionCurve) { + return curve.postExtrap === ExtrapMode.CLAMP + && curve.preExtrap === ExtrapMode.CLAMP + && Array.from(curve.values()).every((value) => value.interpMode === QuaternionInterpMode.SLERP); + } + + get curveCount () { + return this._curves.length; + } + + public matchCurve (curve: QuaternionCurve, EPSILON = 1e-5) { + if (curve.keyFramesCount !== this.keyframesCount) { + return false; + } + const times = Array.from(curve.times()); + return super.matchTimes(times, EPSILON); + } + + public addCurve (curve: QuaternionCurve) { + assertIsTrue(curve.keyFramesCount === this.keyframesCount); + this._curves.push({ + values: Array.from(curve.values()).map(({ value }) => Quat.clone(value)), + }); + } + + public evaluate (time: number, values: IQuatLike[]) { + const { + _curves: curves, + keyframesCount: nKeyframes, + } = this; + + const nCurves = curves.length; + assertIsTrue(values.length === nCurves); + + if (nKeyframes === 0) { + return; + } + + const firstTime = super.getFirstTime(); + if (time <= firstTime) { + for (let iCurve = 0; iCurve < nCurves; ++iCurve) { + Quat.copy(values[iCurve], this._curves[iCurve].values[0]); + } + return; + } + + const lastTime = super.getLastTime(); + if (time >= lastTime) { + const iLastFrame = nKeyframes - 1; + for (let iCurve = 0; iCurve < nCurves; ++iCurve) { + Quat.copy(values[iCurve], this._curves[iCurve].values[iLastFrame]); + } + return; + } + + const { previous, ratio } = super.calculateLocation(time, globalLocation); + if (ratio !== 0.0) { + for (let iCurve = 0; iCurve < nCurves; ++iCurve) { + const { values: curveValues } = this._curves[iCurve]; + Quat.slerp(values[iCurve], curveValues[previous], curveValues[previous + 1], ratio); + } + } else { + for (let iCurve = 0; iCurve < nCurves; ++iCurve) { + const { values: curveValues } = this._curves[iCurve]; + Quat.copy(values[iCurve], curveValues[previous]); + } + } + } + + @serializable + private _curves: { + values: Quat[]; + }[] = []; +} diff --git a/cocos/core/curves/object-curve.ts b/cocos/core/curves/object-curve.ts new file mode 100644 index 00000000000..d2126073fc6 --- /dev/null +++ b/cocos/core/curves/object-curve.ts @@ -0,0 +1,17 @@ +import { ccclass } from '../data/decorators'; +import { clamp } from '../math'; +import { KeyframeCurve } from './keyframe-curve'; + +export type ObjectCurveKeyframe = T; + +@ccclass('cc.ObjectCurve') +export class ObjectCurve extends KeyframeCurve> { + public evaluate (time: number) { + const iSearch = this.searchKeyframe(time); + if (iSearch >= 0) { + return this._values[iSearch]; + } + const iPrev = clamp((~iSearch) - 1, 0, this._values.length - 1); + return this._values[iPrev]; + } +} diff --git a/cocos/core/curves/quat-curve.ts b/cocos/core/curves/quat-curve.ts new file mode 100644 index 00000000000..f8968b9e9ab --- /dev/null +++ b/cocos/core/curves/quat-curve.ts @@ -0,0 +1,298 @@ +import { assertIsTrue } from '../data/utils/asserts'; +import { IQuatLike, pingPong, Quat, repeat } from '../math'; +import { KeyframeCurve } from './keyframe-curve'; +import { ExtrapMode } from './curve'; +import { binarySearchEpsilon } from '../algorithm/binary-search'; +import { ccclass, serializable } from '../data/decorators'; +import { deserializeSymbol, serializeSymbol } from '../data/serialization-symbols'; + +@ccclass('cc.QuaternionKeyframeValue') +export class QuaternionKeyframeValue { + /** + * Interpolation method used for this keyframe. + */ + @serializable + public interpMode: QuaternionInterpMode = QuaternionInterpMode.SLERP; + + /** + * Value of the keyframe. + */ + @serializable + public value: IQuatLike = Quat.clone(Quat.IDENTITY); + + constructor ({ + value, + interpMode, + }: Partial) { + // TODO: shall we normalize it? + this.value = value ? Quat.clone(value) : this.value; + this.interpMode = interpMode ?? this.interpMode; + } +} + +/** + * The method used for interpolation between values of a keyframe and its next keyframe. + */ +export enum QuaternionInterpMode { + /** + * Perform spherical linear interpolation. + */ + SLERP, + + /** + * Always use the value from this keyframe. + */ + CONSTANT, + + // #region TODO: Spherical Quadrangle Interpolation + /** + * TODO: Spherical Quadrangle Interpolation + * - https://theory.org/software/qfa/writeup/node12.html + * - http://digitalrune.github.io/DigitalRune-Documentation/html/58f74cca-83a3-5e9e-6d5d-63b09a723f90.htm + */ + // SQUAD, + // #endregion +} + +/** + * Quaternion curve. + */ +@ccclass('cc.QuaternionCurve') +export class QuaternionCurve extends KeyframeCurve { + /** + * Gets or sets the operation should be taken + * if input time is less than the time of first keyframe when evaluating this curve. + * Defaults to `ExtrapMode.CLAMP`. + */ + get preExtrap () { + return this._preExtrap; + } + + set preExtrap (value) { + this._preExtrap = value; + } + + /** + * Gets or sets the operation should be taken + * if input time is greater than the time of last keyframe when evaluating this curve. + * Defaults to `ExtrapMode.CLAMP`. + */ + get postExtrap () { + return this._postExtrap; + } + + set postExtrap (value) { + this._postExtrap = value; + } + + /** + * Evaluates this curve at specified time. + * @param time Input time. + * @returns Result value. + */ + public evaluate (time: number, quat?: Quat): Quat { + quat ??= new Quat(); + + const { + _times: times, + _values: values, + _postExtrap: postExtrap, + _preExtrap: preExtrap, + } = this; + const nFrames = times.length; + + if (nFrames === 0) { + return quat; + } + + const firstTime = times[0]; + const lastTime = times[nFrames - 1]; + if (time < firstTime) { + // Underflow + const preValue = values[0]; + switch (preExtrap) { + case ExtrapMode.REPEAT: + time = firstTime + repeat(time - firstTime, lastTime - firstTime); + break; + case ExtrapMode.PING_PONG: + time = firstTime + pingPong(time - firstTime, lastTime - firstTime); + break; + case ExtrapMode.CLAMP: + default: + return Quat.copy(quat, preValue.value); + } + } else if (time > lastTime) { + // Overflow + const preValue = values[nFrames - 1]; + switch (postExtrap) { + case ExtrapMode.REPEAT: + time = firstTime + repeat(time - firstTime, lastTime - firstTime); + break; + case ExtrapMode.PING_PONG: + time = firstTime + pingPong(time - firstTime, lastTime - firstTime); + break; + case ExtrapMode.CLAMP: + default: + return Quat.copy(quat, preValue.value); + } + } + + const index = binarySearchEpsilon(times, time); + if (index >= 0) { + return Quat.copy(quat, values[index].value); + } + + const iNext = ~index; + assertIsTrue(iNext !== 0 && iNext !== nFrames && nFrames > 1); + + const iPre = iNext - 1; + const preTime = times[iPre]; + const preValue = values[iPre]; + const nextTime = times[iNext]; + const nextValue = values[iNext]; + assertIsTrue(nextTime > time && time > preTime); + const dt = nextTime - preTime; + + const ratio = (time - preTime) / dt; + switch (preValue.interpMode) { + default: + case QuaternionInterpMode.CONSTANT: + return Quat.copy(quat, preValue.value); + case QuaternionInterpMode.SLERP: + return Quat.slerp(quat, preValue.value, nextValue.value, ratio); + } + } + + /** + * Adds a keyframe into this curve. + * @param time Time of the keyframe. + * @param value Value of the keyframe. + * @returns The index to the new keyframe. + */ + public addKeyFrame (time: number, value: IQuatLike | QuaternionKeyframeValue): number { + const keyframeValue = value instanceof QuaternionKeyframeValue + ? value : new QuaternionKeyframeValue({ value }); + return super.addKeyFrame(time, keyframeValue); + } + + public [serializeSymbol] () { + const { + _times: times, + _values: keyframeValues, + } = this; + + let interpModeRepeated = true; + keyframeValues.forEach((keyframeValue, _index, [firstKeyframeValue]) => { + // Values are unlikely to be unified. + if (interpModeRepeated + && keyframeValue.interpMode !== firstKeyframeValue.interpMode) { interpModeRepeated = false; } + }); + + const nKeyframes = times.length; + + const nFrames = nKeyframes; + const interpModesSize = INTERP_MODE_BYTES * (interpModeRepeated ? 1 : nFrames); + + let dataSize = 0; + dataSize += ( + FLAGS_BYTES + + FRAME_COUNT_BYTES + + TIME_BYTES * nFrames + + VALUE_BYTES * 4 * nFrames + + interpModesSize + + 0 + ); + + const dataView = new DataView(new ArrayBuffer(dataSize)); + let P = 0; + + // Flags + let flags = 0; + if (interpModeRepeated) { flags |= KeyframeValueFlagMask.INTERP_MODE; } + dataView.setUint32(P, flags, true); P += FLAGS_BYTES; + + // Frame count + dataView.setUint32(P, nFrames, true); P += FRAME_COUNT_BYTES; + + // Times + times.forEach((time, index) => dataView.setFloat32(P + TIME_BYTES * index, time, true)); + P += TIME_BYTES * nFrames; + + keyframeValues.forEach(({ value: { x, y, z, w } }, index) => { + const pQuat = P + VALUE_BYTES * 4 * index; + dataView.setFloat32(pQuat + VALUE_BYTES * 0, x, true); + dataView.setFloat32(pQuat + VALUE_BYTES * 1, y, true); + dataView.setFloat32(pQuat + VALUE_BYTES * 2, z, true); + dataView.setFloat32(pQuat + VALUE_BYTES * 3, w, true); + }); + P += VALUE_BYTES * 4 * nFrames; + + // Frame values + const INTERP_MODES_START = P; P += interpModesSize; + let pInterpMode = INTERP_MODES_START; + keyframeValues.forEach(({ interpMode }) => { + dataView.setUint8(pInterpMode, interpMode); + + if (!interpModeRepeated) { pInterpMode += INTERP_MODE_BYTES; } + }); + + return new Uint8Array(dataView.buffer); + } + + public [deserializeSymbol] (serialized: ReturnType) { + const dataView = new DataView(serialized.buffer, serialized.byteOffset, serialized.byteLength); + let P = 0; + + // Flags + const flags = dataView.getUint32(P, true); P += FLAGS_BYTES; + const interpModeRepeated = flags & KeyframeValueFlagMask.INTERP_MODE; + + // Frame count + const nFrames = dataView.getUint32(P, true); P += FRAME_COUNT_BYTES; + + // Times + const times = Array.from({ length: nFrames }, + (_, index) => dataView.getFloat32(P + TIME_BYTES * index, true)); + P += TIME_BYTES * nFrames; + + // Frame values + const P_VALUES = P; P += VALUE_BYTES * 4 * nFrames; + let pInterpModes = P; + const keyframeValues = Array.from({ length: nFrames }, + (_, index) => { + const pQuat = P_VALUES + VALUE_BYTES * 4 * index; + const x = dataView.getFloat32(pQuat + VALUE_BYTES * 0, true); + const y = dataView.getFloat32(pQuat + VALUE_BYTES * 1, true); + const z = dataView.getFloat32(pQuat + VALUE_BYTES * 2, true); + const w = dataView.getFloat32(pQuat + VALUE_BYTES * 3, true); + const keyframeValue = new QuaternionKeyframeValue({ + value: { x, y, z, w }, + interpMode: dataView.getUint8(pInterpModes), + }); + if (!interpModeRepeated) { + pInterpModes += INTERP_MODE_BYTES; + } + return keyframeValue; + }); + + this._times = times; + this._values = keyframeValues; + } + + // Always sorted by time + @serializable + private _preExtrap: ExtrapMode = ExtrapMode.CLAMP; + + @serializable + private _postExtrap: ExtrapMode = ExtrapMode.CLAMP; +} + +enum KeyframeValueFlagMask { + INTERP_MODE = 1 << 0, +} + +const FLAGS_BYTES = 1; +const FRAME_COUNT_BYTES = 4; +const TIME_BYTES = 4; +const VALUE_BYTES = 4; +const INTERP_MODE_BYTES = 1; diff --git a/cocos/core/curves/real-curve-param.ts b/cocos/core/curves/real-curve-param.ts new file mode 100644 index 00000000000..ba9d31c6b54 --- /dev/null +++ b/cocos/core/curves/real-curve-param.ts @@ -0,0 +1,60 @@ +/** + * The method used for interpolation between values of a keyframe and its next keyframe. + */ +export enum RealInterpMode { + /** + * Perform linear interpolation. + */ + LINEAR, + + /** + * Always use the value from this keyframe. + */ + CONSTANT, + + /** + * Perform cubic(hermite) interpolation. + */ + CUBIC, +} + +/** + * Specifies how to extrapolate the value + * if input time is underflow(less than the the first frame time) or + * overflow(greater than the last frame time) when evaluating an animation curve. + */ +export enum ExtrapMode { + /** + * Compute the result + * according to the first two frame's linear trend in the case of underflow and + * according to the last two frame's linear trend in the case of overflow. + * If there are less than two frames, fallback to `CLAMP`. + */ + LINEAR, + + /** + * Use first frame's value in the case of underflow, + * use last frame's value in the case of overflow. + */ + CLAMP, + + /** + * Before evaluation, repeatedly mapping the input time into the allowed range. + */ + REPEAT, + + /** + * Before evaluation, mapping the input time into the allowed range like ping pong. + */ + PING_PONG, +} + +export enum TangentWeightMode { + NONE = 0, + + START = 1, + + END = 2, + + BOTH = 1 | 2, +} diff --git a/cocos/core/curves/solve-cubic.ts b/cocos/core/curves/solve-cubic.ts new file mode 100644 index 00000000000..b5bae6adb83 --- /dev/null +++ b/cocos/core/curves/solve-cubic.ts @@ -0,0 +1,63 @@ +// cSpell:words Cardano's irreducibilis + +/** + * Solve Cubic Equation using Cardano's formula. + * The equation is formed from coeff0 + coeff1 * x + coeff2 * x^2 + coeff3 * x^3 = 0. + * Modified from https://github.com/erich666/GraphicsGems/blob/master/gems/Roots3And4.c . + */ +export function solveCubic (coeff0: number, coeff1: number, coeff2: number, coeff3: number, solutions: [number, number, number]) { + // normal form: x^3 + Ax^2 + Bx + C = 0 + const a = coeff2 / coeff3; + const b = coeff1 / coeff3; + const c = coeff0 / coeff3; + + // substitute x = y - A/3 to eliminate quadric term: + // x^3 +px + q = 0 + const sqrA = a * a; + const p = 1.0 / 3.0 * (-1.0 / 3 * sqrA + b); + const q = 1.0 / 2.0 * (2.0 / 27.0 * a * sqrA - 1.0 / 3 * a * b + c); + + // use Cardano's formula + const cubicP = p * p * p; + const d = q * q + cubicP; + + let nSolutions = 0; + if (isZero(d)) { + if (isZero(q)) { // one triple solution + solutions[0] = 0; + return 1; + } else { // one single and one double solution + const u = Math.cbrt(-q); + solutions[0] = 2 * u; + solutions[1] = -u; + return 2; + } + } else if (d < 0) { // Casus irreducibilis: three real solutions + const phi = 1.0 / 3 * Math.acos(-q / Math.sqrt(-cubicP)); + const t = 2 * Math.sqrt(-p); + + solutions[0] = t * Math.cos(phi); + solutions[1] = -t * Math.cos(phi + Math.PI / 3); + solutions[2] = -t * Math.cos(phi - Math.PI / 3); + nSolutions = 3; + } else { // one real solution + const sqrtD = Math.sqrt(d); + const u = Math.cbrt(sqrtD - q); + const v = -Math.cbrt(sqrtD + q); + solutions[0] = u + v; + nSolutions = 1; + } + + const sub = 1.0 / 3 * a; + for (let i = 0; i < nSolutions; ++i) { + solutions[i] -= sub; + } + + return nSolutions; +} + +const EQN_EPS = 1e-9; + +function isZero (x: number) { + return x > -EQN_EPS && x < EQN_EPS; +} diff --git a/cocos/core/data/serialization-symbols.ts b/cocos/core/data/serialization-symbols.ts new file mode 100644 index 00000000000..ec28fea7324 --- /dev/null +++ b/cocos/core/data/serialization-symbols.ts @@ -0,0 +1,5 @@ +import { TEST } from 'internal:constants'; + +export const serializeSymbol: unique symbol = (TEST ? Symbol.for('serialize') : Symbol('[[deserialize]]')) as any; + +export const deserializeSymbol: unique symbol = (TEST ? Symbol.for('deserialize') : Symbol('[[deserialize]]')) as any; diff --git a/cocos/core/data/utils/asserts.ts b/cocos/core/data/utils/asserts.ts index df7c16d1d75..0df4c4b9429 100644 --- a/cocos/core/data/utils/asserts.ts +++ b/cocos/core/data/utils/asserts.ts @@ -39,7 +39,7 @@ export function assertIsNonNullable (expr: T, message?: string): asserts expr * @param expr Testing expression. * @param message Optional message. */ -export function assertIsTrue (expr: boolean, message?: string) { +export function assertIsTrue (expr: unknown, message?: string): asserts expr { if (DEBUG && !expr) { throw new Error(`Assertion failed: ${message ?? ''}`); } diff --git a/cocos/core/geometry/curve.ts b/cocos/core/geometry/curve.ts index 9622129067a..99ab5069197 100644 --- a/cocos/core/geometry/curve.ts +++ b/cocos/core/geometry/curve.ts @@ -30,19 +30,12 @@ import { CCClass } from '../data/class'; import { clamp, inverseLerp, pingPong, repeat } from '../math/utils'; -import { Enum } from '../value-types/enum'; import { WrapModeMask } from '../animation/types'; +import { ExtrapMode, RealCurve, RealInterpMode, RealKeyframeValue } from '../curves'; +import { ccclass, serializable } from '../data/decorators'; const LOOK_FORWARD = 3; -const WrapMode = Enum({ - Default: WrapModeMask.Default, - Normal: WrapModeMask.Normal, - Clamp: WrapModeMask.Clamp, - Loop: WrapModeMask.Loop, - PingPong: WrapModeMask.PingPong, -}); - /** * @en * A key frame in the curve. @@ -110,7 +103,11 @@ export function evalOptCurve (t: number, coefs: Float32Array | number[]) { * @zh * 描述一条曲线,其中每个相邻关键帧采用三次hermite插值计算。 */ +@ccclass('cc.AnimationCurve') export class AnimationCurve { + @serializable + private _curve!: RealCurve; + private static defaultKF: Keyframe[] = [{ time: 0, value: 1, @@ -123,13 +120,42 @@ export class AnimationCurve { outTangent: 0, }]; + /** + * For internal usage only. + * @internal + */ + get _internalCurve () { + return this._curve; + } + /** * @en * The key frame of the curve. * @zh * 曲线的关键帧。 */ - public keyFrames: Keyframe[] | null; + get keyFrames () { + return Array.from(this._curve.keyframes()).map(([time, value]) => { + const legacyKeyframe = new Keyframe(); + legacyKeyframe.time = time; + legacyKeyframe.value = value.value; + legacyKeyframe.inTangent = value.startTangent; + legacyKeyframe.outTangent = value.endTangent; + return legacyKeyframe; + }); + } + + set keyFrames (value) { + this._curve.assignSorted(value.map((legacyCurve) => [ + legacyCurve.time, + new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + value: legacyCurve.value, + startTangent: legacyCurve.inTangent, + endTangent: legacyCurve.outTangent, + }), + ])); + } /** * @en @@ -137,7 +163,13 @@ export class AnimationCurve { * @zh * 当采样时间超出左端时采用的循环模式[[WrapMode]]。 */ - public preWrapMode: number = WrapMode.Loop; + get preWrapMode () { + return toLegacyWrapMode(this._curve.preExtrap); + } + + set preWrapMode (value) { + this._curve.preExtrap = fromLegacyWrapMode(value); + } /** * @en @@ -145,7 +177,13 @@ export class AnimationCurve { * @zh * 当采样时间超出右端时采用的循环模式[[WrapMode]]。 */ - public postWrapMode: number = WrapMode.Clamp; + get postWrapMode () { + return toLegacyWrapMode(this._curve.postExtrap); + } + + set postWrapMode (value) { + this._curve.postExtrap = fromLegacyWrapMode(value); + } private cachedKey: OptimizedKey; @@ -153,8 +191,28 @@ export class AnimationCurve { * 构造函数。 * @param keyFrames 关键帧。 */ - constructor (keyFrames: Keyframe[] | null = null) { - this.keyFrames = keyFrames || ([] as Keyframe[]).concat(AnimationCurve.defaultKF); + constructor (keyFrames: Keyframe[] | null | RealCurve = null) { + if (keyFrames instanceof RealCurve) { + this._curve = keyFrames; + } else { + const curve = new RealCurve(); + this._curve = curve; + curve.preExtrap = ExtrapMode.REPEAT; + curve.postExtrap = ExtrapMode.CLAMP; + if (!keyFrames) { + curve.assignSorted([ + [0.0, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 1.0 })], + [1.0, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 1.0 })], + ]); + } else { + curve.assignSorted(keyFrames.map((legacyKeyframe) => [legacyKeyframe.time, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + value: legacyKeyframe.value, + startTangent: legacyKeyframe.inTangent, + endTangent: legacyKeyframe.outTangent, + })])); + } + } this.cachedKey = new OptimizedKey(); } @@ -165,11 +223,17 @@ export class AnimationCurve { * 添加一个关键帧。 * @param keyFrame 关键帧。 */ - public addKey (keyFrame: Keyframe) { - if (this.keyFrames == null) { - this.keyFrames = []; + public addKey (keyFrame: Keyframe | null) { + if (!keyFrame) { + this._curve.clear(); + } else { + this._curve.addKeyFrame(keyFrame.time, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + value: keyFrame.value, + startTangent: keyFrame.inTangent, + endTangent: keyFrame.outTangent, + })); } - this.keyFrames.push(keyFrame); } /** @@ -177,57 +241,7 @@ export class AnimationCurve { * @param time */ public evaluate_slow (time: number) { - let wrappedTime = time; - const wrapMode = time < 0 ? this.preWrapMode : this.postWrapMode; - const startTime = this.keyFrames![0].time; - const endTime = this.keyFrames![this.keyFrames!.length - 1].time; - switch (wrapMode) { - case WrapMode.Loop: - wrappedTime = repeat(time - startTime, endTime - startTime) + startTime; - break; - case WrapMode.PingPong: - wrappedTime = pingPong(time - startTime, endTime - startTime) + startTime; - break; - case WrapMode.Default: - case WrapMode.Normal: - case WrapMode.Clamp: - wrappedTime = clamp(time, startTime, endTime); - break; - default: - wrappedTime = clamp(time, startTime, endTime); - break; - } - let preKFIndex = 0; - if (wrappedTime > this.keyFrames![0].time) { - if (wrappedTime >= this.keyFrames![this.keyFrames!.length - 1].time) { - preKFIndex = this.keyFrames!.length - 2; - } else { - for (let i = 0; i < this.keyFrames!.length - 1; i++) { - if (wrappedTime >= this.keyFrames![0].time && wrappedTime <= this.keyFrames![i + 1].time) { - preKFIndex = i; - break; - } - } - } - } - const keyframe0 = this.keyFrames![preKFIndex]; - const keyframe1 = this.keyFrames![preKFIndex + 1]; - - const t = inverseLerp(keyframe0.time, keyframe1.time, wrappedTime); - const dt = keyframe1.time - keyframe0.time; - - const m0 = keyframe0.outTangent * dt; - const m1 = keyframe1.inTangent * dt; - - const t2 = t * t; - const t3 = t2 * t; - - const a = 2 * t3 - 3 * t2 + 1; - const b = t3 - 2 * t2 + t; - const c = t3 - t2; - const d = -2 * t3 + 3 * t2; - - return a * keyframe0.value + b * m0 + c * m1 + d * keyframe1.value; + return this._curve.evaluate(time); } /** @@ -238,33 +252,32 @@ export class AnimationCurve { * @param time 时间。 */ public evaluate (time: number) { + const { cachedKey, _curve: curve } = this; + const nKeyframes = curve.keyFramesCount; + const lastKeyframeIndex = nKeyframes - 1; let wrappedTime = time; - const wrapMode = time < 0 ? this.preWrapMode : this.postWrapMode; - const startTime = this.keyFrames![0].time; - const endTime = this.keyFrames![this.keyFrames!.length - 1].time; - switch (wrapMode) { - case WrapMode.Loop: + const extrapMode = time < 0 ? curve.preExtrap : curve.postExtrap; + const startTime = curve.getKeyframeTime(0); + const endTime = curve.getKeyframeTime(lastKeyframeIndex); + switch (extrapMode) { + case ExtrapMode.REPEAT: wrappedTime = repeat(time - startTime, endTime - startTime) + startTime; break; - case WrapMode.PingPong: + case ExtrapMode.PING_PONG: wrappedTime = pingPong(time - startTime, endTime - startTime) + startTime; break; - case WrapMode.Default: - case WrapMode.Normal: - case WrapMode.Clamp: - wrappedTime = clamp(time, startTime, endTime); - break; + case ExtrapMode.CLAMP: default: wrappedTime = clamp(time, startTime, endTime); break; } - if (wrappedTime >= this.cachedKey.time && wrappedTime < this.cachedKey.endTime) { - return this.cachedKey.evaluate(wrappedTime); + if (wrappedTime >= cachedKey.time && wrappedTime < cachedKey.endTime) { + return cachedKey.evaluate(wrappedTime); } - const leftIndex = this.findIndex(this.cachedKey, wrappedTime); - const rightIndex = Math.min(leftIndex + 1, this.keyFrames!.length - 1); - this.calcOptimizedKey(this.cachedKey, leftIndex, rightIndex); - return this.cachedKey.evaluate(wrappedTime); + const leftIndex = this.findIndex(cachedKey, wrappedTime); + const rightIndex = Math.min(leftIndex + 1, lastKeyframeIndex); + this.calcOptimizedKey(cachedKey, leftIndex, rightIndex); + return cachedKey.evaluate(wrappedTime); } /** @@ -274,22 +287,24 @@ export class AnimationCurve { * @param rightIndex */ public calcOptimizedKey (optKey: OptimizedKey, leftIndex: number, rightIndex: number) { - const lhs = this.keyFrames![leftIndex]; - const rhs = this.keyFrames![rightIndex]; + const lhsTime = this._curve.getKeyframeTime(leftIndex); + const rhsTime = this._curve.getKeyframeTime(rightIndex); + const { value: lhsValue, endTangent: lhsOutTangent } = this._curve.getKeyframeValue(leftIndex); + const { value: rhsValue, startTangent: rhsInTangent } = this._curve.getKeyframeValue(rightIndex); optKey.index = leftIndex; - optKey.time = lhs.time; - optKey.endTime = rhs.time; + optKey.time = lhsTime; + optKey.endTime = rhsTime; - const dx = rhs.time - lhs.time; - const dy = rhs.value - lhs.value; + const dx = rhsTime - lhsTime; + const dy = rhsValue - lhsValue; const length = 1 / (dx * dx); - const d1 = lhs.outTangent * dx; - const d2 = rhs.inTangent * dx; + const d1 = lhsOutTangent * dx; + const d2 = rhsInTangent * dx; optKey.coefficient[0] = (d1 + d2 - dy - dy) * length / dx; optKey.coefficient[1] = (dy + dy + dy - d1 - d1 - d2) * length; - optKey.coefficient[2] = lhs.outTangent; - optKey.coefficient[3] = lhs.value; + optKey.coefficient[2] = lhsOutTangent; + optKey.coefficient[3] = lhsValue; } /** @@ -298,31 +313,33 @@ export class AnimationCurve { * @param t */ private findIndex (optKey: OptimizedKey, t: number) { + const { _curve: curve } = this; + const nKeyframes = curve.keyFramesCount; const cachedIndex = optKey.index; if (cachedIndex !== -1) { - const cachedTime = this.keyFrames![cachedIndex].time; + const cachedTime = curve.getKeyframeTime(cachedIndex); if (t > cachedTime) { for (let i = 0; i < LOOK_FORWARD; i++) { const currIndex = cachedIndex + i; - if (currIndex + 1 < this.keyFrames!.length && this.keyFrames![currIndex + 1].time > t) { + if (currIndex + 1 < nKeyframes && curve.getKeyframeTime(currIndex + 1) > t) { return currIndex; } } } else { for (let i = 0; i < LOOK_FORWARD; i++) { const currIndex = cachedIndex - i; - if (currIndex >= 0 && this.keyFrames![currIndex - 1].time <= t) { + if (currIndex >= 0 && curve.getKeyframeTime(currIndex - 1) <= t) { return currIndex - 1; } } } } let left = 0; - let right = this.keyFrames!.length; + let right = nKeyframes; let mid; while (right - left > 1) { mid = Math.floor((left + right) / 2); - if (this.keyFrames![mid].time >= t) { + if (curve.getKeyframeTime(mid) >= t) { right = mid; } else { left = mid; @@ -332,8 +349,35 @@ export class AnimationCurve { } } -CCClass.fastDefine('cc.AnimationCurve', AnimationCurve, { - preWrapMode: WrapMode.Default, - postWrapMode: WrapMode.Default, - keyFrames: [], -}); +function fromLegacyWrapMode (legacyWrapMode: WrapModeMask): ExtrapMode { + switch (legacyWrapMode) { + default: + case WrapModeMask.Default: + case WrapModeMask.Normal: + case WrapModeMask.Clamp: return ExtrapMode.CLAMP; + case WrapModeMask.PingPong: return ExtrapMode.PING_PONG; + case WrapModeMask.Loop: return ExtrapMode.REPEAT; + } +} + +function toLegacyWrapMode (extrapMode: ExtrapMode): WrapModeMask { + switch (extrapMode) { + default: + case ExtrapMode.LINEAR: + case ExtrapMode.CLAMP: return WrapModeMask.Clamp; + case ExtrapMode.PING_PONG: return WrapModeMask.PingPong; + case ExtrapMode.REPEAT: return WrapModeMask.Loop; + } +} + +/** + * Same as but more effective than `new LegacyCurve()._internalCurve`. + */ +export function constructLegacyCurveAndConvert () { + const curve = new RealCurve(); + curve.assignSorted([ + [0.0, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 1.0 })], + [1.0, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 1.0 })], + ]); + return curve; +} diff --git a/cocos/core/index.ts b/cocos/core/index.ts index 5dcf81c9495..1637201e7aa 100644 --- a/cocos/core/index.ts +++ b/cocos/core/index.ts @@ -61,3 +61,4 @@ export * from './scene-graph'; export * from './components'; export * from './builtin'; export * from './animation'; +export * from './curves'; diff --git a/cocos/particle/animator/curve-range.ts b/cocos/particle/animator/curve-range.ts index 471ff07ca2c..b4266a46698 100644 --- a/cocos/particle/animator/curve-range.ts +++ b/cocos/particle/animator/curve-range.ts @@ -28,18 +28,18 @@ * @hidden */ -import { ccclass, type, serializable, editable } from 'cc.decorator'; +import { ccclass, type, serializable, editable, formerlySerializedAs } from 'cc.decorator'; import { EDITOR } from 'internal:constants'; import { lerp } from '../../core/math'; import { Enum } from '../../core/value-types'; -import { AnimationCurve } from '../../core/geometry'; -import { Texture2D, ImageAsset } from '../../core'; +import { AnimationCurve, constructLegacyCurveAndConvert } from '../../core/geometry/curve'; +import { Texture2D, ImageAsset, RealCurve } from '../../core'; import { PixelFormat, Filter, WrapMode } from '../../core/assets/asset-enum'; -const SerializableTable = EDITOR && [ +const SerializableTable = [ ['mode', 'constant', 'multiplier'], - ['mode', 'curve', 'multiplier'], - ['mode', 'curveMin', 'curveMax', 'multiplier'], + ['mode', 'value', 'multiplier'], + ['mode', 'min', 'max', 'multiplier'], ['mode', 'constantMin', 'constantMax', 'multiplier'], ]; @@ -63,20 +63,59 @@ export default class CurveRange { /** * @zh 当mode为Curve时,使用的曲线。 */ - @type(AnimationCurve) - public curve = new AnimationCurve(); + @type(RealCurve) + public value = constructLegacyCurveAndConvert(); /** * @zh 当mode为TwoCurves时,使用的曲线下限。 */ - @type(AnimationCurve) - public curveMin = new AnimationCurve(); + @type(RealCurve) + public min = constructLegacyCurveAndConvert(); /** * @zh 当mode为TwoCurves时,使用的曲线上限。 */ - @type(AnimationCurve) - public curveMax = new AnimationCurve(); + @type(RealCurve) + public max = constructLegacyCurveAndConvert(); + + /** + * @zh 当mode为Curve时,使用的曲线。 + * @deprecated Since V3.2. Use `value` instead. + */ + get curve () { + return this._curve; + } + + set curve (value) { + this._curve = value; + this.value = value._internalCurve; + } + + /** + * @zh 当mode为TwoCurves时,使用的曲线下限。 + * @deprecated Since V3.2. Use `min` instead. + */ + get curveMin () { + return this._curveMin; + } + + set curveMin (value) { + this._curveMin = value; + this.min = value._internalCurve; + } + + /** + * @zh 当mode为TwoCurves时,使用的曲线上限。 + * @deprecated Since V3.2. Use `max` instead. + */ + get curveMax () { + return this._curveMax; + } + + set curveMax (value) { + this._curveMax = value; + this.max = value._internalCurve; + } /** * @zh 当mode为Constant时,曲线的值。 @@ -112,12 +151,13 @@ export default class CurveRange { public evaluate (time: number, rndRatio: number) { switch (this.mode) { + default: case Mode.Constant: return this.constant; case Mode.Curve: - return this.curve.evaluate(time) * this.multiplier; + return this.value.evaluate(time) * this.multiplier; case Mode.TwoCurves: - return lerp(this.curveMin.evaluate(time), this.curveMax.evaluate(time), rndRatio) * this.multiplier; + return lerp(this.min.evaluate(time), this.max.evaluate(time), rndRatio) * this.multiplier; case Mode.TwoConstants: return lerp(this.constantMin, this.constantMax, rndRatio); } @@ -125,6 +165,7 @@ export default class CurveRange { public getMax (): number { switch (this.mode) { + default: case Mode.Constant: return this.constant; case Mode.Curve: @@ -134,12 +175,16 @@ export default class CurveRange { case Mode.TwoCurves: return this.multiplier; } - return 0; } public _onBeforeSerialize (props) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return SerializableTable[this.mode]; } + + private _curve = new AnimationCurve(this.value); + private _curveMin = new AnimationCurve(this.min); + private _curveMax = new AnimationCurve(this.max); } function evaluateCurve (cr: CurveRange, time: number, index: number) { @@ -147,9 +192,9 @@ function evaluateCurve (cr: CurveRange, time: number, index: number) { case Mode.Constant: return cr.constant; case Mode.Curve: - return cr.curve.evaluate(time) * cr.multiplier; + return cr.value.evaluate(time) * cr.multiplier; case Mode.TwoCurves: - return index === 0 ? cr.curveMin.evaluate(time) * cr.multiplier : cr.curveMax.evaluate(time) * cr.multiplier; + return index === 0 ? cr.min.evaluate(time) * cr.multiplier : cr.max.evaluate(time) * cr.multiplier; case Mode.TwoConstants: return index === 0 ? cr.constantMin : cr.constantMax; default: diff --git a/tests/animation/animaion-clip-migration-3.x.test.ts b/tests/animation/animaion-clip-migration-3.x.test.ts new file mode 100644 index 00000000000..d2224403164 --- /dev/null +++ b/tests/animation/animaion-clip-migration-3.x.test.ts @@ -0,0 +1,246 @@ +import { math, RealInterpMode } from "../../cocos/core"; +import { AnimationClip, animation, BezierControlPoints, bezierByTime } from "../../cocos/core/animation"; +import { timeBezierToTangents } from "../../cocos/core/animation/legacy-clip-data"; +import { RealCurve, RealKeyframeValue, TangentWeightMode } from "../../cocos/core/curves/curve"; + + +describe('Animation Clip Migration 3.x', () => { + test('Numeric curves', () => { + const clip = new AnimationClip(); + clip.keys = [ + [0.0, 0.2, 0.8], + ]; + clip.curves = [ + { + modifiers: ['p'], + data: { + keys: 0, + values: [3.14, 6.18, 8.9], + }, + }, + ] + clip.syncLegacyData(); + expect(clip.tracksCount).toBe(1); + const track = clip.getTrack(0) as animation.RealTrack; + expect(track).toBeInstanceOf(animation.RealTrack); + const curve = track.channel.curve; + expect(Array.from(curve.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(curve.values())).toStrictEqual( + createRealKeyframesWithoutTangent([3.14, 6.18, 8.9], RealInterpMode.LINEAR), + ); + }); + + test('Vec2 curves', () => { + const clip = new AnimationClip(); + clip.keys = [[0.0, 0.2, 0.8]]; + clip.curves = [{ + modifiers: ['p'], + data: { + keys: 0, + values: [ + new math.Vec2(1.0, 4.0), + new math.Vec2(2.0, 5.0), + new math.Vec2(3.0, 6.0), + ], + }, + }]; + clip.syncLegacyData(); + expect(clip.tracksCount).toBe(1); + const track = clip.getTrack(0) as animation.VectorTrack; + expect(track).toBeInstanceOf(animation.VectorTrack); + expect(track.componentsCount).toBe(2); + const [{ curve: x }, { curve: y }] = track.getChannels(); + expect(Array.from(x.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(x.values())).toStrictEqual( + createRealKeyframesWithoutTangent([1.0, 2.0, 3.0], RealInterpMode.LINEAR), + ); + expect(Array.from(y.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(y.values())).toStrictEqual( + createRealKeyframesWithoutTangent([4.0, 5.0, 6.0], RealInterpMode.LINEAR), + ); + + }); + + test('Vec3 curves', () => { + const clip = new AnimationClip(); + clip.keys = [[0.0, 0.2, 0.8]]; + clip.curves = [{ + modifiers: ['p'], + data: { + keys: 0, + values: [ + new math.Vec3(1.0, 4.0, 7.0), + new math.Vec3(2.0, 5.0, 8.0), + new math.Vec3(3.0, 6.0, 9.0), + ], + }, + }]; + clip.syncLegacyData(); + expect(clip.tracksCount).toBe(1); + const track = clip.getTrack(0) as animation.VectorTrack; + expect(track).toBeInstanceOf(animation.VectorTrack); + expect(track.componentsCount).toBe(3); + const [{ curve: x }, { curve: y }, { curve: z }] = track.getChannels(); + expect(Array.from(x.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(x.values())).toStrictEqual( + createRealKeyframesWithoutTangent([1.0, 2.0, 3.0], RealInterpMode.LINEAR), + ); + expect(Array.from(y.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(y.values())).toStrictEqual( + createRealKeyframesWithoutTangent([4.0, 5.0, 6.0], RealInterpMode.LINEAR), + ); + expect(Array.from(z.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(z.values())).toStrictEqual( + createRealKeyframesWithoutTangent([7.0, 8.0, 9.0], RealInterpMode.LINEAR), + ); + }); + + test('Vec4 curves', () => { + const clip = new AnimationClip(); + clip.keys = [[0.0, 0.2, 0.8]]; + clip.curves = [{ + modifiers: ['p'], + data: { + keys: 0, + values: [ + new math.Vec4(1.0, 4.0, 7.0, 10.0), + new math.Vec4(2.0, 5.0, 8.0, 11.0), + new math.Vec4(3.0, 6.0, 9.0, 12.0), + ], + }, + }]; + clip.syncLegacyData(); + expect(clip.tracksCount).toBe(1); + const track = clip.getTrack(0) as animation.VectorTrack; + expect(track).toBeInstanceOf(animation.VectorTrack); + expect(track.componentsCount).toBe(4); + const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.getChannels(); + expect(Array.from(x.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(x.values())).toStrictEqual( + createRealKeyframesWithoutTangent([1.0, 2.0, 3.0], RealInterpMode.LINEAR), + ); + expect(Array.from(y.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(y.values())).toStrictEqual( + createRealKeyframesWithoutTangent([4.0, 5.0, 6.0], RealInterpMode.LINEAR), + ); + expect(Array.from(z.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(z.values())).toStrictEqual( + createRealKeyframesWithoutTangent([7.0, 8.0, 9.0], RealInterpMode.LINEAR), + ); + expect(Array.from(w.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(w.values())).toStrictEqual( + createRealKeyframesWithoutTangent([10.0, 11.0, 12.0], RealInterpMode.LINEAR), + ); + }); + + test('Color curves', () => { + const clip = new AnimationClip(); + clip.keys = [[0.0, 0.2, 0.8]]; + clip.curves = [{ + modifiers: ['p'], + data: { + keys: 0, + values: [ + new math.Color(10, 40, 70, 100), + new math.Color(20, 50, 80, 110), + new math.Color(30, 60, 90, 120), + ], + }, + }]; + clip.syncLegacyData(); + expect(clip.tracksCount).toBe(1); + const track = clip.getTrack(0) as animation.ColorTrack; + expect(track).toBeInstanceOf(animation.ColorTrack); + const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.getChannels(); + expect(Array.from(r.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(r.values())).toStrictEqual( + createRealKeyframesWithoutTangent([10, 20, 30], RealInterpMode.LINEAR), + ); + expect(Array.from(g.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(g.values())).toStrictEqual( + createRealKeyframesWithoutTangent([40, 50, 60], RealInterpMode.LINEAR), + ); + expect(Array.from(b.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(b.values())).toStrictEqual( + createRealKeyframesWithoutTangent([70, 80, 90], RealInterpMode.LINEAR), + ); + expect(Array.from(a.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(a.values())).toStrictEqual( + createRealKeyframesWithoutTangent([100, 110, 120], RealInterpMode.LINEAR), + ); + }); + + test('Common target: Color', () => { + + }); + + test('Common target: Color with components as floats', () => { + + }); + + test('Time bezier to tangent', () => { + testTimeBezierCurveConversion({ + // t0: 0.0, + // t1: 1.0, + // v0: 0.0, + // v1: 1.0, + t0: 0.08333333333333333, + v0: 3, + t1: 0.18333333333333332, + v1: 5, + bezierPoints: [.04,.94,.53,.63], + }); + + type TimeBezierTestCase = { + t0: number; + t1: number; + v0: number; + v1: number; + bezierPoints: BezierControlPoints; + }; + + function testTimeBezierCurveConversion (testCase: TimeBezierTestCase) { + const [endTangent, endTangentWeight, startTangent, startTangentWeight] = timeBezierToTangents( + testCase.bezierPoints, + testCase.t0, + testCase.v0, + testCase.t1, + testCase.v1, + ); + const curve = new RealCurve(); + curve.assignSorted([ + [testCase.t0, new RealKeyframeValue({ + value: testCase.v0, + endTangent, + endTangentWeight, + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.END, + })], + [testCase.t1, new RealKeyframeValue({ + value: testCase.v1, + startTangent, + startTangentWeight, + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.START, + })], + ]); + + for (let inputRatio = 0.0; inputRatio <= 1.0; inputRatio += 0.01) { + const ratio = bezierByTime(testCase.bezierPoints, inputRatio); + const timeBezierResult = testCase.v0 + (testCase.v1 - testCase.v0) * ratio; + const inputTime = testCase.t0 + inputRatio * (testCase.t1 - testCase.t0); + const curveResult = curve.evaluate(inputTime); + expect(timeBezierResult).toBeCloseTo(curveResult, 2); + } + } + }); +}); + +function createRealKeyframesWithoutTangent (values: number[], interpMode: RealInterpMode): RealKeyframeValue[] { + return values.map((value) => { + return new RealKeyframeValue({ + value, + interpMode, + }); + }); +} \ No newline at end of file diff --git a/tests/animation/animation-clip-3.x.test.ts b/tests/animation/animation-clip-3.x.test.ts new file mode 100644 index 00000000000..83b6dba78d1 --- /dev/null +++ b/tests/animation/animation-clip-3.x.test.ts @@ -0,0 +1,125 @@ +import { Mat4, Node, RealKeyframeValue, Vec3 } from '../../cocos/core'; +import { AnimationClip, RealTrack, searchForRootBonePathSymbol, VectorTrack } from '../../cocos/core/animation/animation-clip'; +import { ComponentPath, HierarchyPath, TargetPath } from '../../cocos/core/animation/target-path'; + +describe('Animation Clip', () => { + describe('Evaluation', () => { + describe('Ill-formed track path', () => { + // test('Absent component', () => { + // const errorMock = jest.spyOn(console, "log").mockImplementation(() => {}); + // const clip = createClipWithPath([new ComponentPath('cc.MeshRenderer'), 'mesh']); + // clip.createEvaluator({ target: new Node() }); + // expect(errorMock).toBeCalledTimes(1); + // expect(errorMock.mock.calls[0][0]).toMatch(/ill-formed/i); + // errorMock.mockRestore(); + // }); + + // test('Absent hierarchy', () => { + // const errorMock = jest.spyOn(console, "log").mockImplementation(() => {}); + // const clip = createClipWithPath([new HierarchyPath('/absent'), 'position']); + // clip.createEvaluator({ target: new Node() }); + // expect(errorMock).toBeCalledTimes(1); + // expect(errorMock.mock.calls[0][0]).toMatch(/ill-formed/i); + // errorMock.mockRestore(); + // }); + + // test('Exception when bound', () => { + // const errorMock = jest.spyOn(console, "log").mockImplementation(() => {}); + // const clip = createClipWithPath(['property', 0]); + // clip.createEvaluator({ target: new Node() }); + // expect(errorMock).toBeCalledTimes(1); + // expect(errorMock.mock.calls[0][0]).toMatch(/ill-formed/i); + // errorMock.mockRestore(); + // }); + + function createClipWithPath (path: TargetPath[]) { + const clip = new AnimationClip(); + const track = new RealTrack(); + track.path = [new HierarchyPath('Foo')]; + clip.addTrack(track); + return clip; + } + }); + + test('Root bone search', () => { + function testWith(paths: string[]) { + const clip = new AnimationClip(); + for (const path of paths) { + const track = new VectorTrack(); + track.path = [new HierarchyPath(path), 'position']; + clip.addTrack(track); + } + return clip[searchForRootBonePathSymbol](); + } + + expect(testWith([''])).toBe(''); + expect(testWith(['Root'])).toBe('Root'); + expect(testWith(['Root/Pelvis'])).toBe('Root/Pelvis'); + expect(testWith(['Root', 'Root/Pelvis'])).toBe('Root'); + expect(testWith(['Root', 'Root/Pelvis/Head'])).toBe('Root'); + expect(testWith(['Pelvis/Left Leg', 'Pelvis/Right Leg'])).toBe(''); + }); + + describe('Root motion', () => { + const clip = new AnimationClip(); + clip.duration = 1.0; + + const rootJointName = 'RootJoint'; + + const rootBoneTranslationTrack = new VectorTrack(); + { + rootBoneTranslationTrack.componentsCount = 3; + rootBoneTranslationTrack.path = [new HierarchyPath(rootJointName), 'position']; + const [x, _y, _z] = rootBoneTranslationTrack.getChannels(); + x.curve.assignSorted([ + [0.4, new RealKeyframeValue({ value: 0.4 })], + [0.6, new RealKeyframeValue({ value: 0.6 })], + [0.8, new RealKeyframeValue({ value: 0.8 })], + ]); + } + + clip.addTrack(rootBoneTranslationTrack); + + const dummyRootNode = new Node(); + + const dummyRootJointNode = new Node(rootJointName); + dummyRootNode.addChild(dummyRootJointNode); + + const evaluation = clip.createEvaluator({ + target: dummyRootNode, + pose: undefined, + rootMotion: { }, + }); + + test('Never touch root joint', () => { + dummyRootJointNode.setPosition(0.0, 0.0, 0.0); + evaluation.evaluate(0.1); + expect(Vec3.equals(dummyRootNode.position, new Vec3(0.0, 0.0, 0.0))).toBe(true); + }); + + test('In same duration: Motion not changed', () => { + dummyRootJointNode.setPosition(0.0, 0.0, 0.0); + evaluation.evaluateRootMotion(0.2, 0.1); + expect(Vec3.equals(dummyRootJointNode.position, new Vec3())).toBe(true); + }); + + test('In same duration: Motion changed ', () => { + dummyRootJointNode.setPosition(0.0, 0.0, 0.0); + evaluation.evaluateRootMotion(0.5, 0.2); + expect(Vec3.equals(dummyRootJointNode.position, new Vec3(0.2))).toBe(true); + }); + + test('Motion extended to next duration ', () => { + dummyRootJointNode.setPosition(0.0, 0.0, 0.0); + evaluation.evaluateRootMotion(0.5, 0.7); + expect(Vec3.equals(dummyRootJointNode.position, new Vec3(0.3))).toBe(true); + }); + + test('Motion extended to next multiple duration ', () => { + dummyRootJointNode.setPosition(0.0, 0.0, 0.0); + evaluation.evaluateRootMotion(0.5, 3.2); + expect(Vec3.equals(dummyRootJointNode.position, new Vec3(1.4))).toBe(true); + }); + }); + }); +}); \ No newline at end of file diff --git a/tests/animation/animation-clip.test.ts b/tests/animation/animation-clip.test.ts index a1b082f0af3..a4481e02463 100644 --- a/tests/animation/animation-clip.test.ts +++ b/tests/animation/animation-clip.test.ts @@ -1,6 +1,6 @@ -import { js, Node, Component, Vec3 } from '../../cocos/core'; +import { js, Node, Component, Vec3, RealKeyframeValue } from '../../cocos/core'; import { AnimationClip, AnimationState, AnimationManager } from '../../cocos/core/animation'; -import { ComponentPath, HierarchyPath } from '../../cocos/core/animation/animation'; +import { ComponentPath, HierarchyPath, IValueProxyFactory, VectorTrack } from '../../cocos/core/animation/animation'; import { ccclass } from 'cc.decorator'; test('Common target', () => { @@ -115,6 +115,84 @@ test('Common targets that modify eulerAngles', () => { mockInstance.mockRestore(); }); +describe('Custom track setter', () => { + class Target { + public setValue(value: Target['_value']) { Vec3.copy(this._value, value); } + public getValue() { return this._value; } + private _value = { x: 0, y: 0, z: 0 }; + } + + const valueProxyWithOnlySet: IValueProxyFactory = { + forTarget: (target: Target) => { + return { + set: (value: Target['_value']) => { target.setValue(value) }, + }; + }, + }; + + const valueProxyWithGetSet: IValueProxyFactory = { + forTarget: (target: Target) => { + return { + set: (value: Target['_value']) => { target.setValue(value) }, + get: () => target.getValue(), + }; + }, + }; + + test('get() got not called if non of channels is empty', () => { + const target = new Target(); + const mockGetValue = target.getValue = jest.fn(target.getValue); + const mockSetValue = target.setValue = jest.fn(target.setValue); + + const track = new VectorTrack(); + track.setter = valueProxyWithGetSet; + track.getChannels().forEach(({ curve }) => { + curve.assignSorted([[0.0, new RealKeyframeValue({ value: 0.0 })]]); + }); + + const clip = new AnimationClip(); + clip.addTrack(track); + const clipEval = clip.createEvaluator({ + target, + }); + clipEval.evaluate(0.0); + expect(mockGetValue).not.toBeCalled(); + expect(mockSetValue).toBeCalled(); + }); + + test('get() got called if any of channels is empty', () => { + const target = new Target(); + const mockGetValue = target.getValue = jest.fn(target.getValue); + const mockSetValue = target.setValue = jest.fn(target.setValue); + + const track = new VectorTrack(); + track.setter = valueProxyWithGetSet; + const clip = new AnimationClip(); + clip.addTrack(track); + const clipEval = clip.createEvaluator({ + target, + }); + clipEval.evaluate(0.0); + expect(mockGetValue).toBeCalled(); + expect(mockSetValue).toBeCalled(); + }); + + test('If get() is not defined, the default channel value would be used', () => { + const target = new Target(); + const mockSetValue = target.setValue = jest.fn(target.setValue); + + const track = new VectorTrack(); + track.setter = valueProxyWithOnlySet; + const clip = new AnimationClip(); + clip.addTrack(track); + const clipEval = clip.createEvaluator({ + target, + }); + clipEval.evaluate(0.0); + expect(mockSetValue).toBeCalled(); + }); +}); + test('animation state', () => { const animationManager = new AnimationManager(); const mockInstance = jest.spyOn((global as any).cc.director, 'getAnimationManager').mockImplementation(() => { diff --git a/tests/animation/animation-curve.test.ts b/tests/animation/animation-curve.test.ts deleted file mode 100644 index 241bd41a760..00000000000 --- a/tests/animation/animation-curve.test.ts +++ /dev/null @@ -1,26 +0,0 @@ - -import { AnimCurve, RatioSampler, sampleAnimationCurve } from '../../cocos/core/animation/animation-curve'; -import { ComponentPath, TargetPath, HierarchyPath } from '../../cocos/core/animation/target-path'; -import { Node } from '../../cocos/core/scene-graph'; -import { createBoundTarget } from '../../cocos/core/animation/bound-target'; - -test('sample from animation curve', () => { - const curve = new AnimCurve({ - values: [0, 1, 2, 3], - }, 1); - const sampler = new RatioSampler([ - 0, 0.3, 0.6, 1.0 - ]); - - expect(sampleAnimationCurve(curve, sampler, 0)).toBe(0); - expect(sampleAnimationCurve(curve, sampler, 0.3)).toBe(1); - expect(sampleAnimationCurve(curve, sampler, 0.6)).toBe(2); - expect(sampleAnimationCurve(curve, sampler, 1.0)).toBe(3); -}); - -test('Erroneous target path', () => {; - expect(createBoundTarget(new Node("TestNode"), [new ComponentPath('cc.MeshRenderer'), 'position'])).toBeNull(); - expect(createBoundTarget(new Node("TestNode"), [new HierarchyPath('/absent'), 'position'])).toBeNull(); - expect(createBoundTarget(Object.create(null), ['property', 0])).toBeNull(); - expect(createBoundTarget([], [1, 0])).toBeNull(); -}); \ No newline at end of file diff --git a/tests/animation/compression.test.ts b/tests/animation/compression.test.ts new file mode 100644 index 00000000000..4f8d78c07b5 --- /dev/null +++ b/tests/animation/compression.test.ts @@ -0,0 +1,89 @@ + +import { removeLinearKeys, removeTrivialKeys } from '../../cocos/core/animation/compression'; + +describe('Curve compression', () => { + describe('Remove linear keys', () => { + test('Curves with only zero/one/two keys are not effected', () => { + expect(runRemoveLinearKeys([], [])).toBeRemovedWith([]); + expect(runRemoveLinearKeys([0.3], [0.4])).toBeRemovedWith([]); + expect(runRemoveLinearKeys([0.3, 0.4], [4.0, 5.0])).toBeRemovedWith([]); + }); + + test('Linear', () => { + expect(runRemoveLinearKeys([0.3, 0.4, 0.5], [3.0, 4.0, 5.0])).toBeRemovedWith([1]); + expect(runRemoveLinearKeys([0.3, 0.4, 0.5, 0.7], [3.0, 4.0, 5.0, 10.0])).toBeRemovedWith([1]); + }); + + test('Successive linear', () => { + expect(runRemoveLinearKeys([0.3, 0.4, 0.5, 0.6], [3.0, 4.0, 5.0, 6.0])).toBeRemovedWith([1, 2]); + expect(runRemoveLinearKeys([0.3, 0.4, 0.5, 0.6, 0.7], [3.0, 4.0, 5.0, 6.0, 10.0])).toBeRemovedWith([1, 2]); + }); + + test('Max error', () => { + expect(runRemoveLinearKeys([0.3, 0.4, 0.5], [3.0, 4.0002, 5.0], 1e-3)).toBeRemovedWith([1]); + expect(runRemoveLinearKeys([0.3, 0.4, 0.5], [3.0, 4.002, 5.0], 1e-3)).toBeRemovedWith([]); + }); + }); + + describe('Remove trivial keys', () => { + test('Zero/One length', () => { + expect(runRemoveTrivialKeys([], [])).toBeRemovedWith([]); + expect(runRemoveTrivialKeys([0.3], [0.4])).toBeRemovedWith([]); + }); + }); +}); + +type CompressionResult = ReturnType; + +function runCompression( + keys: number[], + values: number[], + fn: (keys: number[], values: number[], ...args: TArgs) => { keys: number[], values: number[] }, + ...args: TArgs +) { + const { keys: newKeys, values: newValues } = fn(keys, values, ...args); + return { + keys, values, + newKeys, newValues, + }; +} + +function runRemoveLinearKeys (keys: number[], values: number[], maxError?: number) { + return runCompression( + keys, + values, + removeLinearKeys, + maxError, + ); +} + +function runRemoveTrivialKeys (keys: number[], values: number[], maxError?: number) { + return runCompression( + keys, + values, + removeTrivialKeys, + maxError, + ); +} + +expect.extend({ + toBeRemovedWith(received: CompressionResult, removals: number[]): jest.CustomMatcherResult { + const { keys, values, newKeys, newValues } = received; + const actualRemovals = keys.map((_, index) => index).filter((index) => !newKeys.includes(keys[index])); + return { + pass: actualRemovals.length === removals.length && + actualRemovals.every((actualRemoval, index) => actualRemoval === removals[index]), + message: () => + `Expected removals: ${removals.join(', ')}\n` + + `Actual removals: ${actualRemovals.join(', ')}`, + }; + }, +}); + +declare global { + namespace jest { + interface Matchers { + toBeRemovedWith(expected: number[]): CustomMatcherResult + } + } +} diff --git a/tests/core/geometry/geometry-curve.test.ts b/tests/core/geometry/geometry-curve.test.ts new file mode 100644 index 00000000000..adfd80227a2 --- /dev/null +++ b/tests/core/geometry/geometry-curve.test.ts @@ -0,0 +1,196 @@ +import { WrapModeMask } from '../../../cocos/core/animation/types'; +import { ExtrapMode, RealCurve, RealInterpMode, RealKeyframeValue, TangentWeightMode } from '../../../cocos/core/curves'; +import { AnimationCurve, Keyframe } from '../../../cocos/core/geometry/curve'; + +describe('geometry.AnimationCurve', () => { + describe('Constructor', () => { + test('new AnimationCurve()', () => { + const curve = new AnimationCurve(); + expect(curve.keyFrames).toStrictEqual([ + createLegacyKeyframe({ time: 0.0, value: 1.0 }), + createLegacyKeyframe({ time: 1.0, value: 1.0 }), + ] as Keyframe[]); + }); + + test('new AnimationCurve(keyframes)', () => { + const curve = new AnimationCurve([ + createLegacyKeyframe({ time: 2.0, value: 8.0, inTangent: -3.3, outTangent: 1.75 }), + createLegacyKeyframe({ time: 3.0, value: 9.0, inTangent: 4.2, outTangent: -7.1 }), + ]); + expect(curve.keyFrames).toStrictEqual([ + createLegacyKeyframe({ time: 2.0, value: 8.0, inTangent: -3.3, outTangent: 1.75 }), + createLegacyKeyframe({ time: 3.0, value: 9.0, inTangent: 4.2, outTangent: -7.1 }), + ] as Keyframe[]); + }); + + test('new AnimationCurve(realCurve)(INTERNAL)', () => { + const realCurve = new RealCurve(); + realCurve.assignSorted([ + // Non weighted tangent + [0.1, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + value: 0.1, + startTangent: 0.2, + endTangent: 0.3, + })], + // Non cubic keyframe + [0.2, new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, + value: 0.1, + })], + // Weighted tangent + [0.3, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + value: 0.1, + startTangent: 0.2, + endTangent: 0.3, + tangentWeightMode: TangentWeightMode.START, + startTangentWeight: 0.4, + endTangentWeight: 0.5, + })], + ]); + + const geometryCurve = new AnimationCurve(realCurve); + expect(geometryCurve.keyFrames).toStrictEqual([ + createLegacyKeyframe({ time: 0.1, value: 0.1, inTangent: 0.2, outTangent: 0.3 }), + createLegacyKeyframe({ time: 0.2, value: 0.1, inTangent: 0.0, outTangent: 0.0 }), + createLegacyKeyframe({ time: 0.3, value: 0.1, inTangent: 0.2, outTangent: 0.3 }), + ] as Keyframe[]); + }); + + test.each([ + { extrapMode: ExtrapMode.REPEAT, expected: WrapModeMask.Loop }, + { extrapMode: ExtrapMode.PING_PONG, expected: WrapModeMask.PingPong }, + { extrapMode: ExtrapMode.CLAMP, expected: WrapModeMask.Clamp }, + { extrapMode: ExtrapMode.LINEAR, expected: WrapModeMask.Clamp }, + ])(`new AnimationCurve(realCurve)(INTERNAL): conversion of extrapolation mode $extrapMode`, ({ extrapMode, expected }) => { + const realCurve = new RealCurve(); + realCurve.preExtrap = extrapMode; + realCurve.postExtrap = extrapMode; + const geometryCurve = new AnimationCurve(realCurve); + expect(geometryCurve.preWrapMode).toStrictEqual(expected); + expect(geometryCurve.postWrapMode).toStrictEqual(expected); + }); + }); + + test.each([ + { wrapMode: WrapModeMask.Clamp, extrapMode: ExtrapMode.CLAMP, }, + { wrapMode: WrapModeMask.Loop, extrapMode: ExtrapMode.REPEAT, }, + { wrapMode: WrapModeMask.PingPong, extrapMode: ExtrapMode.PING_PONG, }, + ])(`Wrap mode $wrapMode`, ({ wrapMode, extrapMode }) => { + const curve = new AnimationCurve(); + + curve.preWrapMode = wrapMode; + expect(curve.preWrapMode).toStrictEqual(wrapMode); + expect(curve._internalCurve.preExtrap).toStrictEqual(extrapMode); + + curve.postWrapMode = wrapMode; + expect(curve.postWrapMode).toStrictEqual(wrapMode); + expect(curve._internalCurve.postExtrap).toStrictEqual(extrapMode); + }); + + test(`Add key`, () => { + const curve = new AnimationCurve(); + + // Clear + curve.addKey(null); + expect(curve.keyFrames).toStrictEqual([]); + + curve.addKey(createLegacyKeyframe({ + time: 0.1, + value: 0.2, + inTangent: 0.3, + outTangent: 0.4, + })); + expect(curve.keyFrames).toStrictEqual([createLegacyKeyframe({ + time: 0.1, + value: 0.2, + inTangent: 0.3, + outTangent: 0.4, + })]); + + // Clear again + curve.addKey(null); + expect(curve.keyFrames).toStrictEqual([]); + }); + + test('Keyframes', () => { + const curve = new AnimationCurve(); + + curve.keyFrames = [createLegacyKeyframe({ + time: 0.1, + value: 0.2, + inTangent: 0.3, + outTangent: 0.4, + }), createLegacyKeyframe({ + time: 0.5, + value: 0.6, + inTangent: 0.7, + outTangent: 0.8, + })]; + + expect(curve.keyFrames).toStrictEqual([createLegacyKeyframe({ + time: 0.1, + value: 0.2, + inTangent: 0.3, + outTangent: 0.4, + }), createLegacyKeyframe({ + time: 0.5, + value: 0.6, + inTangent: 0.7, + outTangent: 0.8, + })]); + + curve._internalCurve.clear(); + expect(curve.keyFrames).toStrictEqual([]); + + curve._internalCurve.assignSorted([ + // Non weighted tangent + [0.1, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + value: 0.1, + startTangent: 0.2, + endTangent: 0.3, + })], + // Non cubic keyframe + [0.2, new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, + value: 0.1, + })], + // Weighted tangent + [0.3, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + value: 0.1, + startTangent: 0.2, + endTangent: 0.3, + tangentWeightMode: TangentWeightMode.START, + startTangentWeight: 0.4, + endTangentWeight: 0.5, + })], + ]); + expect(curve.keyFrames).toStrictEqual([ + createLegacyKeyframe({ time: 0.1, value: 0.1, inTangent: 0.2, outTangent: 0.3 }), + createLegacyKeyframe({ time: 0.2, value: 0.1, inTangent: 0.0, outTangent: 0.0 }), + createLegacyKeyframe({ time: 0.3, value: 0.1, inTangent: 0.2, outTangent: 0.3 }), + ] as Keyframe[]); + }); +}); + +function createLegacyKeyframe ({ + time, + value, + inTangent = 0.0, + outTangent = 0.0, +}: { + time: number, + value: number; + inTangent?: number; + outTangent?: number; +}) { + const keyFrame = new Keyframe(); + keyFrame.time = time; + keyFrame.value = value; + keyFrame.inTangent = inTangent; + keyFrame.outTangent = outTangent; + return keyFrame; +} diff --git a/tests/curves/curve.test.ts b/tests/curves/curve.test.ts new file mode 100644 index 00000000000..7a5f46c6818 --- /dev/null +++ b/tests/curves/curve.test.ts @@ -0,0 +1,275 @@ +import { toRadian } from '../../cocos/core'; +import { RealCurve, RealInterpMode } from '../../cocos/core/curves'; +import { RealKeyframeValue } from '../../cocos/core/curves/curve'; +import { ExtrapMode, TangentWeightMode } from '../../cocos/core/curves/real-curve-param'; +import { deserializeSymbol, serializeSymbol } from '../../cocos/core/data/serialization-symbols'; + +describe('Curve', () => { + test('assign sorted', () => { + const curve = new RealCurve(); + curve.assignSorted([0.1, 0.2, 0.3], [ + realKeyframeWithoutTangent(0.4), + realKeyframeWithoutTangent(0.5), + realKeyframeWithoutTangent(0.6), + ]); + + // Assign empty(keys, values) + curve.assignSorted([], []); + expect(curve.keyFramesCount).toBe(0); + + // Assign empty(frames) + curve.assignSorted([]); + expect(curve.keyFramesCount).toBe(0); + + // The count of keys and values should be same, if not, the behavior is undefined. + // In test mode, assertion error would be thrown. + expect(() => curve.assignSorted([0.1, 0.2], [])).toThrow(); + + // Keys should be sorted, if not, the behavior is undefined. + // In test mode, assertion error would be thrown. + expect(() => curve.assignSorted( + [0.2, 0.1], + [realKeyframeWithoutTangent(0.4), realKeyframeWithoutTangent(0.5)], + )).toThrow(); + expect(() => curve.assignSorted([ + [0.2, realKeyframeWithoutTangent(0.4)], + [0.1, realKeyframeWithoutTangent(0.5)], + ])).toThrow(); + }); + + describe('serialization', () => { + test('Normal', () => { + const curve = new RealCurve(); + curve.assignSorted([0.1, 0.2, 0.3], [ + new RealKeyframeValue({ value: 0.4, startTangent: 0.0, endTangent: 0.0, interpMode: RealInterpMode.CONSTANT }), + new RealKeyframeValue({ value: 0.5, startTangent: 0.0, endTangent: 0.0, interpMode: RealInterpMode.LINEAR }), + new RealKeyframeValue({ + value: 0.6, + startTangent: 0.487, + startTangentWeight: 0.2, + endTangent: 0.4598, + endTangentWeight: 0.32, + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.BOTH, + }), + ]); + compareCurves(serializeAndDeserialize(curve), curve); + }); + + test('Optimized for linear curve', () => { + const curve = new RealCurve(); + curve.assignSorted([0.1, 0.2, 0.3], [ + realKeyframeWithoutTangent(0.4), + realKeyframeWithoutTangent(0.5), + realKeyframeWithoutTangent(0.6), + ]); + compareCurves(serializeAndDeserialize(curve), curve); + }); + + test('Optimized for constant curve', () => { + const curve = new RealCurve(); + curve.assignSorted([0.1, 0.2, 0.3], [ + realKeyframeWithoutTangent(0.4, RealInterpMode.CONSTANT), + realKeyframeWithoutTangent(0.5, RealInterpMode.CONSTANT), + realKeyframeWithoutTangent(0.6, RealInterpMode.CONSTANT), + ]); + compareCurves(serializeAndDeserialize(curve), curve); + }); + }); + + test('Default keyframe value', () => { + const keyframeValue = new RealKeyframeValue({}); + expect(keyframeValue.value).toBe(0.0); + expect(keyframeValue.interpMode).toBe(RealInterpMode.LINEAR); + expect(keyframeValue.startTangent).toBe(0.0); + expect(keyframeValue.startTangentWeight).toBe(0.0); + expect(keyframeValue.endTangent).toBe(0.0); + expect(keyframeValue.endTangentWeight).toBe(0.0); + }); + + describe('Evaluation', () => { + test('Empty curve', () => { + const curve = new RealCurve(); + expect(curve.evaluate(12.34)).toBe(0.0); + }); + + test('Interpolation mode: constant', () => { + const curve = new RealCurve(); + curve.assignSorted([ + [0.2, new RealKeyframeValue({ value: 0.7, interpMode: RealInterpMode.CONSTANT, })], + [0.4, new RealKeyframeValue({ value: 0.8, interpMode: RealInterpMode.LINEAR, })], + ]); + expect(curve.evaluate(0.28)).toBe(0.7); + }); + + test('Interpolation mode: linear', () => { + const curve = new RealCurve(); + curve.assignSorted([ + [0.2, new RealKeyframeValue({ value: 0.7, interpMode: RealInterpMode.LINEAR, })], + [0.4, new RealKeyframeValue({ value: 0.8, interpMode: RealInterpMode.CONSTANT, })], + ]); + expect(curve.evaluate(0.28)).toBeCloseTo(0.74); + }); + + test('Interpolation mode: cubic; Both weights are unused', () => { + const curve = new RealCurve(); + curve.assignSorted([ + [0.2, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.NONE, + value: 0.7, + endTangent: Math.tan(toRadian(30.0)), + })], + [0.4, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.NONE, + value: 0.8, + startTangent: Math.tan(toRadian(30.0)), + })], + ]); + expect(curve.evaluate(0.28)).toBeCloseTo(0.740742562, 5); + }); + + test('Interpolation mode: cubic; Start weight is used', () => { + const curve = new RealCurve(); + curve.assignSorted([ + [0.2, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.END, + value: 0.7, + endTangent: Math.tan(toRadian(30.0)), + })], + [0.4, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.NONE, + value: 0.8, + startTangent: Math.tan(toRadian(30.0)), + })], + ]); + expect(curve.evaluate(0.28)).toBeCloseTo(0.737992646, 5); + }); + + test('Interpolation mode: cubic; End weight is used', () => { + const curve = new RealCurve(); + curve.assignSorted([ + [0.2, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.NONE, + value: 0.7, + endTangent: Math.tan(toRadian(30.0)), + })], + [0.4, new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.START, + value: 0.8, + startTangent: Math.tan(toRadian(30.0)), + })], + ]); + expect(curve.evaluate(0.28)).toBeCloseTo(0.7422913, 5); + }); + + test('Extrap mode: clamp', () => { + const curve = new RealCurve(); + curve.preExtrap = ExtrapMode.CLAMP; + curve.postExtrap = ExtrapMode.CLAMP; + + curve.assignSorted([ + [0.2, new RealKeyframeValue({ value: 5.0 })], + ]); + // Fall back to clamp + expect(curve.evaluate(0.05)).toBeCloseTo(5.0); + expect(curve.evaluate(0.46)).toBeCloseTo(5.0); + }); + + test('Extrap mode: linear', () => { + const curve = new RealCurve(); + curve.preExtrap = ExtrapMode.LINEAR; + curve.postExtrap = ExtrapMode.LINEAR; + + curve.assignSorted([ + [0.2, new RealKeyframeValue({ value: 5.0 })], + [0.3, new RealKeyframeValue({ value: 3.14 })], + ]); + expect(curve.evaluate(0.05)).toBeCloseTo(7.79); + expect(curve.evaluate(-102.4)).toBeCloseTo(1913.36); + expect(curve.evaluate(0.46)).toBeCloseTo(0.164); + + curve.assignSorted([ + [0.2, new RealKeyframeValue({ value: 5.0 })], + ]); + // Fall back to clamp + expect(curve.evaluate(0.05)).toBeCloseTo(5.0); + expect(curve.evaluate(0.46)).toBeCloseTo(5.0); + }); + + test('Extrap mode: repeat', () => { + const curve = new RealCurve(); + curve.preExtrap = ExtrapMode.REPEAT; + curve.postExtrap = ExtrapMode.REPEAT; + + curve.assignSorted([ + [0.2, new RealKeyframeValue({ value: 5.0 })], + [0.36, new RealKeyframeValue({ value: 3.14 })], + ]); + expect(curve.evaluate(-2.7)).toBeCloseTo(curve.evaluate(0.34)); + expect(curve.evaluate(4.6)).toBeCloseTo(curve.evaluate(0.28)); + + curve.assignSorted([ + [0.2, new RealKeyframeValue({ value: 5.0 })], + ]); + // Fall back to clamp + expect(curve.evaluate(0.05)).toBeCloseTo(5.0); + expect(curve.evaluate(0.46)).toBeCloseTo(5.0); + }); + + test('Extrap mode: ping-pong', () => { + const curve = new RealCurve(); + curve.preExtrap = ExtrapMode.PING_PONG; + curve.postExtrap = ExtrapMode.PING_PONG; + + curve.assignSorted([ + [0.2, new RealKeyframeValue({ value: 5.0 })], + [0.36, new RealKeyframeValue({ value: 3.14 })], + ]); + expect(curve.evaluate(-2.7)).toBeCloseTo(curve.evaluate(0.22)); + expect(curve.evaluate(4.6)).toBeCloseTo(curve.evaluate(0.28)); + expect(curve.evaluate(4.77)).toBeCloseTo(curve.evaluate(0.29)); + + curve.assignSorted([ + [0.2, new RealKeyframeValue({ value: 5.0 })], + ]); + // Fall back to clamp + expect(curve.evaluate(0.05)).toBeCloseTo(5.0); + expect(curve.evaluate(0.46)).toBeCloseTo(5.0); + }); + }); +}); + +function realKeyframeWithoutTangent (value: number, interpMode: RealInterpMode = RealInterpMode.LINEAR): RealKeyframeValue { + return new RealKeyframeValue({ + value, + interpMode, + }); +} + +function serializeAndDeserialize (curve: RealCurve) { + const serialized = curve[serializeSymbol](); + const newCurve = new RealCurve(); + newCurve[deserializeSymbol](serialized); + return newCurve; +} + +function compareCurves (left: RealCurve, right: RealCurve, numDigits = 2) { + expect(left.keyFramesCount).toBe(right.keyFramesCount); + for (let iKeyframe = 0; iKeyframe < left.keyFramesCount; ++iKeyframe) { + expect(left.getKeyframeTime(iKeyframe)).toBeCloseTo(right.getKeyframeTime(iKeyframe), numDigits); + const leftKeyframeValue = left.getKeyframeValue(iKeyframe); + const rightKeyframeValue = right.getKeyframeValue(iKeyframe); + expect(leftKeyframeValue.value).toBeCloseTo(rightKeyframeValue.value, numDigits); + expect(leftKeyframeValue.startTangent).toBeCloseTo(rightKeyframeValue.startTangent, numDigits); + expect(leftKeyframeValue.startTangentWeight).toBeCloseTo(rightKeyframeValue.startTangentWeight, numDigits); + expect(leftKeyframeValue.endTangent).toBeCloseTo(rightKeyframeValue.endTangent, numDigits); + expect(leftKeyframeValue.endTangentWeight).toBeCloseTo(rightKeyframeValue.endTangentWeight, numDigits); + expect(leftKeyframeValue.interpMode).toStrictEqual(rightKeyframeValue.interpMode); + } +} \ No newline at end of file diff --git a/tests/curves/key-shared-curves.test.ts b/tests/curves/key-shared-curves.test.ts new file mode 100644 index 00000000000..e505a71d83e --- /dev/null +++ b/tests/curves/key-shared-curves.test.ts @@ -0,0 +1,244 @@ + +import { ExtrapMode, RealCurve, RealInterpMode, RealKeyframeValue } from '../../cocos/core/curves/curve'; +import { KeySharedQuaternionCurves, KeySharedRealCurves } from '../../cocos/core/curves/keys-shared-curves'; +import { QuaternionCurve, QuaternionInterpMode, QuaternionKeyframeValue } from '../../cocos/core/curves/quat-curve'; +import { Quat } from '../../cocos/core/math'; + +describe('Keys shared real curves', () => { + test('Enabling', () => { + { + const curve = new RealCurve(); + curve.assignSorted([[0.1, new RealKeyframeValue({ + value: 0.1, + })]]); + expect(KeySharedRealCurves.allowedForCurve(curve)).toBe(true); + } + + { + const curve = new RealCurve(); + curve.assignSorted([[0.1, new RealKeyframeValue({ + value: 0.1, + })]]); + curve.postExtrap = ExtrapMode.REPEAT; + expect(KeySharedRealCurves.allowedForCurve(curve)).toBe(false); + } + + { + const curve = new RealCurve(); + curve.assignSorted([[0.1, new RealKeyframeValue({ + value: 0.1, + })]]); + curve.preExtrap = ExtrapMode.REPEAT; + expect(KeySharedRealCurves.allowedForCurve(curve)).toBe(false); + } + + { + const curve = new RealCurve(); + curve.assignSorted([[0.1, new RealKeyframeValue({ + value: 0.1, + interpMode: RealInterpMode.CUBIC, + })]]); + expect(KeySharedRealCurves.allowedForCurve(curve)).toBe(false); + } + }); + + test('Composite', () => { + const curves1 = new KeySharedRealCurves([0.1, 0.7, 0.8]); + + const curveMatched = new RealCurve(); + curveMatched.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, () => new RealKeyframeValue({ value: 0.1 }))); + expect(curves1.matchCurve(curveMatched)).toBe(true); + + const curveNonMatched = new RealCurve(); + curveNonMatched.assignSorted([0.1, 0.3, 0.8], Array.from({ length: 3 }, () => new RealKeyframeValue({ value: 0.1 }))); + expect(curves1.matchCurve(curveNonMatched)).toBe(false); + }); + + test('Composite (may be baked)', () => { + const curves1 = new KeySharedRealCurves([0.1, 0.2, 0.3]); + + const curveMatched = new RealCurve(); + curveMatched.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, () => new RealKeyframeValue({ value: 0.1 }))); + expect(curves1.matchCurve(curveMatched)).toBe(true); + + const curveNonMatched = new RealCurve(); + curveNonMatched.assignSorted([0.2, 0.3, 0.4], Array.from({ length: 3 }, () => new RealKeyframeValue({ value: 0.1 }))); + expect(curves1.matchCurve(curveNonMatched)).toBe(false); + }); + + test('Evaluate', () => { + const curve = new RealCurve(); + curve.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, (_, index) => new RealKeyframeValue({ value: index + 1 }))); + + const curves = new KeySharedRealCurves(Array.from(curve.times())); + curves.addCurve(curve); + const values = [0.0]; + const resetAndEval = (time: number) => { + values[0] = NaN; + curves.evaluate(time, values); + }; + + resetAndEval(0.0); + expect(values[0]).toBeCloseTo(1.0); + + resetAndEval(0.81); + expect(values[0]).toBeCloseTo(3.0); + + resetAndEval(0.7); + expect(values[0]).toBeCloseTo(2.0); + + resetAndEval(0.73); + expect(values[0]).toBeCloseTo(2.3); + }); + + test('Evaluate optimized keys', () => { + const curve = new RealCurve(); + curve.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, (_, index) => new RealKeyframeValue({ value: index + 1 }))); + + const curves = new KeySharedRealCurves(Array.from(curve.times())); + curves.addCurve(curve); + const values = [0.0]; + const resetAndEval = (time: number) => { + values[0] = NaN; + curves.evaluate(time, values); + }; + + resetAndEval(0.0); + expect(values[0]).toBeCloseTo(1.0); + + resetAndEval(0.31); + expect(values[0]).toBeCloseTo(3.0); + + resetAndEval(0.2); + expect(values[0]).toBeCloseTo(2.0); + + resetAndEval(0.25); + expect(values[0]).toBeCloseTo(2.5); + }); +}); + +describe('Keys shared quaternion curves', () => { + test('Enabling', () => { + { + const curve = new QuaternionCurve(); + curve.assignSorted([[0.1, new QuaternionKeyframeValue({ + interpMode: QuaternionInterpMode.SLERP, + value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, + })]]); + expect(KeySharedQuaternionCurves.allowedForCurve(curve)).toBe(true); + } + + { + const curve = new QuaternionCurve(); + curve.assignSorted([[0.1, new QuaternionKeyframeValue({ + value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, + })]]); + curve.postExtrap = ExtrapMode.REPEAT; + expect(KeySharedQuaternionCurves.allowedForCurve(curve)).toBe(false); + } + + { + const curve = new QuaternionCurve(); + curve.assignSorted([[0.1, new QuaternionKeyframeValue({ + value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, + })]]); + curve.preExtrap = ExtrapMode.REPEAT; + expect(KeySharedQuaternionCurves.allowedForCurve(curve)).toBe(false); + } + + { + const curve = new QuaternionCurve(); + curve.assignSorted([[0.1, new QuaternionKeyframeValue({ + value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, + interpMode: QuaternionInterpMode.CONSTANT, + })]]); + expect(KeySharedQuaternionCurves.allowedForCurve(curve)).toBe(false); + } + }); + + test('Composite', () => { + const curves1 = new KeySharedQuaternionCurves([0.1, 0.7, 0.8]); + + const curveMatched = new QuaternionCurve(); + curveMatched.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, () => + new QuaternionKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + expect(curves1.matchCurve(curveMatched)).toBe(true); + + const curveNonMatched = new QuaternionCurve(); + curveNonMatched.assignSorted([0.1, 0.3, 0.8], Array.from({ length: 3 }, () => + new QuaternionKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + expect(curves1.matchCurve(curveNonMatched)).toBe(false); + }); + + test('Composite (may be baked)', () => { + const curves1 = new KeySharedQuaternionCurves([0.1, 0.2, 0.3]); + + const curveMatched = new QuaternionCurve(); + curveMatched.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, () => + new QuaternionKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + expect(curves1.matchCurve(curveMatched)).toBe(true); + + const curveNonMatched = new QuaternionCurve(); + curveNonMatched.assignSorted([0.2, 0.3, 0.4], Array.from({ length: 3 }, () => + new QuaternionKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + expect(curves1.matchCurve(curveNonMatched)).toBe(false); + }); + + const quaternions = [ + new Quat(-0.542, -0.688 -0.439, 0.199), + new Quat(-0.403, 0.723, -0.545, -0.135), + new Quat(0.658, 0.422, 0.455, 0.427), + ]; + + test('Evaluate', () => { + const curve = new QuaternionCurve(); + curve.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, (_, index) => + new QuaternionKeyframeValue({ value: Quat.clone(quaternions[index]) }))); + + const curves = new KeySharedQuaternionCurves(Array.from(curve.times())); + curves.addCurve(curve); + const values = [new Quat()]; + const resetAndEval = (time: number) => { + Quat.set(values[0], NaN, NaN, NaN, NaN); + curves.evaluate(time, values); + }; + + resetAndEval(0.0); + expect(Quat.equals(values[0], quaternions[0])).toBe(true); + + resetAndEval(0.81); + expect(Quat.equals(values[0], quaternions[2])).toBe(true); + + resetAndEval(0.7); + expect(Quat.equals(values[0], quaternions[1])).toBe(true); + + resetAndEval(0.73); + expect(Quat.equals(values[0], Quat.slerp(new Quat(), quaternions[1], quaternions[2], 0.3))).toBe(true); + }); + + test('Evaluate optimized keys', () => { + const curve = new QuaternionCurve(); + curve.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, (_, index) => + new QuaternionKeyframeValue({ value: Quat.clone(quaternions[index]) }))); + + const curves = new KeySharedQuaternionCurves(Array.from(curve.times())); + curves.addCurve(curve); + const values = [new Quat()]; + const resetAndEval = (time: number) => { + Quat.set(values[0], NaN, NaN, NaN, NaN); + curves.evaluate(time, values); + }; + + resetAndEval(0.0); + expect(Quat.equals(values[0], quaternions[0])).toBe(true); + + resetAndEval(0.31); + expect(Quat.equals(values[0], quaternions[2])).toBe(true); + + resetAndEval(0.2); + expect(Quat.equals(values[0], quaternions[1])).toBe(true); + + resetAndEval(0.25); + expect(Quat.equals(values[0], Quat.slerp(new Quat(), quaternions[1], quaternions[2], 0.5))).toBe(true); + }); +}); \ No newline at end of file diff --git a/tests/curves/quat-curve.test.ts b/tests/curves/quat-curve.test.ts new file mode 100644 index 00000000000..249f2fe396c --- /dev/null +++ b/tests/curves/quat-curve.test.ts @@ -0,0 +1,67 @@ + +import { Quat, QuaternionCurve, QuaternionInterpMode, QuaternionKeyframeValue } from '../../cocos/core'; +import { deserializeSymbol, serializeSymbol } from '../../cocos/core/data/serialization-symbols'; + +describe('Curve', () => { + test('Evaluate an empty curve', () => { + const curve = new QuaternionCurve(); + expect(curve.evaluate(12.34)).toStrictEqual(Quat.IDENTITY); + }); + + describe('serialization', () => { + test('Normal', () => { + const curve = new QuaternionCurve(); + curve.assignSorted([0.1, 0.2, 0.3], [ + new QuaternionKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpMode: QuaternionInterpMode.CONSTANT }), + new QuaternionKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpMode: QuaternionInterpMode.SLERP }), + new QuaternionKeyframeValue({ value: { x: 0.9, y: 0.1, z: 0.11, w: 0.12 }, interpMode: QuaternionInterpMode.CONSTANT }), + ]); + compareCurves(serializeAndDeserialize(curve), curve); + }); + + test('Optimized for linear curve', () => { + const curve = new QuaternionCurve(); + curve.assignSorted([0.1, 0.2], [ + new QuaternionKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpMode: QuaternionInterpMode.SLERP }), + new QuaternionKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpMode: QuaternionInterpMode.SLERP }), + ]); + compareCurves(serializeAndDeserialize(curve), curve); + }); + + test('Optimized for constant curve', () => { + const curve = new QuaternionCurve(); + curve.assignSorted([0.1, 0.2], [ + new QuaternionKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpMode: QuaternionInterpMode.CONSTANT }), + new QuaternionKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpMode: QuaternionInterpMode.CONSTANT }), + ]); + compareCurves(serializeAndDeserialize(curve), curve); + }); + }); + + test('Default keyframe value', () => { + const keyframeValue = new QuaternionKeyframeValue({}); + expect(Quat.equals(keyframeValue.value, Quat.IDENTITY)).toBe(true); + expect(keyframeValue.interpMode).toBe(QuaternionInterpMode.SLERP); + }); +}); + +function serializeAndDeserialize (curve: QuaternionCurve) { + const serialized = curve[serializeSymbol](); + const newCurve = new QuaternionCurve(); + newCurve[deserializeSymbol](serialized); + return newCurve; +} + +function compareCurves (left: QuaternionCurve, right: QuaternionCurve, numDigits = 2) { + expect(left.keyFramesCount).toBe(right.keyFramesCount); + for (let iKeyframe = 0; iKeyframe < left.keyFramesCount; ++iKeyframe) { + expect(left.getKeyframeTime(iKeyframe)).toBeCloseTo(right.getKeyframeTime(iKeyframe), numDigits); + const leftKeyframeValue = left.getKeyframeValue(iKeyframe); + const rightKeyframeValue = right.getKeyframeValue(iKeyframe); + expect(leftKeyframeValue.value.x).toBeCloseTo(rightKeyframeValue.value.x, numDigits); + expect(leftKeyframeValue.value.y).toBeCloseTo(rightKeyframeValue.value.y, numDigits); + expect(leftKeyframeValue.value.z).toBeCloseTo(rightKeyframeValue.value.z, numDigits); + expect(leftKeyframeValue.value.w).toBeCloseTo(rightKeyframeValue.value.w, numDigits); + expect(leftKeyframeValue.interpMode).toStrictEqual(rightKeyframeValue.interpMode); + } +} \ No newline at end of file From 5c9369680fe9802ec3b0f84ad7477835b9823027 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Wed, 30 Jun 2021 18:47:14 +0800 Subject: [PATCH 02/35] Mark curve frame values as uniquelyReferenced --- cocos/core/curves/curve.ts | 3 ++- cocos/core/curves/quat-curve.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cocos/core/curves/curve.ts b/cocos/core/curves/curve.ts index 1b245c474ea..31bd4413986 100644 --- a/cocos/core/curves/curve.ts +++ b/cocos/core/curves/curve.ts @@ -1,7 +1,7 @@ import { assertIsTrue } from '../data/utils/asserts'; import { approx, lerp, pingPong, repeat } from '../math'; import { KeyframeCurve } from './keyframe-curve'; -import { ccclass, serializable } from '../data/decorators'; +import { ccclass, serializable, uniquelyReferenced } from '../data/decorators'; import { deserializeSymbol, serializeSymbol } from '../data/serialization-symbols'; import { RealInterpMode, ExtrapMode, TangentWeightMode } from './real-curve-param'; import { binarySearchEpsilon } from '../algorithm/binary-search'; @@ -11,6 +11,7 @@ import { EditorExtendableMixin } from '../data/editor-extendable'; export { RealInterpMode, ExtrapMode, TangentWeightMode }; @ccclass('cc.RealKeyframeValue') +@uniquelyReferenced export class RealKeyframeValue { constructor ({ interpMode, diff --git a/cocos/core/curves/quat-curve.ts b/cocos/core/curves/quat-curve.ts index f8968b9e9ab..664f31a73a6 100644 --- a/cocos/core/curves/quat-curve.ts +++ b/cocos/core/curves/quat-curve.ts @@ -3,10 +3,11 @@ import { IQuatLike, pingPong, Quat, repeat } from '../math'; import { KeyframeCurve } from './keyframe-curve'; import { ExtrapMode } from './curve'; import { binarySearchEpsilon } from '../algorithm/binary-search'; -import { ccclass, serializable } from '../data/decorators'; +import { ccclass, serializable, uniquelyReferenced } from '../data/decorators'; import { deserializeSymbol, serializeSymbol } from '../data/serialization-symbols'; @ccclass('cc.QuaternionKeyframeValue') +@uniquelyReferenced export class QuaternionKeyframeValue { /** * Interpolation method used for this keyframe. From 08d90f18e3d986871fd92bd993d2f0c3e927a61d Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Thu, 1 Jul 2021 12:39:58 +0800 Subject: [PATCH 03/35] Adjust modules --- cocos/core/animation/animation-clip.ts | 1052 +---------------- cocos/core/animation/animation.ts | 16 +- .../animation/compression/compressed-data.ts | 323 +++++ cocos/core/animation/define.ts | 3 + cocos/core/animation/legacy-clip-data.ts | 226 +++- cocos/core/animation/tracks/color-track.ts | 69 ++ cocos/core/animation/tracks/integer-track.ts | 11 + cocos/core/animation/tracks/object-track.ts | 11 + cocos/core/animation/tracks/quat-track.ts | 29 + cocos/core/animation/tracks/real-track.ts | 11 + cocos/core/animation/tracks/track.ts | 134 +++ cocos/core/animation/tracks/untyped-track.ts | 130 ++ cocos/core/animation/tracks/utils.ts | 10 + cocos/core/animation/tracks/vector-track.ts | 144 +++ 14 files changed, 1122 insertions(+), 1047 deletions(-) create mode 100644 cocos/core/animation/compression/compressed-data.ts create mode 100644 cocos/core/animation/define.ts create mode 100644 cocos/core/animation/tracks/color-track.ts create mode 100644 cocos/core/animation/tracks/integer-track.ts create mode 100644 cocos/core/animation/tracks/object-track.ts create mode 100644 cocos/core/animation/tracks/quat-track.ts create mode 100644 cocos/core/animation/tracks/real-track.ts create mode 100644 cocos/core/animation/tracks/track.ts create mode 100644 cocos/core/animation/tracks/untyped-track.ts create mode 100644 cocos/core/animation/tracks/utils.ts create mode 100644 cocos/core/animation/tracks/vector-track.ts diff --git a/cocos/core/animation/animation-clip.ts b/cocos/core/animation/animation-clip.ts index 7c6b0188f0b..626490181b8 100644 --- a/cocos/core/animation/animation-clip.ts +++ b/cocos/core/animation/animation-clip.ts @@ -36,23 +36,25 @@ import { DataPoolManager } from '../../3d/skeletal-animation/data-pool-manager'; import { binarySearchEpsilon } from '../algorithm/binary-search'; import { murmurhash2_32_gc } from '../utils/murmurhash2_gc'; import { SkelAnimDataHub } from '../../3d/skeletal-animation/skeletal-animation-data-hub'; -import { ComponentPath, HierarchyPath, TargetPath, evaluatePath, isPropertyPath } from './target-path'; +import { ComponentPath, evaluatePath, isPropertyPath } from './target-path'; import { WrapMode as AnimationWrapMode, WrapMode, WrapModeMask } from './types'; import { IValueProxyFactory } from './value-proxy'; import { legacyCC } from '../global-exports'; -import { RealCurve, RealInterpMode } from '../curves'; -import { ObjectCurve } from '../curves/object-curve'; -import { Color, Mat4, Quat, Size, Vec2, Vec3, Vec4 } from '../math'; +import { Mat4, Quat, Vec3 } from '../math'; import { Node } from '../scene-graph/node'; -import { IntegerCurve } from '../curves/integer-curve'; -import { QuaternionCurve, QuaternionInterpMode } from '../curves/quat-curve'; -import { KeySharedQuaternionCurves, KeySharedRealCurves } from '../curves/keys-shared-curves'; import { assertIsTrue } from '../data/utils/asserts'; import type { PoseOutput } from './pose-output'; import * as legacy from './legacy-clip-data'; import { BAKE_SKELETON_CURVE_SYMBOL } from './internal-symbols'; -import { RealKeyframeValue } from '../curves/curve'; -import { CubicSplineNumberValue, CubicSplineVec2Value, CubicSplineVec3Value, CubicSplineVec4Value } from './cubic-spline-value'; +import { Binder, isTargetingTRS, RuntimeBinding, Track, TrackEval, TrackPath, TrsTrackPath } from './tracks/track'; +import { createEvalSymbol } from './define'; +import { VectorTrack } from './tracks/vector-track'; +import { UntypedTrack, UntypedTrackRefine } from './tracks/untyped-track'; +import { Range } from './tracks/utils'; +import { ObjectTrack } from './tracks/object-track'; +import { CompressedData, CompressedDataEvaluator } from './compression/compressed-data'; +import { RealTrack } from './tracks/real-track'; +import { QuaternionTrack } from './tracks/quat-track'; export declare namespace AnimationClip { export interface IEvent { @@ -66,429 +68,9 @@ export declare namespace AnimationClip { // #region Tracks -type TrackPath = TargetPath[]; - -interface Range { - min: number; - max: number; -} - -const createEvalSymbol = Symbol('CreateEval'); - -const CLASS_NAME_PREFIX_ANIM = 'cc.animation.'; - // Export for test export const searchForRootBonePathSymbol = Symbol('SearchForRootBonePath'); -/** - * A track describes the path of animate a target. - * It's the basic unit of animation clip. - */ -@ccclass(`${CLASS_NAME_PREFIX_ANIM}Track`) -export class Track { - @serializable - public path: TrackPath = []; - - @serializable - public setter!: IValueProxyFactory | undefined; - - public getChannels (): Channel[] { - return []; - } - - public getRange (): Range { - const range: Range = { min: Infinity, max: -Infinity }; - for (const channel of this.getChannels()) { - range.min = Math.min(range.min, channel.curve.rangeMin); - range.max = Math.max(range.max, channel.curve.rangeMax); - } - return range; - } - - public [createEvalSymbol] (runtimeBinding: RuntimeBinding): TrackEval { - throw new Error(`No Impl`); - } -} - -interface TrackEval { - /** - * Evaluates the track. - * @param time The time. - */ - evaluate(time: number, runtimeBinding: RuntimeBinding): unknown; -} - -type Curve = RealCurve | IntegerCurve | QuaternionCurve | ObjectCurve; - -@ccclass(`${CLASS_NAME_PREFIX_ANIM}Channel`) -export class Channel { - constructor (curve: T) { - this._curve = curve; - } - - @serializable - public name = ''; - - get curve () { - return this._curve; - } - - @serializable - private _curve!: T; -} - -type RealChannel = Channel; - -type IntegerChannel = Channel; - -type QuaternionChannel = Channel; - -@ccclass(`${CLASS_NAME_PREFIX_ANIM}SingleChannelTrack`) -export abstract class SingleChannelTrack extends Track { - constructor () { - super(); - this._channel = new Channel(this.createCurve()); - } - - get channel () { - return this._channel; - } - - public getChannels () { - return [this._channel]; - } - - protected createCurve (): TCurve { - throw new Error(`Not impl`); - } - - public [createEvalSymbol] (_runtimeBinding: RuntimeBinding): TrackEval { - const { curve } = this._channel; - return { - evaluate: (time) => curve.evaluate(time), - }; - } - - @serializable - private _channel: Channel; -} - -@ccclass(`${CLASS_NAME_PREFIX_ANIM}RealTrack`) -export class RealTrack extends SingleChannelTrack { - protected createCurve () { - return new RealCurve(); - } -} - -@ccclass(`${CLASS_NAME_PREFIX_ANIM}IntegerTrack`) -export class IntegerTrack extends SingleChannelTrack { - protected createCurve () { - return new IntegerCurve(); - } -} - -@ccclass(`${CLASS_NAME_PREFIX_ANIM}QuaternionTrack`) -export class QuaternionTrack extends SingleChannelTrack { - protected createCurve () { - return new QuaternionCurve(); - } - - public [createEvalSymbol] () { - return new QuatTrackEval(this.getChannels()[0].curve); - } -} - -class QuatTrackEval { - constructor (private _curve: QuaternionCurve) { - - } - - public evaluate (time: number) { - this._curve.evaluate(time, this._result); - return this._result; - } - - private _result: Quat = new Quat(); -} - -@ccclass(`${CLASS_NAME_PREFIX_ANIM}ObjectTrack`) -export class ObjectTrack extends SingleChannelTrack> { - protected createCurve () { - return new ObjectCurve(); - } -} - -function maskIfEmpty (curve: T) { - return curve.empty ? undefined : curve; -} - -@ccclass(`${CLASS_NAME_PREFIX_ANIM}VectorTrack`) -export class VectorTrack extends Track { - constructor () { - super(); - this._channels = new Array(4) as VectorTrack['_channels']; - for (let i = 0; i < this._channels.length; ++i) { - const channel = new Channel(new RealCurve()); - channel.name = 'X'; - this._channels[i] = channel; - } - } - - get componentsCount () { - return this._nComponents; - } - - set componentsCount (value) { - this._nComponents = value; - } - - public getChannels () { - return this._channels; - } - - public [createEvalSymbol] () { - switch (this._nComponents) { - default: - case 2: - return new Vec2TrackEval( - maskIfEmpty(this._channels[0].curve), - maskIfEmpty(this._channels[1].curve), - ); - case 3: - return new Vec3TrackEval( - maskIfEmpty(this._channels[0].curve), - maskIfEmpty(this._channels[1].curve), - maskIfEmpty(this._channels[2].curve), - ); - case 4: - return new Vec4TrackEval( - maskIfEmpty(this._channels[0].curve), - maskIfEmpty(this._channels[1].curve), - maskIfEmpty(this._channels[2].curve), - maskIfEmpty(this._channels[3].curve), - ); - } - } - - @serializable - private _channels: [RealChannel, RealChannel, RealChannel, RealChannel]; - - @serializable - private _nComponents: 2 | 3 | 4 = 4; -} - -class Vec2TrackEval { - constructor (private _x: RealCurve | undefined, private _y: RealCurve | undefined) { - - } - - public evaluate (time: number, runtimeBinding: RuntimeBinding) { - if ((!this._x || !this._y) && runtimeBinding.getValue) { - Vec2.copy(this._result, runtimeBinding.getValue() as Vec2); - } - - if (this._x) { - this._result.x = this._x.evaluate(time); - } - if (this._y) { - this._result.y = this._y.evaluate(time); - } - - return this._result; - } - - private _result: Vec2 = new Vec2(); -} - -class Vec3TrackEval { - constructor (private _x: RealCurve | undefined, private _y: RealCurve | undefined, private _z: RealCurve | undefined) { - - } - - public evaluate (time: number, runtimeBinding: RuntimeBinding) { - if ((!this._x || !this._y || !this._z) && runtimeBinding.getValue) { - Vec3.copy(this._result, runtimeBinding.getValue() as Vec3); - } - - if (this._x) { - this._result.x = this._x.evaluate(time); - } - if (this._y) { - this._result.y = this._y.evaluate(time); - } - if (this._z) { - this._result.z = this._z.evaluate(time); - } - - return this._result; - } - - private _result: Vec3 = new Vec3(); -} - -class Vec4TrackEval { - constructor ( - private _x: RealCurve | undefined, - private _y: RealCurve | undefined, - private _z: RealCurve | undefined, - private _w: RealCurve | undefined, - ) { - - } - - public evaluate (time: number, runtimeBinding: RuntimeBinding) { - if ((!this._x || !this._y || !this._z || !this._w) && runtimeBinding.getValue) { - Vec4.copy(this._result, runtimeBinding.getValue() as Vec4); - } - - if (this._x) { - this._result.x = this._x.evaluate(time); - } - if (this._y) { - this._result.y = this._y.evaluate(time); - } - if (this._z) { - this._result.z = this._z.evaluate(time); - } - if (this._w) { - this._result.w = this._w.evaluate(time); - } - - return this._result; - } - - private _result: Vec4 = new Vec4(); -} - -@ccclass(`${CLASS_NAME_PREFIX_ANIM}ColorTrack`) -export class ColorTrack extends Track { - constructor () { - super(); - this._channels = new Array(4) as ColorTrack['_channels']; - for (let i = 0; i < this._channels.length; ++i) { - const channel = new Channel(new IntegerCurve()); - channel.name = 'R'; - this._channels[i] = channel; - } - } - - public getChannels () { - return this._channels; - } - - public [createEvalSymbol] () { - return new ColorTrackEval( - maskIfEmpty(this._channels[0].curve), - maskIfEmpty(this._channels[1].curve), - maskIfEmpty(this._channels[2].curve), - maskIfEmpty(this._channels[3].curve), - ); - } - - @serializable - private _channels: [IntegerChannel, IntegerChannel, IntegerChannel, IntegerChannel]; -} - -class ColorTrackEval { - constructor ( - private _x: TCurve | undefined, - private _y: TCurve | undefined, - private _z: TCurve | undefined, - private _w: TCurve | undefined, - ) { - - } - - public evaluate (time: number, runtimeBinding: RuntimeBinding) { - if ((!this._x || !this._y || !this._z || !this._w) && runtimeBinding.getValue) { - Color.copy(this._result, runtimeBinding.getValue() as Color); - } - - if (this._x) { - this._result.r = this._x.evaluate(time); - } - if (this._y) { - this._result.g = this._y.evaluate(time); - } - if (this._z) { - this._result.b = this._z.evaluate(time); - } - if (this._w) { - this._result.a = this._w.evaluate(time); - } - - return this._result; - } - - private _result: Color = new Color(); -} - -@ccclass(`${CLASS_NAME_PREFIX_ANIM}UntypedTrackChannel`) -class UntypedTrackChannel extends Channel { - @serializable - public property!: string; - - constructor () { - super(new RealCurve()); - } -} - -@ccclass(`${CLASS_NAME_PREFIX_ANIM}UntypedTrack`) -class UntypedTrack extends Track { - @serializable - private _channels: UntypedTrackChannel[] = []; - - public getChannels () { - return this._channels; - } - - public [createEvalSymbol] (runtimeBinding: RuntimeBinding) { - if (!runtimeBinding.getValue) { - throw new Error(`Can not decide type for untyped track: runtime binding does not provide a getter.`); - } - const trySearchCurve = (property: string) => this._channels.find((channel) => channel.property === property)?.curve; - const value = runtimeBinding.getValue(); - switch (true) { - case value instanceof Size: - default: - throw new Error(`Can not decide type for untyped track: got a unsupported value from runtime binding.`); - case value instanceof Vec2: - return new Vec2TrackEval( - trySearchCurve('x'), - trySearchCurve('y'), - ); - case value instanceof Vec3: - return new Vec3TrackEval( - trySearchCurve('x'), - trySearchCurve('y'), - trySearchCurve('z'), - ); - case value instanceof Vec4: - return new Vec4TrackEval( - trySearchCurve('x'), - trySearchCurve('y'), - trySearchCurve('z'), - trySearchCurve('w'), - ); - case value instanceof Color: - // TODO: what if x, y, z, w? - return new ColorTrackEval( - trySearchCurve('r'), - trySearchCurve('g'), - trySearchCurve('b'), - trySearchCurve('a'), - ); - } - } - - public addChannel (property: string): UntypedTrackChannel { - const channel = new UntypedTrackChannel(); - channel.property = property; - this._channels.push(channel); - return channel; - } -} - // #endregion interface SkeletonAnimationBakeInfo { @@ -865,64 +447,15 @@ export class AnimationClip extends Asset { * @param refine How to decide the type on specified path. * @internal DO NOT USE THIS IN YOUR CODE. */ - public upgradeUntypedTracks (refine: (path: TrackPath, setter?: IValueProxyFactory) => 'vec2' | 'vec3' | 'vec4' | 'color' | 'size') { + public upgradeUntypedTracks (refine: UntypedTrackRefine) { const newTracks: Track[] = []; for (const track of this._tracks) { if (!(track instanceof UntypedTrack)) { continue; } - const untypedTrack = track; - - const trySearchChannel = (property: string, outChannel: RealChannel) => { - const untypedChannel = untypedTrack.getChannels().find((channel) => channel.property === property); - if (untypedChannel) { - outChannel.name = untypedChannel.name; - outChannel.curve.assignSorted( - Array.from(untypedChannel.curve.times()), - Array.from(untypedChannel.curve.values()), - ); - } - }; - const kind = refine(track.path, track.setter); - switch (kind) { - default: - continue; - case 'vec2': case 'vec3': case 'vec4': { - const track = new VectorTrack(); - newTracks.push(track); - track.componentsCount = kind === 'vec2' ? 2 : kind === 'vec3' ? 3 : 4; - const [x, y, z, w] = track.getChannels(); - switch (kind) { - case 'vec4': - trySearchChannel('w', w); - // fall through - case 'vec3': - trySearchChannel('z', z); - // fall through - default: - case 'vec2': - trySearchChannel('x', x); - trySearchChannel('y', y); - } - break; - } - case 'color': { - const track = new ColorTrack(); - newTracks.push(track); - const [r, g, b, a] = track.getChannels(); - trySearchChannel('r', r); - trySearchChannel('g', g); - trySearchChannel('b', b); - trySearchChannel('a', a); - // TODO: we need float-int conversion if xyzw - trySearchChannel('x', r); - trySearchChannel('y', g); - trySearchChannel('z', b); - trySearchChannel('w', a); - break; - } - case 'size': - break; + const newTrack = track.upgrade(refine); + if (newTrack) { + newTracks.push(newTrack); } } } @@ -1242,214 +775,7 @@ export class AnimationClip extends Asset { } private _fromLegacy (legacyData: legacy.AnimationClipLegacyData) { - const newTracks: Track[] = []; - - const { - keys: legacyKeys, - curves: legacyCurves, - commonTargets: legacyCommonTargets, - } = legacyData; - - const untypedTracks = legacyCommonTargets.map((legacyCommonTarget) => { - const track = new UntypedTrack(); - track.path = legacyCommonTarget.modifiers; - track.setter = legacyCommonTarget.valueAdapter; - newTracks.push(track); - return track; - }); - - for (const legacyCurve of legacyCurves) { - const legacyCurveData = legacyCurve.data; - const legacyValues = legacyCurveData.values; - if (legacyValues.length === 0) { - // Legacy clip did not record type info. - continue; - } - const legacyKeysIndex = legacyCurveData.keys; - // Rule: negative index means single frame. - const times = legacyKeysIndex < 0 ? [0.0] : legacyKeys[legacyCurveData.keys]; - const firstValue = legacyValues[0]; - // Rule: default to true. - const interpolate = legacyCurveData ?? true; - // Rule: _arrayLength only used for morph target, internally. - assertIsTrue(typeof legacyCurveData._arrayLength !== 'number' || typeof firstValue === 'number'); - const legacyEasingMethodConverter = new legacy.LegacyEasingMethodConverter(legacyCurveData, times.length); - - const installPathAndSetter = (track: Track) => { - track.path = legacyCurve.modifiers; - track.setter = legacyCurve.valueAdapter; - }; - - let legacyCommonTargetCurve: RealCurve | undefined; - if (typeof legacyCurve.commonTarget === 'number') { - // Rule: common targets should only target Vectors/`Size`/`Color`. - if (!legacyValues.every((value) => typeof value === 'number')) { - warn(`Incorrect curve.`); - continue; - } - // Rule: Each curve that has common target should be numeric curve and targets string property. - if (legacyCurve.valueAdapter || legacyCurve.modifiers.length !== 1 || typeof legacyCurve.modifiers[0] !== 'string') { - warn(`Incorrect curve.`); - continue; - } - const propertyName = legacyCurve.modifiers[0]; - const untypedTrack = untypedTracks[legacyCurve.commonTarget]; - const { curve } = untypedTrack.addChannel(propertyName); - legacyCommonTargetCurve = curve; - } - - const convertCurve = () => { - if (typeof firstValue === 'number') { - if (!legacyValues.every((value) => typeof value === 'number')) { - warn(`Misconfigured curve.`); - return; - } - let realCurve: RealCurve; - if (legacyCommonTargetCurve) { - realCurve = legacyCommonTargetCurve; - } else { - const track = new RealTrack(); - installPathAndSetter(track); - newTracks.push(track); - realCurve = track.channel.curve; - } - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - realCurve.assignSorted(times, (legacyValues as number[]).map((value) => new RealKeyframeValue({ value, interpMode: interpMethod }))); - legacyEasingMethodConverter.convert(realCurve); - return; - } else if (typeof firstValue === 'object') { - switch (true) { - default: - break; - case legacyValues.every((value) => value instanceof Vec2): - case legacyValues.every((value) => value instanceof Vec3): - case legacyValues.every((value) => value instanceof Vec4): { - type Vec4plus = Vec4[]; - type Vec3plus = (Vec3 | Vec4)[]; - type Vec2plus = (Vec2 | Vec3 | Vec4)[]; - const components = firstValue instanceof Vec2 ? 2 : firstValue instanceof Vec3 ? 3 : 4; - const track = new VectorTrack(); - installPathAndSetter(track); - track.componentsCount = components; - const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.getChannels(); - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); - switch (components) { - case 4: - w.assignSorted(times, (legacyValues as Vec4plus).map((value) => valueToFrame(value.w))); - legacyEasingMethodConverter.convert(w); - // falls through - case 3: - z.assignSorted(times, (legacyValues as Vec3plus).map((value) => valueToFrame(value.z))); - legacyEasingMethodConverter.convert(z); - // falls through - default: - x.assignSorted(times, (legacyValues as Vec2plus).map((value) => valueToFrame(value.x))); - legacyEasingMethodConverter.convert(x); - y.assignSorted(times, (legacyValues as Vec2plus).map((value) => valueToFrame(value.y))); - legacyEasingMethodConverter.convert(y); - break; - } - newTracks.push(track); - return; - } - case legacyValues.every((value) => value instanceof Quat): { - assertIsTrue(legacyEasingMethodConverter.nil); - const track = new QuaternionTrack(); - installPathAndSetter(track); - const interpMode = interpolate ? QuaternionInterpMode.SLERP : QuaternionInterpMode.CONSTANT; - track.channel.curve.assignSorted(times, (legacyValues as Quat[]).map((value) => ({ - value: Quat.clone(value), - interpMode, - }))); - newTracks.push(track); - return; - } - case legacyValues.every((value) => value instanceof Color): { - const track = new ColorTrack(); - installPathAndSetter(track); - const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.getChannels(); - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); - r.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.r))); - legacyEasingMethodConverter.convert(r); - g.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.g))); - legacyEasingMethodConverter.convert(g); - b.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.b))); - legacyEasingMethodConverter.convert(b); - a.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.a))); - legacyEasingMethodConverter.convert(a); - newTracks.push(track); - return; - } - case legacyValues.every((value) => value instanceof CubicSplineNumberValue): { - assertIsTrue(legacyEasingMethodConverter.nil); - const track = new RealTrack(); - installPathAndSetter(track); - const interpMethod = interpolate ? RealInterpMode.CUBIC : RealInterpMode.CONSTANT; - track.channel.curve.assignSorted(times, (legacyValues as CubicSplineNumberValue[]).map((value) => new RealKeyframeValue({ - value: value.dataPoint, - startTangent: value.inTangent, - endTangent: value.outTangent, - interpMode: interpMethod, - }))); - newTracks.push(track); - return; - } - case legacyValues.every((value) => value instanceof CubicSplineVec2Value): - case legacyValues.every((value) => value instanceof CubicSplineVec3Value): - case legacyValues.every((value) => value instanceof CubicSplineVec4Value): { - assertIsTrue(legacyEasingMethodConverter.nil); - type Vec4plus = CubicSplineVec4Value[]; - type Vec3plus = (CubicSplineVec3Value | CubicSplineVec4Value)[]; - type Vec2plus = (CubicSplineVec2Value | CubicSplineVec3Value | CubicSplineVec4Value)[]; - const components = firstValue instanceof CubicSplineVec2Value ? 2 : firstValue instanceof CubicSplineVec3Value ? 3 : 4; - const track = new VectorTrack(); - installPathAndSetter(track); - track.componentsCount = components; - const [x, y, z, w] = track.getChannels(); - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - const valueToFrame = (value: number, startTangent: number, endTangent: number): RealKeyframeValue => new RealKeyframeValue({ - value, - startTangent, - endTangent, - interpMode: interpMethod, - }); - switch (components) { - case 4: - w.curve.assignSorted(times, (legacyValues as Vec4plus).map( - (value) => valueToFrame(value.dataPoint.w, value.inTangent.w, value.outTangent.w), - )); - // falls through - case 3: - z.curve.assignSorted(times, (legacyValues as Vec3plus).map( - (value) => valueToFrame(value.dataPoint.z, value.inTangent.z, value.outTangent.z), - )); - // falls through - default: - x.curve.assignSorted(times, (legacyValues as Vec2plus).map( - (value) => valueToFrame(value.dataPoint.y, value.inTangent.y, value.outTangent.y), - )); - y.curve.assignSorted(times, (legacyValues as Vec2plus).map( - (value) => valueToFrame(value.dataPoint.x, value.inTangent.x, value.outTangent.x), - )); - break; - } - newTracks.push(track); - return; - } - } // End switch - } - - const objectTrack = new ObjectTrack(); - installPathAndSetter(objectTrack); - objectTrack.channel.curve.assignSorted(times, legacyValues); - newTracks.push(objectTrack); - }; - - convertCurve(); - } - + const newTracks = legacy.convertAnimationClipLegacyData(legacyData); for (const track of newTracks) { this.addTrack(track); } @@ -1477,334 +803,13 @@ export class AnimationClip extends Asset { legacyCC.AnimationClip = AnimationClip; -type Binder = (path: TrackPath, setter: IValueProxyFactory | undefined) => undefined | RuntimeBinding; - -type RuntimeBinding = { - setValue(value: unknown): void; - - getValue?(): unknown; -}; - // #region Data compression -@ccclass(`${CLASS_NAME_PREFIX_ANIM}CompressedData`) -class CompressedData { - public compressRealTrack (track: RealTrack) { - const curve = track.channel.curve; - const mayBeCompressed = KeySharedRealCurves.allowedForCurve(curve); - if (!mayBeCompressed) { - return false; - } - this._tracks.push({ - type: CompressedDataTrackType.FLOAT, - path: track.path, - setter: track.setter, - components: [this._addRealCurve(curve)], - }); - return true; - } - - public compressVectorTrack (vectorTrack: VectorTrack) { - const nComponents = vectorTrack.componentsCount; - const channels = vectorTrack.getChannels(); - const mayBeCompressed = channels.every(({ curve }) => KeySharedRealCurves.allowedForCurve(curve)); - if (!mayBeCompressed) { - return false; - } - const components = new Array(nComponents); - for (let i = 0; i < nComponents; ++i) { - const channel = channels[i]; - components[i] = this._addRealCurve(channel.curve); - } - this._tracks.push({ - type: - nComponents === 2 - ? CompressedDataTrackType.VEC2 - : nComponents === 3 - ? CompressedDataTrackType.VEC3 - : CompressedDataTrackType.VEC4, - path: vectorTrack.path, - setter: vectorTrack.setter, - components, - }); - return true; - } - - public compressQuatTrack (track: QuaternionTrack) { - const curve = track.channel.curve; - const mayBeCompressed = KeySharedQuaternionCurves.allowedForCurve(curve); - if (!mayBeCompressed) { - return false; - } - this._quatTracks.push({ - path: track.path, - setter: track.setter, - pointer: this._addQuaternionCurve(curve), - }); - return true; - } - - public createEval (binder: Binder) { - const compressedDataEvalStatus: CompressedDataEvalStatus = { - keySharedCurvesEvalStatuses: [], - trackEvalStatuses: [], - keysSharedQuatCurvesEvalStatues: [], - quatTrackEvalStatuses: [], - }; - - const { - keySharedCurvesEvalStatuses, - trackEvalStatuses, - keysSharedQuatCurvesEvalStatues, - quatTrackEvalStatuses, - } = compressedDataEvalStatus; - - for (const curves of this._curves) { - keySharedCurvesEvalStatuses.push({ - curves, - result: new Array(curves.curveCount).fill(0.0), - }); - } - - for (const track of this._tracks) { - const trackTarget = binder(track.path, track.setter); - if (!trackTarget) { - continue; - } - let immediate: CompressedTrackImmediate | undefined; - switch (track.type) { - default: - case CompressedDataTrackType.FLOAT: - break; - case CompressedDataTrackType.VEC2: - immediate = new Vec2(); - break; - case CompressedDataTrackType.VEC3: - immediate = new Vec3(); - break; - case CompressedDataTrackType.VEC4: - immediate = new Vec4(); - break; - } - trackEvalStatuses.push({ - type: track.type, - target: trackTarget, - curves: track.components, - immediate, - }); - } - - for (const curves of this._quatCurves) { - keysSharedQuatCurvesEvalStatues.push({ - curves, - result: Array.from({ length: curves.curveCount }, () => new Quat()), - }); - } - - for (const track of this._quatTracks) { - const trackTarget = binder(track.path, track.setter); - if (!trackTarget) { - continue; - } - quatTrackEvalStatuses.push({ - target: trackTarget, - curve: track.pointer, - }); - } - - return new CompressedDataEvaluator(compressedDataEvalStatus); - } - - public collectAnimatedJoints () { - const joints: string[] = []; - - for (const track of this._tracks) { - if (!track.setter && isTargetingTRS(track.path)) { - const { path } = track.path[0]; - joints.push(path); - } - } - - return joints; - } - - @serializable - private _curves: KeySharedRealCurves[] = []; - - @serializable - private _tracks: CompressedTrack[] = []; - - @serializable - private _quatCurves: KeySharedQuaternionCurves[] = []; - - @serializable - private _quatTracks: CompressedQuatTrack[] = []; - - private _addRealCurve (curve: RealCurve): CompressedCurvePointer { - const times = Array.from(curve.times()); - let iKeySharedCurves = this._curves.findIndex((shared) => shared.matchCurve(curve)); - if (iKeySharedCurves < 0) { - iKeySharedCurves = this._curves.length; - const keySharedCurves = new KeySharedRealCurves(times); - this._curves.push(keySharedCurves); - } - const iCurve = this._curves[iKeySharedCurves].curveCount; - this._curves[iKeySharedCurves].addCurve(curve); - return { - shared: iKeySharedCurves, - component: iCurve, - }; - } - - public _addQuaternionCurve (curve: QuaternionCurve): CompressedQuatCurvePointer { - const times = Array.from(curve.times()); - let iKeySharedCurves = this._quatCurves.findIndex((shared) => shared.matchCurve(curve)); - if (iKeySharedCurves < 0) { - iKeySharedCurves = this._quatCurves.length; - const keySharedCurves = new KeySharedQuaternionCurves(times); - this._quatCurves.push(keySharedCurves); - } - const iCurve = this._quatCurves[iKeySharedCurves].curveCount; - this._quatCurves[iKeySharedCurves].addCurve(curve); - return { - shared: iKeySharedCurves, - curve: iCurve, - }; - } - - public validate () { - return this._tracks.length > 0; - } -} - -class CompressedDataEvaluator { - constructor (compressedDataEvalStatus: CompressedDataEvalStatus) { - this._compressedDataEvalStatus = compressedDataEvalStatus; - } - - public evaluate (time: number) { - const { - keySharedCurvesEvalStatuses, - trackEvalStatuses: compressedTrackEvalStatuses, - keysSharedQuatCurvesEvalStatues, - quatTrackEvalStatuses, - } = this._compressedDataEvalStatus; - - const getPreEvaluated = (pointer: CompressedCurvePointer) => keySharedCurvesEvalStatuses[pointer.shared].result[pointer.component]; - - for (const { curves, result } of keySharedCurvesEvalStatuses) { - curves.evaluate(time, result); - } - - for (const { type, target, immediate, curves } of compressedTrackEvalStatuses) { - let value: unknown = immediate; - switch (type) { - default: - break; - case CompressedDataTrackType.FLOAT: - value = getPreEvaluated(curves[0]); - break; - case CompressedDataTrackType.VEC2: - Vec2.set( - value as Vec2, - getPreEvaluated(curves[0]), - getPreEvaluated(curves[1]), - ); - break; - case CompressedDataTrackType.VEC3: - Vec3.set( - value as Vec3, - getPreEvaluated(curves[0]), - getPreEvaluated(curves[1]), - getPreEvaluated(curves[2]), - ); - break; - case CompressedDataTrackType.VEC4: - Vec4.set( - value as Vec4, - getPreEvaluated(curves[0]), - getPreEvaluated(curves[1]), - getPreEvaluated(curves[2]), - getPreEvaluated(curves[4]), - ); - break; - } - target.setValue(value); - } - - for (const { curves, result } of keysSharedQuatCurvesEvalStatues) { - curves.evaluate(time, result); - } - - for (const { target, curve } of quatTrackEvalStatuses) { - target.setValue(keysSharedQuatCurvesEvalStatues[curve.shared].result[curve.curve]); - } - } - - private _compressedDataEvalStatus: CompressedDataEvalStatus; -} - -interface CompressedTrack { - path: TrackPath; - setter: IValueProxyFactory | undefined; - type: CompressedDataTrackType; - components: CompressedCurvePointer[]; -} - -interface CompressedQuatTrack { - path: TrackPath; - setter: IValueProxyFactory | undefined; - pointer: CompressedQuatCurvePointer; -} - -enum CompressedDataTrackType { - FLOAT, - VEC2, - VEC3, - VEC4, -} - interface TrackEvalStatus { binding: RuntimeBinding; trackEval: TrackEval; } -type CompressedTrackImmediate = Vec2 | Vec3 | Vec4; - -interface CompressedDataEvalStatus { - keySharedCurvesEvalStatuses: Array<{ - curves: KeySharedRealCurves; - result: number[]; - }>; - - trackEvalStatuses: Array<{ - type: CompressedDataTrackType; - target: RuntimeBinding; - immediate: CompressedTrackImmediate | undefined; - curves: CompressedCurvePointer[]; - }>; - - keysSharedQuatCurvesEvalStatues: Array<{ - curves: KeySharedQuaternionCurves; - result: Quat[]; - }>; - - quatTrackEvalStatuses: Array<{ - target: RuntimeBinding; - curve: CompressedQuatCurvePointer; - }>; -} - -interface CompressedCurvePointer { - shared: number; - component: number; -} - -interface CompressedQuatCurvePointer { - shared: number; - curve: number; -} - // #endregion interface AnimationClipEvalContext { /** @@ -2069,31 +1074,6 @@ function createGeneralBinding ( return null; } -type TrsTrackPath = [HierarchyPath, 'position' | 'rotation' | 'scale' | 'eulerAngles']; - -function isTargetingTRS (path: TargetPath[]): path is TrsTrackPath { - let prs: string | undefined; - if (path.length === 1 && typeof path[0] === 'string') { - prs = path[0]; - } else if (path.length > 1) { - for (let i = 0; i < path.length - 1; ++i) { - if (!(path[i] instanceof HierarchyPath)) { - return false; - } - } - prs = path[path.length - 1] as string; - } - switch (prs) { - case 'position': - case 'scale': - case 'rotation': - case 'eulerAngles': - return true; - default: - return false; - } -} - function createRuntimeBinding (target: unknown, trackPath: TrackPath, setter?: IValueProxyFactory): null | RuntimeBinding { const lastPath = trackPath[trackPath.length - 1]; if (trackPath.length !== 0 && isPropertyPath(lastPath) && !setter) { diff --git a/cocos/core/animation/animation.ts b/cocos/core/animation/animation.ts index 9887a9192bb..71dc3a3d0e4 100644 --- a/cocos/core/animation/animation.ts +++ b/cocos/core/animation/animation.ts @@ -33,12 +33,10 @@ export * from './value-proxy'; export { UniformProxyFactory } from './value-proxy-factories/uniform'; export { MorphWeightValueProxy, MorphWeightsValueProxy, MorphWeightsAllValueProxy } from './value-proxy-factories/morph-weights'; export * from './cubic-spline-value'; -export { - Track, - RealTrack, - IntegerTrack, - VectorTrack, - QuaternionTrack, - ColorTrack, - ObjectTrack, -} from './animation-clip'; +export { Track } from './tracks/track'; +export { RealTrack } from './tracks/real-track'; +export { IntegerTrack } from './tracks/integer-track'; +export { VectorTrack } from './tracks/vector-track'; +export { QuaternionTrack } from './tracks/quat-track'; +export { ColorTrack } from './tracks/color-track'; +export { ObjectTrack } from './tracks/object-track'; diff --git a/cocos/core/animation/compression/compressed-data.ts b/cocos/core/animation/compression/compressed-data.ts new file mode 100644 index 00000000000..63c55c915d3 --- /dev/null +++ b/cocos/core/animation/compression/compressed-data.ts @@ -0,0 +1,323 @@ +import { QuaternionCurve, RealCurve } from '../../curves'; +import { KeySharedQuaternionCurves, KeySharedRealCurves } from '../../curves/keys-shared-curves'; +import { ccclass, serializable } from '../../data/decorators'; +import { Quat, Vec2, Vec3, Vec4 } from '../../math'; +import { CLASS_NAME_PREFIX_ANIM } from '../define'; +import { QuaternionTrack } from '../tracks/quat-track'; +import { RealTrack } from '../tracks/real-track'; +import { Binder, isTargetingTRS, RuntimeBinding, TrackPath } from '../tracks/track'; +import { VectorTrack } from '../tracks/vector-track'; +import { IValueProxyFactory } from '../value-proxy'; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}CompressedData`) +export class CompressedData { + public compressRealTrack (track: RealTrack) { + const curve = track.channel.curve; + const mayBeCompressed = KeySharedRealCurves.allowedForCurve(curve); + if (!mayBeCompressed) { + return false; + } + this._tracks.push({ + type: CompressedDataTrackType.FLOAT, + path: track.path, + setter: track.setter, + components: [this._addRealCurve(curve)], + }); + return true; + } + + public compressVectorTrack (vectorTrack: VectorTrack) { + const nComponents = vectorTrack.componentsCount; + const channels = vectorTrack.getChannels(); + const mayBeCompressed = channels.every(({ curve }) => KeySharedRealCurves.allowedForCurve(curve)); + if (!mayBeCompressed) { + return false; + } + const components = new Array(nComponents); + for (let i = 0; i < nComponents; ++i) { + const channel = channels[i]; + components[i] = this._addRealCurve(channel.curve); + } + this._tracks.push({ + type: + nComponents === 2 + ? CompressedDataTrackType.VEC2 + : nComponents === 3 + ? CompressedDataTrackType.VEC3 + : CompressedDataTrackType.VEC4, + path: vectorTrack.path, + setter: vectorTrack.setter, + components, + }); + return true; + } + + public compressQuatTrack (track: QuaternionTrack) { + const curve = track.channel.curve; + const mayBeCompressed = KeySharedQuaternionCurves.allowedForCurve(curve); + if (!mayBeCompressed) { + return false; + } + this._quatTracks.push({ + path: track.path, + setter: track.setter, + pointer: this._addQuaternionCurve(curve), + }); + return true; + } + + public createEval (binder: Binder) { + const compressedDataEvalStatus: CompressedDataEvalStatus = { + keySharedCurvesEvalStatuses: [], + trackEvalStatuses: [], + keysSharedQuatCurvesEvalStatues: [], + quatTrackEvalStatuses: [], + }; + + const { + keySharedCurvesEvalStatuses, + trackEvalStatuses, + keysSharedQuatCurvesEvalStatues, + quatTrackEvalStatuses, + } = compressedDataEvalStatus; + + for (const curves of this._curves) { + keySharedCurvesEvalStatuses.push({ + curves, + result: new Array(curves.curveCount).fill(0.0), + }); + } + + for (const track of this._tracks) { + const trackTarget = binder(track.path, track.setter); + if (!trackTarget) { + continue; + } + let immediate: CompressedTrackImmediate | undefined; + switch (track.type) { + default: + case CompressedDataTrackType.FLOAT: + break; + case CompressedDataTrackType.VEC2: + immediate = new Vec2(); + break; + case CompressedDataTrackType.VEC3: + immediate = new Vec3(); + break; + case CompressedDataTrackType.VEC4: + immediate = new Vec4(); + break; + } + trackEvalStatuses.push({ + type: track.type, + target: trackTarget, + curves: track.components, + immediate, + }); + } + + for (const curves of this._quatCurves) { + keysSharedQuatCurvesEvalStatues.push({ + curves, + result: Array.from({ length: curves.curveCount }, () => new Quat()), + }); + } + + for (const track of this._quatTracks) { + const trackTarget = binder(track.path, track.setter); + if (!trackTarget) { + continue; + } + quatTrackEvalStatuses.push({ + target: trackTarget, + curve: track.pointer, + }); + } + + return new CompressedDataEvaluator(compressedDataEvalStatus); + } + + public collectAnimatedJoints () { + const joints: string[] = []; + + for (const track of this._tracks) { + if (!track.setter && isTargetingTRS(track.path)) { + const { path } = track.path[0]; + joints.push(path); + } + } + + return joints; + } + + @serializable + private _curves: KeySharedRealCurves[] = []; + + @serializable + private _tracks: CompressedTrack[] = []; + + @serializable + private _quatCurves: KeySharedQuaternionCurves[] = []; + + @serializable + private _quatTracks: CompressedQuatTrack[] = []; + + private _addRealCurve (curve: RealCurve): CompressedCurvePointer { + const times = Array.from(curve.times()); + let iKeySharedCurves = this._curves.findIndex((shared) => shared.matchCurve(curve)); + if (iKeySharedCurves < 0) { + iKeySharedCurves = this._curves.length; + const keySharedCurves = new KeySharedRealCurves(times); + this._curves.push(keySharedCurves); + } + const iCurve = this._curves[iKeySharedCurves].curveCount; + this._curves[iKeySharedCurves].addCurve(curve); + return { + shared: iKeySharedCurves, + component: iCurve, + }; + } + + public _addQuaternionCurve (curve: QuaternionCurve): CompressedQuatCurvePointer { + const times = Array.from(curve.times()); + let iKeySharedCurves = this._quatCurves.findIndex((shared) => shared.matchCurve(curve)); + if (iKeySharedCurves < 0) { + iKeySharedCurves = this._quatCurves.length; + const keySharedCurves = new KeySharedQuaternionCurves(times); + this._quatCurves.push(keySharedCurves); + } + const iCurve = this._quatCurves[iKeySharedCurves].curveCount; + this._quatCurves[iKeySharedCurves].addCurve(curve); + return { + shared: iKeySharedCurves, + curve: iCurve, + }; + } + + public validate () { + return this._tracks.length > 0; + } +} + +export class CompressedDataEvaluator { + constructor (compressedDataEvalStatus: CompressedDataEvalStatus) { + this._compressedDataEvalStatus = compressedDataEvalStatus; + } + + public evaluate (time: number) { + const { + keySharedCurvesEvalStatuses, + trackEvalStatuses: compressedTrackEvalStatuses, + keysSharedQuatCurvesEvalStatues, + quatTrackEvalStatuses, + } = this._compressedDataEvalStatus; + + const getPreEvaluated = (pointer: CompressedCurvePointer) => keySharedCurvesEvalStatuses[pointer.shared].result[pointer.component]; + + for (const { curves, result } of keySharedCurvesEvalStatuses) { + curves.evaluate(time, result); + } + + for (const { type, target, immediate, curves } of compressedTrackEvalStatuses) { + let value: unknown = immediate; + switch (type) { + default: + break; + case CompressedDataTrackType.FLOAT: + value = getPreEvaluated(curves[0]); + break; + case CompressedDataTrackType.VEC2: + Vec2.set( + value as Vec2, + getPreEvaluated(curves[0]), + getPreEvaluated(curves[1]), + ); + break; + case CompressedDataTrackType.VEC3: + Vec3.set( + value as Vec3, + getPreEvaluated(curves[0]), + getPreEvaluated(curves[1]), + getPreEvaluated(curves[2]), + ); + break; + case CompressedDataTrackType.VEC4: + Vec4.set( + value as Vec4, + getPreEvaluated(curves[0]), + getPreEvaluated(curves[1]), + getPreEvaluated(curves[2]), + getPreEvaluated(curves[4]), + ); + break; + } + target.setValue(value); + } + + for (const { curves, result } of keysSharedQuatCurvesEvalStatues) { + curves.evaluate(time, result); + } + + for (const { target, curve } of quatTrackEvalStatuses) { + target.setValue(keysSharedQuatCurvesEvalStatues[curve.shared].result[curve.curve]); + } + } + + private _compressedDataEvalStatus: CompressedDataEvalStatus; +} + +interface CompressedTrack { + path: TrackPath; + setter: IValueProxyFactory | undefined; + type: CompressedDataTrackType; + components: CompressedCurvePointer[]; +} + +interface CompressedQuatTrack { + path: TrackPath; + setter: IValueProxyFactory | undefined; + pointer: CompressedQuatCurvePointer; +} + +enum CompressedDataTrackType { + FLOAT, + VEC2, + VEC3, + VEC4, +} + +type CompressedTrackImmediate = Vec2 | Vec3 | Vec4; + +interface CompressedDataEvalStatus { + keySharedCurvesEvalStatuses: Array<{ + curves: KeySharedRealCurves; + result: number[]; + }>; + + trackEvalStatuses: Array<{ + type: CompressedDataTrackType; + target: RuntimeBinding; + immediate: CompressedTrackImmediate | undefined; + curves: CompressedCurvePointer[]; + }>; + + keysSharedQuatCurvesEvalStatues: Array<{ + curves: KeySharedQuaternionCurves; + result: Quat[]; + }>; + + quatTrackEvalStatuses: Array<{ + target: RuntimeBinding; + curve: CompressedQuatCurvePointer; + }>; +} + +interface CompressedCurvePointer { + shared: number; + component: number; +} + +interface CompressedQuatCurvePointer { + shared: number; + curve: number; +} diff --git a/cocos/core/animation/define.ts b/cocos/core/animation/define.ts new file mode 100644 index 00000000000..4d3e9946e3d --- /dev/null +++ b/cocos/core/animation/define.ts @@ -0,0 +1,3 @@ +export const CLASS_NAME_PREFIX_ANIM = 'cc.animation.'; + +export const createEvalSymbol = Symbol('CreateEval'); diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts index abcff042f33..2b74e76d792 100644 --- a/cocos/core/animation/legacy-clip-data.ts +++ b/cocos/core/animation/legacy-clip-data.ts @@ -5,8 +5,18 @@ import { BezierControlPoints } from './bezier'; import { CompactValueTypeArray } from '../data/utils/compact-value-type-array'; import { serializable } from '../data/decorators'; import { AnimCurve, RatioSampler } from './animation-curve'; -import { RealCurve, RealInterpMode, TangentWeightMode } from '../curves'; +import { QuaternionInterpMode, RealCurve, RealInterpMode, RealKeyframeValue, TangentWeightMode } from '../curves'; import { assertIsTrue } from '../data/utils/asserts'; +import { Track } from './tracks/track'; +import { UntypedTrack } from './tracks/untyped-track'; +import { warn } from '../platform'; +import { RealTrack } from './tracks/real-track'; +import { Color, Quat, Vec2, Vec3, Vec4 } from '../math'; +import { CubicSplineNumberValue, CubicSplineVec2Value, CubicSplineVec3Value, CubicSplineVec4Value } from './cubic-spline-value'; +import { ColorTrack } from './tracks/color-track'; +import { VectorTrack } from './tracks/vector-track'; +import { QuaternionTrack } from './tracks/quat-track'; +import { ObjectTrack } from './tracks/object-track'; /** * 表示曲线值,曲线值可以是任意类型,但必须符合插值方式的要求。 @@ -220,7 +230,7 @@ export interface LegacyNodeCurveData { // #endregion -export class LegacyEasingMethodConverter { +class LegacyEasingMethodConverter { constructor (legacyCurveData: LegacyClipCurveData, keyframesCount: number) { const { easingMethods } = legacyCurveData; if (Array.isArray(easingMethods)) { @@ -371,3 +381,215 @@ function powerToBernstein ([p0, p1, p2, p3]: [number, number, number, number], b bernstein[2] = p2 / 3.0 + p3; bernstein[3] = p3; } + +export function convertAnimationClipLegacyData (animationClipLegacyData: AnimationClipLegacyData) { + const newTracks: Track[] = []; + + const { + keys: legacyKeys, + curves: legacyCurves, + commonTargets: legacyCommonTargets, + } = animationClipLegacyData; + + const untypedTracks = legacyCommonTargets.map((legacyCommonTarget) => { + const track = new UntypedTrack(); + track.path = legacyCommonTarget.modifiers; + track.setter = legacyCommonTarget.valueAdapter; + newTracks.push(track); + return track; + }); + + for (const legacyCurve of legacyCurves) { + const legacyCurveData = legacyCurve.data; + const legacyValues = legacyCurveData.values; + if (legacyValues.length === 0) { + // Legacy clip did not record type info. + continue; + } + const legacyKeysIndex = legacyCurveData.keys; + // Rule: negative index means single frame. + const times = legacyKeysIndex < 0 ? [0.0] : legacyKeys[legacyCurveData.keys]; + const firstValue = legacyValues[0]; + // Rule: default to true. + const interpolate = legacyCurveData ?? true; + // Rule: _arrayLength only used for morph target, internally. + assertIsTrue(typeof legacyCurveData._arrayLength !== 'number' || typeof firstValue === 'number'); + const legacyEasingMethodConverter = new LegacyEasingMethodConverter(legacyCurveData, times.length); + + const installPathAndSetter = (track: Track) => { + track.path = legacyCurve.modifiers; + track.setter = legacyCurve.valueAdapter; + }; + + let legacyCommonTargetCurve: RealCurve | undefined; + if (typeof legacyCurve.commonTarget === 'number') { + // Rule: common targets should only target Vectors/`Size`/`Color`. + if (!legacyValues.every((value) => typeof value === 'number')) { + warn(`Incorrect curve.`); + continue; + } + // Rule: Each curve that has common target should be numeric curve and targets string property. + if (legacyCurve.valueAdapter || legacyCurve.modifiers.length !== 1 || typeof legacyCurve.modifiers[0] !== 'string') { + warn(`Incorrect curve.`); + continue; + } + const propertyName = legacyCurve.modifiers[0]; + const untypedTrack = untypedTracks[legacyCurve.commonTarget]; + const { curve } = untypedTrack.addChannel(propertyName); + legacyCommonTargetCurve = curve; + } + + const convertCurve = () => { + if (typeof firstValue === 'number') { + if (!legacyValues.every((value) => typeof value === 'number')) { + warn(`Misconfigured curve.`); + return; + } + let realCurve: RealCurve; + if (legacyCommonTargetCurve) { + realCurve = legacyCommonTargetCurve; + } else { + const track = new RealTrack(); + installPathAndSetter(track); + newTracks.push(track); + realCurve = track.channel.curve; + } + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + realCurve.assignSorted(times, (legacyValues as number[]).map((value) => new RealKeyframeValue({ value, interpMode: interpMethod }))); + legacyEasingMethodConverter.convert(realCurve); + return; + } else if (typeof firstValue === 'object') { + switch (true) { + default: + break; + case legacyValues.every((value) => value instanceof Vec2): + case legacyValues.every((value) => value instanceof Vec3): + case legacyValues.every((value) => value instanceof Vec4): { + type Vec4plus = Vec4[]; + type Vec3plus = (Vec3 | Vec4)[]; + type Vec2plus = (Vec2 | Vec3 | Vec4)[]; + const components = firstValue instanceof Vec2 ? 2 : firstValue instanceof Vec3 ? 3 : 4; + const track = new VectorTrack(); + installPathAndSetter(track); + track.componentsCount = components; + const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.getChannels(); + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); + switch (components) { + case 4: + w.assignSorted(times, (legacyValues as Vec4plus).map((value) => valueToFrame(value.w))); + legacyEasingMethodConverter.convert(w); + // falls through + case 3: + z.assignSorted(times, (legacyValues as Vec3plus).map((value) => valueToFrame(value.z))); + legacyEasingMethodConverter.convert(z); + // falls through + default: + x.assignSorted(times, (legacyValues as Vec2plus).map((value) => valueToFrame(value.x))); + legacyEasingMethodConverter.convert(x); + y.assignSorted(times, (legacyValues as Vec2plus).map((value) => valueToFrame(value.y))); + legacyEasingMethodConverter.convert(y); + break; + } + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof Quat): { + assertIsTrue(legacyEasingMethodConverter.nil); + const track = new QuaternionTrack(); + installPathAndSetter(track); + const interpMode = interpolate ? QuaternionInterpMode.SLERP : QuaternionInterpMode.CONSTANT; + track.channel.curve.assignSorted(times, (legacyValues as Quat[]).map((value) => ({ + value: Quat.clone(value), + interpMode, + }))); + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof Color): { + const track = new ColorTrack(); + installPathAndSetter(track); + const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.getChannels(); + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); + r.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.r))); + legacyEasingMethodConverter.convert(r); + g.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.g))); + legacyEasingMethodConverter.convert(g); + b.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.b))); + legacyEasingMethodConverter.convert(b); + a.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.a))); + legacyEasingMethodConverter.convert(a); + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof CubicSplineNumberValue): { + assertIsTrue(legacyEasingMethodConverter.nil); + const track = new RealTrack(); + installPathAndSetter(track); + const interpMethod = interpolate ? RealInterpMode.CUBIC : RealInterpMode.CONSTANT; + track.channel.curve.assignSorted(times, (legacyValues as CubicSplineNumberValue[]).map((value) => new RealKeyframeValue({ + value: value.dataPoint, + startTangent: value.inTangent, + endTangent: value.outTangent, + interpMode: interpMethod, + }))); + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof CubicSplineVec2Value): + case legacyValues.every((value) => value instanceof CubicSplineVec3Value): + case legacyValues.every((value) => value instanceof CubicSplineVec4Value): { + assertIsTrue(legacyEasingMethodConverter.nil); + type Vec4plus = CubicSplineVec4Value[]; + type Vec3plus = (CubicSplineVec3Value | CubicSplineVec4Value)[]; + type Vec2plus = (CubicSplineVec2Value | CubicSplineVec3Value | CubicSplineVec4Value)[]; + const components = firstValue instanceof CubicSplineVec2Value ? 2 : firstValue instanceof CubicSplineVec3Value ? 3 : 4; + const track = new VectorTrack(); + installPathAndSetter(track); + track.componentsCount = components; + const [x, y, z, w] = track.getChannels(); + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const valueToFrame = (value: number, startTangent: number, endTangent: number): RealKeyframeValue => new RealKeyframeValue({ + value, + startTangent, + endTangent, + interpMode: interpMethod, + }); + switch (components) { + case 4: + w.curve.assignSorted(times, (legacyValues as Vec4plus).map( + (value) => valueToFrame(value.dataPoint.w, value.inTangent.w, value.outTangent.w), + )); + // falls through + case 3: + z.curve.assignSorted(times, (legacyValues as Vec3plus).map( + (value) => valueToFrame(value.dataPoint.z, value.inTangent.z, value.outTangent.z), + )); + // falls through + default: + x.curve.assignSorted(times, (legacyValues as Vec2plus).map( + (value) => valueToFrame(value.dataPoint.y, value.inTangent.y, value.outTangent.y), + )); + y.curve.assignSorted(times, (legacyValues as Vec2plus).map( + (value) => valueToFrame(value.dataPoint.x, value.inTangent.x, value.outTangent.x), + )); + break; + } + newTracks.push(track); + return; + } + } // End switch + } + + const objectTrack = new ObjectTrack(); + installPathAndSetter(objectTrack); + objectTrack.channel.curve.assignSorted(times, legacyValues); + newTracks.push(objectTrack); + }; + + convertCurve(); + } + + return newTracks; +} diff --git a/cocos/core/animation/tracks/color-track.ts b/cocos/core/animation/tracks/color-track.ts new file mode 100644 index 00000000000..8effa1c593c --- /dev/null +++ b/cocos/core/animation/tracks/color-track.ts @@ -0,0 +1,69 @@ +import { ccclass, serializable } from 'cc.decorator'; +import { IntegerCurve, RealCurve } from '../../curves'; +import { Color } from '../../math'; +import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; +import { Channel, IntegerChannel, RuntimeBinding, Track } from './track'; +import { maskIfEmpty } from './utils'; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}ColorTrack`) +export class ColorTrack extends Track { + constructor () { + super(); + this._channels = new Array(4) as ColorTrack['_channels']; + for (let i = 0; i < this._channels.length; ++i) { + const channel = new Channel(new IntegerCurve()); + channel.name = 'R'; + this._channels[i] = channel; + } + } + + public getChannels () { + return this._channels; + } + + public [createEvalSymbol] () { + return new ColorTrackEval( + maskIfEmpty(this._channels[0].curve), + maskIfEmpty(this._channels[1].curve), + maskIfEmpty(this._channels[2].curve), + maskIfEmpty(this._channels[3].curve), + ); + } + + @serializable + private _channels: [IntegerChannel, IntegerChannel, IntegerChannel, IntegerChannel]; +} + +export class ColorTrackEval { + constructor ( + private _x: TCurve | undefined, + private _y: TCurve | undefined, + private _z: TCurve | undefined, + private _w: TCurve | undefined, + ) { + + } + + public evaluate (time: number, runtimeBinding: RuntimeBinding) { + if ((!this._x || !this._y || !this._z || !this._w) && runtimeBinding.getValue) { + Color.copy(this._result, runtimeBinding.getValue() as Color); + } + + if (this._x) { + this._result.r = this._x.evaluate(time); + } + if (this._y) { + this._result.g = this._y.evaluate(time); + } + if (this._z) { + this._result.b = this._z.evaluate(time); + } + if (this._w) { + this._result.a = this._w.evaluate(time); + } + + return this._result; + } + + private _result: Color = new Color(); +} diff --git a/cocos/core/animation/tracks/integer-track.ts b/cocos/core/animation/tracks/integer-track.ts new file mode 100644 index 00000000000..18a6f3aeef2 --- /dev/null +++ b/cocos/core/animation/tracks/integer-track.ts @@ -0,0 +1,11 @@ +import { ccclass } from 'cc.decorator'; +import { IntegerCurve } from '../../curves'; +import { CLASS_NAME_PREFIX_ANIM } from '../define'; +import { SingleChannelTrack } from './track'; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}IntegerTrack`) +export class IntegerTrack extends SingleChannelTrack { + protected createCurve () { + return new IntegerCurve(); + } +} diff --git a/cocos/core/animation/tracks/object-track.ts b/cocos/core/animation/tracks/object-track.ts new file mode 100644 index 00000000000..baed5686cf2 --- /dev/null +++ b/cocos/core/animation/tracks/object-track.ts @@ -0,0 +1,11 @@ +import { ccclass } from 'cc.decorator'; +import { ObjectCurve } from '../../curves'; +import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; +import { SingleChannelTrack } from './track'; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}ObjectTrack`) +export class ObjectTrack extends SingleChannelTrack> { + protected createCurve () { + return new ObjectCurve(); + } +} diff --git a/cocos/core/animation/tracks/quat-track.ts b/cocos/core/animation/tracks/quat-track.ts new file mode 100644 index 00000000000..385e0f00dd5 --- /dev/null +++ b/cocos/core/animation/tracks/quat-track.ts @@ -0,0 +1,29 @@ +import { ccclass } from 'cc.decorator'; +import { QuaternionCurve } from '../../curves'; +import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; +import { SingleChannelTrack } from './track'; +import { Quat } from '../../math'; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}QuaternionTrack`) +export class QuaternionTrack extends SingleChannelTrack { + protected createCurve () { + return new QuaternionCurve(); + } + + public [createEvalSymbol] () { + return new QuatTrackEval(this.getChannels()[0].curve); + } +} + +export class QuatTrackEval { + constructor (private _curve: QuaternionCurve) { + + } + + public evaluate (time: number) { + this._curve.evaluate(time, this._result); + return this._result; + } + + private _result: Quat = new Quat(); +} diff --git a/cocos/core/animation/tracks/real-track.ts b/cocos/core/animation/tracks/real-track.ts new file mode 100644 index 00000000000..760691457fa --- /dev/null +++ b/cocos/core/animation/tracks/real-track.ts @@ -0,0 +1,11 @@ +import { ccclass } from 'cc.decorator'; +import { RealCurve } from '../../curves'; +import { CLASS_NAME_PREFIX_ANIM } from '../define'; +import { SingleChannelTrack } from './track'; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}RealTrack`) +export class RealTrack extends SingleChannelTrack { + protected createCurve () { + return new RealCurve(); + } +} diff --git a/cocos/core/animation/tracks/track.ts b/cocos/core/animation/tracks/track.ts new file mode 100644 index 00000000000..7f34353252e --- /dev/null +++ b/cocos/core/animation/tracks/track.ts @@ -0,0 +1,134 @@ +import { ccclass, serializable } from 'cc.decorator'; +import type { IntegerCurve, ObjectCurve, QuaternionCurve, RealCurve } from '../../curves'; +import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; +import { HierarchyPath, TargetPath } from '../target-path'; +import { IValueProxyFactory } from '../value-proxy'; +import { Range } from './utils'; + +export type TrackPath = TargetPath[]; + +/** + * A track describes the path of animate a target. + * It's the basic unit of animation clip. + */ +@ccclass(`${CLASS_NAME_PREFIX_ANIM}Track`) +export class Track { + @serializable + public path: TrackPath = []; + + @serializable + public setter!: IValueProxyFactory | undefined; + + public getChannels (): Channel[] { + return []; + } + + public getRange (): Range { + const range: Range = { min: Infinity, max: -Infinity }; + for (const channel of this.getChannels()) { + range.min = Math.min(range.min, channel.curve.rangeMin); + range.max = Math.max(range.max, channel.curve.rangeMax); + } + return range; + } + + public [createEvalSymbol] (runtimeBinding: RuntimeBinding): TrackEval { + throw new Error(`No Impl`); + } +} + +export interface TrackEval { + /** + * Evaluates the track. + * @param time The time. + */ + evaluate(time: number, runtimeBinding: RuntimeBinding): unknown; +} + +export type Curve = RealCurve | IntegerCurve | QuaternionCurve | ObjectCurve; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}Channel`) +export class Channel { + constructor (curve: T) { + this._curve = curve; + } + + @serializable + public name = ''; + + get curve () { + return this._curve; + } + + @serializable + private _curve!: T; +} + +export type RealChannel = Channel; + +export type IntegerChannel = Channel; + +export type QuaternionChannel = Channel; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}SingleChannelTrack`) +export abstract class SingleChannelTrack extends Track { + constructor () { + super(); + this._channel = new Channel(this.createCurve()); + } + + get channel () { + return this._channel; + } + + public getChannels () { + return [this._channel]; + } + + protected createCurve (): TCurve { + throw new Error(`Not impl`); + } + + public [createEvalSymbol] (_runtimeBinding: RuntimeBinding): TrackEval { + const { curve } = this._channel; + return { + evaluate: (time) => curve.evaluate(time), + }; + } + + @serializable + private _channel: Channel; +} + +export type RuntimeBinding = { + setValue(value: unknown): void; + + getValue?(): unknown; +}; + +export type Binder = (path: TrackPath, setter: IValueProxyFactory | undefined) => undefined | RuntimeBinding; + +export type TrsTrackPath = [HierarchyPath, 'position' | 'rotation' | 'scale' | 'eulerAngles']; + +export function isTargetingTRS (path: TargetPath[]): path is TrsTrackPath { + let prs: string | undefined; + if (path.length === 1 && typeof path[0] === 'string') { + prs = path[0]; + } else if (path.length > 1) { + for (let i = 0; i < path.length - 1; ++i) { + if (!(path[i] instanceof HierarchyPath)) { + return false; + } + } + prs = path[path.length - 1] as string; + } + switch (prs) { + case 'position': + case 'scale': + case 'rotation': + case 'eulerAngles': + return true; + default: + return false; + } +} diff --git a/cocos/core/animation/tracks/untyped-track.ts b/cocos/core/animation/tracks/untyped-track.ts new file mode 100644 index 00000000000..d313d95214c --- /dev/null +++ b/cocos/core/animation/tracks/untyped-track.ts @@ -0,0 +1,130 @@ +import { ccclass, serializable } from 'cc.decorator'; +import { RealCurve } from '../../curves'; +import { Color, Size, Vec2, Vec3, Vec4 } from '../../math'; +import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; +import { IValueProxyFactory } from '../value-proxy'; +import { ColorTrack, ColorTrackEval } from './color-track'; +import { Channel, RealChannel, RuntimeBinding, Track, TrackPath } from './track'; +import { Vec2TrackEval, Vec3TrackEval, Vec4TrackEval, VectorTrack } from './vector-track'; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}UntypedTrackChannel`) +class UntypedTrackChannel extends Channel { + @serializable + public property!: string; + + constructor () { + super(new RealCurve()); + } +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}UntypedTrack`) +export class UntypedTrack extends Track { + @serializable + private _channels: UntypedTrackChannel[] = []; + + public getChannels () { + return this._channels; + } + + public [createEvalSymbol] (runtimeBinding: RuntimeBinding) { + if (!runtimeBinding.getValue) { + throw new Error(`Can not decide type for untyped track: runtime binding does not provide a getter.`); + } + const trySearchCurve = (property: string) => this._channels.find((channel) => channel.property === property)?.curve; + const value = runtimeBinding.getValue(); + switch (true) { + case value instanceof Size: + default: + throw new Error(`Can not decide type for untyped track: got a unsupported value from runtime binding.`); + case value instanceof Vec2: + return new Vec2TrackEval( + trySearchCurve('x'), + trySearchCurve('y'), + ); + case value instanceof Vec3: + return new Vec3TrackEval( + trySearchCurve('x'), + trySearchCurve('y'), + trySearchCurve('z'), + ); + case value instanceof Vec4: + return new Vec4TrackEval( + trySearchCurve('x'), + trySearchCurve('y'), + trySearchCurve('z'), + trySearchCurve('w'), + ); + case value instanceof Color: + // TODO: what if x, y, z, w? + return new ColorTrackEval( + trySearchCurve('r'), + trySearchCurve('g'), + trySearchCurve('b'), + trySearchCurve('a'), + ); + } + } + + public addChannel (property: string): UntypedTrackChannel { + const channel = new UntypedTrackChannel(); + channel.property = property; + this._channels.push(channel); + return channel; + } + + public upgrade (refine: UntypedTrackRefine): Track | null { + const trySearchChannel = (property: string, outChannel: RealChannel) => { + const untypedChannel = this.getChannels().find((channel) => channel.property === property); + if (untypedChannel) { + outChannel.name = untypedChannel.name; + outChannel.curve.assignSorted( + Array.from(untypedChannel.curve.times()), + Array.from(untypedChannel.curve.values()), + ); + } + }; + const kind = refine(this.path, this.setter); + switch (kind) { + default: + break; + case 'vec2': case 'vec3': case 'vec4': { + const track = new VectorTrack(); + track.componentsCount = kind === 'vec2' ? 2 : kind === 'vec3' ? 3 : 4; + const [x, y, z, w] = track.getChannels(); + switch (kind) { + case 'vec4': + trySearchChannel('w', w); + // fall through + case 'vec3': + trySearchChannel('z', z); + // fall through + default: + case 'vec2': + trySearchChannel('x', x); + trySearchChannel('y', y); + } + return track; + } + case 'color': { + const track = new ColorTrack(); + const [r, g, b, a] = track.getChannels(); + trySearchChannel('r', r); + trySearchChannel('g', g); + trySearchChannel('b', b); + trySearchChannel('a', a); + // TODO: we need float-int conversion if xyzw + trySearchChannel('x', r); + trySearchChannel('y', g); + trySearchChannel('z', b); + trySearchChannel('w', a); + return track; + } + case 'size': + break; + } + + return null; + } +} + +export type UntypedTrackRefine = (path: TrackPath, setter?: IValueProxyFactory) => 'vec2' | 'vec3' | 'vec4' | 'color' | 'size'; diff --git a/cocos/core/animation/tracks/utils.ts b/cocos/core/animation/tracks/utils.ts new file mode 100644 index 00000000000..dee6f2050ce --- /dev/null +++ b/cocos/core/animation/tracks/utils.ts @@ -0,0 +1,10 @@ +import type { Curve } from './track'; + +export function maskIfEmpty (curve: T) { + return curve.empty ? undefined : curve; +} + +export interface Range { + min: number; + max: number; +} diff --git a/cocos/core/animation/tracks/vector-track.ts b/cocos/core/animation/tracks/vector-track.ts new file mode 100644 index 00000000000..41c57120348 --- /dev/null +++ b/cocos/core/animation/tracks/vector-track.ts @@ -0,0 +1,144 @@ +import { ccclass, serializable } from 'cc.decorator'; +import { RealCurve } from '../../curves'; +import { Vec2, Vec3, Vec4 } from '../../math'; +import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; +import { Channel, RealChannel, RuntimeBinding, Track } from './track'; +import { maskIfEmpty } from './utils'; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}VectorTrack`) +export class VectorTrack extends Track { + constructor () { + super(); + this._channels = new Array(4) as VectorTrack['_channels']; + for (let i = 0; i < this._channels.length; ++i) { + const channel = new Channel(new RealCurve()); + channel.name = 'X'; + this._channels[i] = channel; + } + } + + get componentsCount () { + return this._nComponents; + } + + set componentsCount (value) { + this._nComponents = value; + } + + public getChannels () { + return this._channels; + } + + public [createEvalSymbol] () { + switch (this._nComponents) { + default: + case 2: + return new Vec2TrackEval( + maskIfEmpty(this._channels[0].curve), + maskIfEmpty(this._channels[1].curve), + ); + case 3: + return new Vec3TrackEval( + maskIfEmpty(this._channels[0].curve), + maskIfEmpty(this._channels[1].curve), + maskIfEmpty(this._channels[2].curve), + ); + case 4: + return new Vec4TrackEval( + maskIfEmpty(this._channels[0].curve), + maskIfEmpty(this._channels[1].curve), + maskIfEmpty(this._channels[2].curve), + maskIfEmpty(this._channels[3].curve), + ); + } + } + + @serializable + private _channels: [RealChannel, RealChannel, RealChannel, RealChannel]; + + @serializable + private _nComponents: 2 | 3 | 4 = 4; +} + +export class Vec2TrackEval { + constructor (private _x: RealCurve | undefined, private _y: RealCurve | undefined) { + + } + + public evaluate (time: number, runtimeBinding: RuntimeBinding) { + if ((!this._x || !this._y) && runtimeBinding.getValue) { + Vec2.copy(this._result, runtimeBinding.getValue() as Vec2); + } + + if (this._x) { + this._result.x = this._x.evaluate(time); + } + if (this._y) { + this._result.y = this._y.evaluate(time); + } + + return this._result; + } + + private _result: Vec2 = new Vec2(); +} + +export class Vec3TrackEval { + constructor (private _x: RealCurve | undefined, private _y: RealCurve | undefined, private _z: RealCurve | undefined) { + + } + + public evaluate (time: number, runtimeBinding: RuntimeBinding) { + if ((!this._x || !this._y || !this._z) && runtimeBinding.getValue) { + Vec3.copy(this._result, runtimeBinding.getValue() as Vec3); + } + + if (this._x) { + this._result.x = this._x.evaluate(time); + } + if (this._y) { + this._result.y = this._y.evaluate(time); + } + if (this._z) { + this._result.z = this._z.evaluate(time); + } + + return this._result; + } + + private _result: Vec3 = new Vec3(); +} + +export class Vec4TrackEval { + constructor ( + private _x: RealCurve | undefined, + private _y: RealCurve | undefined, + private _z: RealCurve | undefined, + private _w: RealCurve | undefined, + ) { + + } + + public evaluate (time: number, runtimeBinding: RuntimeBinding) { + if ((!this._x || !this._y || !this._z || !this._w) && runtimeBinding.getValue) { + Vec4.copy(this._result, runtimeBinding.getValue() as Vec4); + } + + if (this._x) { + this._result.x = this._x.evaluate(time); + } + if (this._y) { + this._result.y = this._y.evaluate(time); + } + if (this._z) { + this._result.z = this._z.evaluate(time); + } + if (this._w) { + this._result.w = this._w.evaluate(time); + } + + return this._result; + } + + private _result: Vec4 = new Vec4(); +} From d7c55131dab6db6891a362d0f133751c121e6f53 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Thu, 1 Jul 2021 15:42:40 +0800 Subject: [PATCH 04/35] Update --- cocos/core/animation/legacy-clip-data.ts | 446 +++++++++++------------ 1 file changed, 212 insertions(+), 234 deletions(-) diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts index 2b74e76d792..16606884dbc 100644 --- a/cocos/core/animation/legacy-clip-data.ts +++ b/cocos/core/animation/legacy-clip-data.ts @@ -155,6 +155,218 @@ export class AnimationClipLegacyData { return this._runtimeCurves!; } + public toTracks () { + const newTracks: Track[] = []; + + const { + keys: legacyKeys, + curves: legacyCurves, + commonTargets: legacyCommonTargets, + } = this; + + const untypedTracks = legacyCommonTargets.map((legacyCommonTarget) => { + const track = new UntypedTrack(); + track.path = legacyCommonTarget.modifiers; + track.setter = legacyCommonTarget.valueAdapter; + newTracks.push(track); + return track; + }); + + for (const legacyCurve of legacyCurves) { + const legacyCurveData = legacyCurve.data; + const legacyValues = legacyCurveData.values; + if (legacyValues.length === 0) { + // Legacy clip did not record type info. + continue; + } + const legacyKeysIndex = legacyCurveData.keys; + // Rule: negative index means single frame. + const times = legacyKeysIndex < 0 ? [0.0] : legacyKeys[legacyCurveData.keys]; + const firstValue = legacyValues[0]; + // Rule: default to true. + const interpolate = legacyCurveData ?? true; + // Rule: _arrayLength only used for morph target, internally. + assertIsTrue(typeof legacyCurveData._arrayLength !== 'number' || typeof firstValue === 'number'); + const legacyEasingMethodConverter = new LegacyEasingMethodConverter(legacyCurveData, times.length); + + const installPathAndSetter = (track: Track) => { + track.path = legacyCurve.modifiers; + track.setter = legacyCurve.valueAdapter; + }; + + let legacyCommonTargetCurve: RealCurve | undefined; + if (typeof legacyCurve.commonTarget === 'number') { + // Rule: common targets should only target Vectors/`Size`/`Color`. + if (!legacyValues.every((value) => typeof value === 'number')) { + warn(`Incorrect curve.`); + continue; + } + // Rule: Each curve that has common target should be numeric curve and targets string property. + if (legacyCurve.valueAdapter || legacyCurve.modifiers.length !== 1 || typeof legacyCurve.modifiers[0] !== 'string') { + warn(`Incorrect curve.`); + continue; + } + const propertyName = legacyCurve.modifiers[0]; + const untypedTrack = untypedTracks[legacyCurve.commonTarget]; + const { curve } = untypedTrack.addChannel(propertyName); + legacyCommonTargetCurve = curve; + } + + const convertCurve = () => { + if (typeof firstValue === 'number') { + if (!legacyValues.every((value) => typeof value === 'number')) { + warn(`Misconfigured curve.`); + return; + } + let realCurve: RealCurve; + if (legacyCommonTargetCurve) { + realCurve = legacyCommonTargetCurve; + } else { + const track = new RealTrack(); + installPathAndSetter(track); + newTracks.push(track); + realCurve = track.channel.curve; + } + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + realCurve.assignSorted(times, (legacyValues as number[]).map((value) => new RealKeyframeValue({ value, interpMode: interpMethod }))); + legacyEasingMethodConverter.convert(realCurve); + return; + } else if (typeof firstValue === 'object') { + switch (true) { + default: + break; + case legacyValues.every((value) => value instanceof Vec2): + case legacyValues.every((value) => value instanceof Vec3): + case legacyValues.every((value) => value instanceof Vec4): { + type Vec4plus = Vec4[]; + type Vec3plus = (Vec3 | Vec4)[]; + type Vec2plus = (Vec2 | Vec3 | Vec4)[]; + const components = firstValue instanceof Vec2 ? 2 : firstValue instanceof Vec3 ? 3 : 4; + const track = new VectorTrack(); + installPathAndSetter(track); + track.componentsCount = components; + const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.getChannels(); + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); + switch (components) { + case 4: + w.assignSorted(times, (legacyValues as Vec4plus).map((value) => valueToFrame(value.w))); + legacyEasingMethodConverter.convert(w); + // falls through + case 3: + z.assignSorted(times, (legacyValues as Vec3plus).map((value) => valueToFrame(value.z))); + legacyEasingMethodConverter.convert(z); + // falls through + default: + x.assignSorted(times, (legacyValues as Vec2plus).map((value) => valueToFrame(value.x))); + legacyEasingMethodConverter.convert(x); + y.assignSorted(times, (legacyValues as Vec2plus).map((value) => valueToFrame(value.y))); + legacyEasingMethodConverter.convert(y); + break; + } + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof Quat): { + assertIsTrue(legacyEasingMethodConverter.nil); + const track = new QuaternionTrack(); + installPathAndSetter(track); + const interpMode = interpolate ? QuaternionInterpMode.SLERP : QuaternionInterpMode.CONSTANT; + track.channel.curve.assignSorted(times, (legacyValues as Quat[]).map((value) => ({ + value: Quat.clone(value), + interpMode, + }))); + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof Color): { + const track = new ColorTrack(); + installPathAndSetter(track); + const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.getChannels(); + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); + r.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.r))); + legacyEasingMethodConverter.convert(r); + g.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.g))); + legacyEasingMethodConverter.convert(g); + b.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.b))); + legacyEasingMethodConverter.convert(b); + a.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.a))); + legacyEasingMethodConverter.convert(a); + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof CubicSplineNumberValue): { + assertIsTrue(legacyEasingMethodConverter.nil); + const track = new RealTrack(); + installPathAndSetter(track); + const interpMethod = interpolate ? RealInterpMode.CUBIC : RealInterpMode.CONSTANT; + track.channel.curve.assignSorted(times, (legacyValues as CubicSplineNumberValue[]).map((value) => new RealKeyframeValue({ + value: value.dataPoint, + startTangent: value.inTangent, + endTangent: value.outTangent, + interpMode: interpMethod, + }))); + newTracks.push(track); + return; + } + case legacyValues.every((value) => value instanceof CubicSplineVec2Value): + case legacyValues.every((value) => value instanceof CubicSplineVec3Value): + case legacyValues.every((value) => value instanceof CubicSplineVec4Value): { + assertIsTrue(legacyEasingMethodConverter.nil); + type Vec4plus = CubicSplineVec4Value[]; + type Vec3plus = (CubicSplineVec3Value | CubicSplineVec4Value)[]; + type Vec2plus = (CubicSplineVec2Value | CubicSplineVec3Value | CubicSplineVec4Value)[]; + const components = firstValue instanceof CubicSplineVec2Value ? 2 : firstValue instanceof CubicSplineVec3Value ? 3 : 4; + const track = new VectorTrack(); + installPathAndSetter(track); + track.componentsCount = components; + const [x, y, z, w] = track.getChannels(); + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const valueToFrame = (value: number, startTangent: number, endTangent: number): RealKeyframeValue => new RealKeyframeValue({ + value, + startTangent, + endTangent, + interpMode: interpMethod, + }); + switch (components) { + case 4: + w.curve.assignSorted(times, (legacyValues as Vec4plus).map( + (value) => valueToFrame(value.dataPoint.w, value.inTangent.w, value.outTangent.w), + )); + // falls through + case 3: + z.curve.assignSorted(times, (legacyValues as Vec3plus).map( + (value) => valueToFrame(value.dataPoint.z, value.inTangent.z, value.outTangent.z), + )); + // falls through + default: + x.curve.assignSorted(times, (legacyValues as Vec2plus).map( + (value) => valueToFrame(value.dataPoint.y, value.inTangent.y, value.outTangent.y), + )); + y.curve.assignSorted(times, (legacyValues as Vec2plus).map( + (value) => valueToFrame(value.dataPoint.x, value.inTangent.x, value.outTangent.x), + )); + break; + } + newTracks.push(track); + return; + } + } // End switch + } + + const objectTrack = new ObjectTrack(); + installPathAndSetter(objectTrack); + objectTrack.channel.curve.assignSorted(times, legacyValues); + newTracks.push(objectTrack); + }; + + convertCurve(); + } + + return newTracks; + } + @serializable private _keys: number[][] = []; @@ -189,28 +401,6 @@ export class AnimationClipLegacyData { commonTarget: targetCurve.commonTarget, })); } - - // private _decodeCVTAs () { - // const binaryBuffer: ArrayBuffer = ArrayBuffer.isView(this._nativeAsset) ? this._nativeAsset.buffer : this._nativeAsset; - // if (!binaryBuffer) { - // return; - // } - - // const maybeCompressedKeys = this._keys; - // for (let iKey = 0; iKey < maybeCompressedKeys.length; ++iKey) { - // const keys = maybeCompressedKeys[iKey]; - // if (keys instanceof CompactValueTypeArray) { - // maybeCompressedKeys[iKey] = keys.decompress(binaryBuffer); - // } - // } - - // for (let iCurve = 0; iCurve < this._curves.length; ++iCurve) { - // const curve = this._curves[iCurve]; - // if (curve.data.values instanceof CompactValueTypeArray) { - // curve.data.values = curve.data.values.decompress(binaryBuffer); - // } - // } - // } } // #region Legacy data structures prior to 1.2 @@ -381,215 +571,3 @@ function powerToBernstein ([p0, p1, p2, p3]: [number, number, number, number], b bernstein[2] = p2 / 3.0 + p3; bernstein[3] = p3; } - -export function convertAnimationClipLegacyData (animationClipLegacyData: AnimationClipLegacyData) { - const newTracks: Track[] = []; - - const { - keys: legacyKeys, - curves: legacyCurves, - commonTargets: legacyCommonTargets, - } = animationClipLegacyData; - - const untypedTracks = legacyCommonTargets.map((legacyCommonTarget) => { - const track = new UntypedTrack(); - track.path = legacyCommonTarget.modifiers; - track.setter = legacyCommonTarget.valueAdapter; - newTracks.push(track); - return track; - }); - - for (const legacyCurve of legacyCurves) { - const legacyCurveData = legacyCurve.data; - const legacyValues = legacyCurveData.values; - if (legacyValues.length === 0) { - // Legacy clip did not record type info. - continue; - } - const legacyKeysIndex = legacyCurveData.keys; - // Rule: negative index means single frame. - const times = legacyKeysIndex < 0 ? [0.0] : legacyKeys[legacyCurveData.keys]; - const firstValue = legacyValues[0]; - // Rule: default to true. - const interpolate = legacyCurveData ?? true; - // Rule: _arrayLength only used for morph target, internally. - assertIsTrue(typeof legacyCurveData._arrayLength !== 'number' || typeof firstValue === 'number'); - const legacyEasingMethodConverter = new LegacyEasingMethodConverter(legacyCurveData, times.length); - - const installPathAndSetter = (track: Track) => { - track.path = legacyCurve.modifiers; - track.setter = legacyCurve.valueAdapter; - }; - - let legacyCommonTargetCurve: RealCurve | undefined; - if (typeof legacyCurve.commonTarget === 'number') { - // Rule: common targets should only target Vectors/`Size`/`Color`. - if (!legacyValues.every((value) => typeof value === 'number')) { - warn(`Incorrect curve.`); - continue; - } - // Rule: Each curve that has common target should be numeric curve and targets string property. - if (legacyCurve.valueAdapter || legacyCurve.modifiers.length !== 1 || typeof legacyCurve.modifiers[0] !== 'string') { - warn(`Incorrect curve.`); - continue; - } - const propertyName = legacyCurve.modifiers[0]; - const untypedTrack = untypedTracks[legacyCurve.commonTarget]; - const { curve } = untypedTrack.addChannel(propertyName); - legacyCommonTargetCurve = curve; - } - - const convertCurve = () => { - if (typeof firstValue === 'number') { - if (!legacyValues.every((value) => typeof value === 'number')) { - warn(`Misconfigured curve.`); - return; - } - let realCurve: RealCurve; - if (legacyCommonTargetCurve) { - realCurve = legacyCommonTargetCurve; - } else { - const track = new RealTrack(); - installPathAndSetter(track); - newTracks.push(track); - realCurve = track.channel.curve; - } - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - realCurve.assignSorted(times, (legacyValues as number[]).map((value) => new RealKeyframeValue({ value, interpMode: interpMethod }))); - legacyEasingMethodConverter.convert(realCurve); - return; - } else if (typeof firstValue === 'object') { - switch (true) { - default: - break; - case legacyValues.every((value) => value instanceof Vec2): - case legacyValues.every((value) => value instanceof Vec3): - case legacyValues.every((value) => value instanceof Vec4): { - type Vec4plus = Vec4[]; - type Vec3plus = (Vec3 | Vec4)[]; - type Vec2plus = (Vec2 | Vec3 | Vec4)[]; - const components = firstValue instanceof Vec2 ? 2 : firstValue instanceof Vec3 ? 3 : 4; - const track = new VectorTrack(); - installPathAndSetter(track); - track.componentsCount = components; - const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.getChannels(); - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); - switch (components) { - case 4: - w.assignSorted(times, (legacyValues as Vec4plus).map((value) => valueToFrame(value.w))); - legacyEasingMethodConverter.convert(w); - // falls through - case 3: - z.assignSorted(times, (legacyValues as Vec3plus).map((value) => valueToFrame(value.z))); - legacyEasingMethodConverter.convert(z); - // falls through - default: - x.assignSorted(times, (legacyValues as Vec2plus).map((value) => valueToFrame(value.x))); - legacyEasingMethodConverter.convert(x); - y.assignSorted(times, (legacyValues as Vec2plus).map((value) => valueToFrame(value.y))); - legacyEasingMethodConverter.convert(y); - break; - } - newTracks.push(track); - return; - } - case legacyValues.every((value) => value instanceof Quat): { - assertIsTrue(legacyEasingMethodConverter.nil); - const track = new QuaternionTrack(); - installPathAndSetter(track); - const interpMode = interpolate ? QuaternionInterpMode.SLERP : QuaternionInterpMode.CONSTANT; - track.channel.curve.assignSorted(times, (legacyValues as Quat[]).map((value) => ({ - value: Quat.clone(value), - interpMode, - }))); - newTracks.push(track); - return; - } - case legacyValues.every((value) => value instanceof Color): { - const track = new ColorTrack(); - installPathAndSetter(track); - const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.getChannels(); - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); - r.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.r))); - legacyEasingMethodConverter.convert(r); - g.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.g))); - legacyEasingMethodConverter.convert(g); - b.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.b))); - legacyEasingMethodConverter.convert(b); - a.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.a))); - legacyEasingMethodConverter.convert(a); - newTracks.push(track); - return; - } - case legacyValues.every((value) => value instanceof CubicSplineNumberValue): { - assertIsTrue(legacyEasingMethodConverter.nil); - const track = new RealTrack(); - installPathAndSetter(track); - const interpMethod = interpolate ? RealInterpMode.CUBIC : RealInterpMode.CONSTANT; - track.channel.curve.assignSorted(times, (legacyValues as CubicSplineNumberValue[]).map((value) => new RealKeyframeValue({ - value: value.dataPoint, - startTangent: value.inTangent, - endTangent: value.outTangent, - interpMode: interpMethod, - }))); - newTracks.push(track); - return; - } - case legacyValues.every((value) => value instanceof CubicSplineVec2Value): - case legacyValues.every((value) => value instanceof CubicSplineVec3Value): - case legacyValues.every((value) => value instanceof CubicSplineVec4Value): { - assertIsTrue(legacyEasingMethodConverter.nil); - type Vec4plus = CubicSplineVec4Value[]; - type Vec3plus = (CubicSplineVec3Value | CubicSplineVec4Value)[]; - type Vec2plus = (CubicSplineVec2Value | CubicSplineVec3Value | CubicSplineVec4Value)[]; - const components = firstValue instanceof CubicSplineVec2Value ? 2 : firstValue instanceof CubicSplineVec3Value ? 3 : 4; - const track = new VectorTrack(); - installPathAndSetter(track); - track.componentsCount = components; - const [x, y, z, w] = track.getChannels(); - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - const valueToFrame = (value: number, startTangent: number, endTangent: number): RealKeyframeValue => new RealKeyframeValue({ - value, - startTangent, - endTangent, - interpMode: interpMethod, - }); - switch (components) { - case 4: - w.curve.assignSorted(times, (legacyValues as Vec4plus).map( - (value) => valueToFrame(value.dataPoint.w, value.inTangent.w, value.outTangent.w), - )); - // falls through - case 3: - z.curve.assignSorted(times, (legacyValues as Vec3plus).map( - (value) => valueToFrame(value.dataPoint.z, value.inTangent.z, value.outTangent.z), - )); - // falls through - default: - x.curve.assignSorted(times, (legacyValues as Vec2plus).map( - (value) => valueToFrame(value.dataPoint.y, value.inTangent.y, value.outTangent.y), - )); - y.curve.assignSorted(times, (legacyValues as Vec2plus).map( - (value) => valueToFrame(value.dataPoint.x, value.inTangent.x, value.outTangent.x), - )); - break; - } - newTracks.push(track); - return; - } - } // End switch - } - - const objectTrack = new ObjectTrack(); - installPathAndSetter(objectTrack); - objectTrack.channel.curve.assignSorted(times, legacyValues); - newTracks.push(objectTrack); - }; - - convertCurve(); - } - - return newTracks; -} From 8d66265ad520c170dfa17d498d672ddd6d2f0154 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Thu, 1 Jul 2021 19:07:57 +0800 Subject: [PATCH 05/35] Manually implements KeyframeCurve.iterator --- cocos/core/curves/keyframe-curve.ts | 32 ++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/cocos/core/curves/keyframe-curve.ts b/cocos/core/curves/keyframe-curve.ts index f8128ee9858..af4e3a928c1 100644 --- a/cocos/core/curves/keyframe-curve.ts +++ b/cocos/core/curves/keyframe-curve.ts @@ -10,7 +10,7 @@ type KeyFrame = [number, TKeyframeValue]; * Curve. */ @ccclass('cc.KeyframeCurve') -export class KeyframeCurve implements CurveBase { +export class KeyframeCurve implements CurveBase, Iterable> { /** * Gets the count of keyframes. */ @@ -42,10 +42,32 @@ export class KeyframeCurve implements CurveBase { /** * Returns an iterator to keyframe pairs. */ - public* keyframes (): Iterable> { - for (let i = 0; i < this._times.length; ++i) { - yield [this._times[i], this._values[i]]; - } + [Symbol.iterator] () { + let index = 0; + return { + next: (): IteratorResult> => { + if (index >= this._times.length) { + return { + done: true, + value: undefined, + }; + } else { + const value: KeyFrame = [this._times[index], this._values[index]]; + ++index; + return { + done: false, + value, + }; + } + }, + }; + } + + /** + * Returns an iterator to keyframe pairs. + */ + public keyframes (): Iterable> { + return this; } public times (): Iterable { From bd1f2b6f191714777c806e5ed0a5d36c34e08dd5 Mon Sep 17 00:00:00 2001 From: Leslie Leigh Date: Thu, 1 Jul 2021 21:44:38 +0800 Subject: [PATCH 06/35] TargetPath --- .../skeletal-animation-blending.ts | 5 +- cocos/core/animation/animation-clip.ts | 124 ++------ cocos/core/animation/animation.ts | 2 +- cocos/core/animation/bound-target.ts | 152 --------- .../animation/compression/compressed-data.ts | 30 +- cocos/core/animation/legacy-clip-data.ts | 39 ++- cocos/core/animation/target-path.ts | 26 -- cocos/core/animation/tracks/color-track.ts | 2 +- cocos/core/animation/tracks/quat-track.ts | 2 +- cocos/core/animation/tracks/track.ts | 297 ++++++++++++++++-- cocos/core/animation/tracks/untyped-track.ts | 12 +- cocos/core/animation/tracks/vector-track.ts | 2 +- .../animaion-clip-migration-3.x.test.ts | 8 +- tests/animation/animation-clip-3.x.test.ts | 11 +- tests/animation/animation-clip.test.ts | 8 +- 15 files changed, 364 insertions(+), 356 deletions(-) delete mode 100644 cocos/core/animation/bound-target.ts diff --git a/cocos/3d/skeletal-animation/skeletal-animation-blending.ts b/cocos/3d/skeletal-animation/skeletal-animation-blending.ts index 99a8b1d994f..1c697e985ea 100644 --- a/cocos/3d/skeletal-animation/skeletal-animation-blending.ts +++ b/cocos/3d/skeletal-animation/skeletal-animation-blending.ts @@ -30,8 +30,7 @@ import { Vec3, Quat } from '../../core/math'; import { Node } from '../../core/scene-graph'; -import { AnimationState } from '../../core/animation/animation-state'; -import { IBoundTarget } from '../../core/animation/bound-target'; +import { RuntimeBinding } from '../../core/animation/tracks/track'; export class BlendStateBuffer { private _nodeBlendStates: Map = new Map(); @@ -89,7 +88,7 @@ export interface BlendStateWriterHost { readonly weight: number; } -class BlendStateWriterInternal

implements IBoundTarget { +class BlendStateWriterInternal

implements RuntimeBinding { constructor ( private _node: Node, private _property: P, diff --git a/cocos/core/animation/animation-clip.ts b/cocos/core/animation/animation-clip.ts index 626490181b8..2e1e837ecec 100644 --- a/cocos/core/animation/animation-clip.ts +++ b/cocos/core/animation/animation-clip.ts @@ -36,9 +36,7 @@ import { DataPoolManager } from '../../3d/skeletal-animation/data-pool-manager'; import { binarySearchEpsilon } from '../algorithm/binary-search'; import { murmurhash2_32_gc } from '../utils/murmurhash2_gc'; import { SkelAnimDataHub } from '../../3d/skeletal-animation/skeletal-animation-data-hub'; -import { ComponentPath, evaluatePath, isPropertyPath } from './target-path'; import { WrapMode as AnimationWrapMode, WrapMode, WrapModeMask } from './types'; -import { IValueProxyFactory } from './value-proxy'; import { legacyCC } from '../global-exports'; import { Mat4, Quat, Vec3 } from '../math'; import { Node } from '../scene-graph/node'; @@ -46,7 +44,7 @@ import { assertIsTrue } from '../data/utils/asserts'; import type { PoseOutput } from './pose-output'; import * as legacy from './legacy-clip-data'; import { BAKE_SKELETON_CURVE_SYMBOL } from './internal-symbols'; -import { Binder, isTargetingTRS, RuntimeBinding, Track, TrackEval, TrackPath, TrsTrackPath } from './tracks/track'; +import { Binder, RuntimeBinding, Track, TrackBinding, trackBindingTag, TrackEval, TrackPath, TrsTrackPath } from './tracks/track'; import { createEvalSymbol } from './define'; import { VectorTrack } from './tracks/vector-track'; import { UntypedTrack, UntypedTrackRefine } from './tracks/untyped-track'; @@ -108,8 +106,8 @@ export class AnimationClip extends Asset { clip.duration = spriteFrames.length / clip.sample; const step = 1 / clip.sample; const track = new ObjectTrack(); - track.path = [new ComponentPath('cc.Sprite'), 'spriteFrame']; - const curve = track.getChannels()[0].curve; + track.path = new TrackPath().component('cc.Sprite').property('spriteFrame'); + const curve = track.channels()[0].curve; curve.assignSorted(spriteFrames.map((spriteFrame, index) => [step * index, spriteFrame])); return clip; } @@ -233,13 +231,13 @@ export class AnimationClip extends Asset { } /** - * Gets the time range this animation spans. + * Counts the time range this animation spans. * @returns The time range. */ - public getRange () { + public range () { const range: Range = { min: Infinity, max: -Infinity }; for (const track of this._tracks) { - const trackRange = track.getRange(); + const trackRange = track.range(); range.min = Math.min(range.min, trackRange.min); range.max = Math.max(range.max, trackRange.max); } @@ -306,11 +304,9 @@ export class AnimationClip extends Asset { target, } = context; - const binder: Binder = (trackPath: TrackPath, setter: IValueProxyFactory | undefined) => { - const trackTarget = createGeneralBinding( + const binder: Binder = (binding: TrackBinding) => { + const trackTarget = binding.createRuntimeBinding( target, - trackPath, - setter ?? undefined, this.enableTrsBlending ? context.pose : undefined, false, ); @@ -403,18 +399,18 @@ export class AnimationClip extends Asset { } } - const binder: Binder = (trackPath: TrackPath, setter: IValueProxyFactory | undefined) => { - if (setter || !isTargetingTRS(trackPath)) { + const binder: Binder = (binding: TrackBinding) => { + const trsPath = binding.parseTrsPath(); + if (!trsPath) { return undefined; } - const { path } = trackPath[0]; - const jointFrame = skeletonFrames[path]; + const jointFrame = skeletonFrames[trsPath.node]; if (!jointFrame) { return undefined; } - return createBoneTransformBinding(jointFrame, trackPath[1]); + return createBoneTransformBinding(jointFrame, trsPath.property); }; const evaluator = this._createEvalWithBinder(undefined, binder, undefined); @@ -634,7 +630,7 @@ export class AnimationClip extends Asset { if (rootMotionTrackExcludes.includes(track)) { continue; } - const trackTarget = binder(track.path, track.setter); + const trackTarget = binder(track[trackBindingTag]); if (!trackTarget) { continue; } @@ -685,16 +681,17 @@ export class AnimationClip extends Asset { const boneTransform = new BoneTransform(); const rootMotionsTrackEvaluations: TrackEvalStatus[] = []; for (const track of this._tracks) { - const { path: trackPath } = track; - if (!isTargetingTRS(trackPath)) { + const { [trackBindingTag]: trackBinding } = track; + const trsPath = trackBinding.parseTrsPath(); + if (!trsPath) { continue; } - const bonePath = trackPath[0].path; + const bonePath = trsPath.node; if (bonePath !== rootBonePath) { continue; } rootMotionTrackExcludes.push(track); - const property = trackPath[1]; + const property = trsPath.property; const trackTarget = createBoneTransformBinding(boneTransform, property); if (!trackTarget) { continue; @@ -717,11 +714,12 @@ export class AnimationClip extends Asset { private _searchForRootBonePath () { const paths = this._tracks.map((track) => { - if (isTargetingTRS(track.path)) { - const { path } = track.path[0]; + const trsPath = track[trackBindingTag].parseTrsPath(); + if (trsPath) { + const nodePath = trsPath.node; return { - path, - rank: path.split('/').length, + path: nodePath, + rank: nodePath.split('/').length, }; } else { return { @@ -775,7 +773,7 @@ export class AnimationClip extends Asset { } private _fromLegacy (legacyData: legacy.AnimationClipLegacyData) { - const newTracks = legacy.convertAnimationClipLegacyData(legacyData); + const newTracks = legacyData.toTracks(); for (const track of newTracks) { this.addTrack(track); } @@ -785,9 +783,9 @@ export class AnimationClip extends Asset { const joints = new Set(); for (const track of this._tracks) { - if (!track.setter && isTargetingTRS(track.path)) { - const { path } = track.path[0]; - joints.add(path); + const trsPath = track[trackBindingTag].parseTrsPath(); + if (trsPath) { + joints.add(trsPath.node); } } @@ -1051,72 +1049,6 @@ function createBoneTransformBinding (boneTransform: BoneTransform, property: Trs } } -/** - * Bind runtime target. Especially optimized for skeletal case. - */ -function createGeneralBinding ( - rootTarget: unknown, - path: TrackPath, - setter: IValueProxyFactory | undefined, - poseOutput: PoseOutput | undefined, - isConstant: boolean, -): RuntimeBinding | null { - if (!isTargetingTRS(path) || !poseOutput) { - return createRuntimeBinding(rootTarget, path, setter); - } else { - const targetNode = evaluatePath(rootTarget, ...path.slice(0, path.length - 1)); - if (targetNode !== null && targetNode instanceof Node) { - const propertyName = path[path.length - 1] as 'position' | 'rotation' | 'scale' | 'eulerAngles'; - const blendStateWriter = poseOutput.createPoseWriter(targetNode, propertyName, isConstant); - return blendStateWriter; - } - } - return null; -} - -function createRuntimeBinding (target: unknown, trackPath: TrackPath, setter?: IValueProxyFactory): null | RuntimeBinding { - const lastPath = trackPath[trackPath.length - 1]; - if (trackPath.length !== 0 && isPropertyPath(lastPath) && !setter) { - const resultTarget = evaluatePath(target, ...trackPath.slice(0, trackPath.length - 1)); - if (resultTarget === null) { - return null; - } - return { - setValue: (value) => { - resultTarget[lastPath] = value; - }, - // eslint-disable-next-line arrow-body-style - getValue: () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return resultTarget[lastPath]; - }, - }; - } else if (!setter) { - error( - `You provided a ill-formed track path.` - + `The last component of track path should be property key, or the setter should not be empty.`, - ); - return null; - } else { - const resultTarget = evaluatePath(target, ...trackPath); - if (resultTarget === null) { - return null; - } - const proxy = setter.forTarget(resultTarget); - const binding: RuntimeBinding = { - setValue: (value) => { - proxy.set(value); - }, - }; - const proxyGet = proxy.get; - if (proxyGet) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - binding.getValue = () => proxyGet.call(proxy); - } - return binding; - } -} - // #region Events interface IAnimationEvent { diff --git a/cocos/core/animation/animation.ts b/cocos/core/animation/animation.ts index 71dc3a3d0e4..7ef6e342f9d 100644 --- a/cocos/core/animation/animation.ts +++ b/cocos/core/animation/animation.ts @@ -33,7 +33,7 @@ export * from './value-proxy'; export { UniformProxyFactory } from './value-proxy-factories/uniform'; export { MorphWeightValueProxy, MorphWeightsValueProxy, MorphWeightsAllValueProxy } from './value-proxy-factories/morph-weights'; export * from './cubic-spline-value'; -export { Track } from './tracks/track'; +export { Track, TrackPath } from './tracks/track'; export { RealTrack } from './tracks/real-track'; export { IntegerTrack } from './tracks/integer-track'; export { VectorTrack } from './tracks/vector-track'; diff --git a/cocos/core/animation/bound-target.ts b/cocos/core/animation/bound-target.ts deleted file mode 100644 index d23d8f0b6df..00000000000 --- a/cocos/core/animation/bound-target.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - Copyright (c) 2020 Xiamen Yaji Software Co., Ltd. - - https://www.cocos.com/ - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated engine source code (the "Software"), a limited, - worldwide, royalty-free, non-assignable, revocable and non-exclusive license - to use Cocos Creator solely to develop games on your target platforms. You shall - not use Cocos Creator software for developing other software or tools that's - used for developing games. You are not granted to publish, distribute, - sublicense, and/or sell copies of Cocos Creator. - - The software or tools in this License Agreement are licensed, not sold. - Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - */ - -/** - * @packageDocumentation - * @hidden - */ - -import { Color, Size, Vec2, Vec3, Vec4 } from '../math'; -import { IValueProxy, IValueProxyFactory } from './value-proxy'; -import { isPropertyPath, PropertyPath, TargetPath, evaluatePath } from './target-path'; -import { error } from '../platform/debug'; - -export interface IBoundTarget { - setValue (value: any): void; - getValue (): any; -} - -export interface IBufferedTarget extends IBoundTarget { - peek(): any; - pull(): void; - push(): void; -} - -export function createBoundTarget (target: any, modifiers: TargetPath[], valueAdapter?: IValueProxyFactory): null | IBoundTarget { - const lastPath = modifiers[modifiers.length - 1]; - if (modifiers.length !== 0 && isPropertyPath(lastPath) && !valueAdapter) { - const resultTarget = evaluatePath(target, ...modifiers.slice(0, modifiers.length - 1)); - if (resultTarget === null) { - return null; - } - return new PropertyAccessTarget(resultTarget, lastPath); - } else if (!valueAdapter) { - error(`Empty animation curve.`); - return null; - } else { - const resultTarget = evaluatePath(target, ...modifiers); - if (resultTarget === null) { - return null; - } - const proxy = valueAdapter.forTarget(resultTarget); - return new ProxyTarget(proxy); - } -} - -export function createBufferedTarget (boundTarget: IBoundTarget): null | IBufferedTarget { - if (boundTarget === null) { - return null; - } - const value = boundTarget.getValue(); - const copyable = getBuiltinCopy(value); - if (!copyable) { - error(`Value is not copyable!`); - return null; - } - const buffer = copyable.createBuffer(); - const copy = copyable.copy; - return Object.assign(boundTarget, { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - peek: () => buffer, - pull: () => { - const value = boundTarget.getValue(); - copy(buffer, value); - }, - push: () => { - boundTarget.setValue(buffer); - }, - }); -} - -interface ICopyable { - createBuffer: () => any; - copy: (out: any, source: any) => any; -} - -function SizeCopy (out: Size, source: Size) { - return out.set(source); -} - -const getBuiltinCopy = (() => { - const map = new Map(); - map.set(Vec2, { createBuffer: () => new Vec2(), copy: Vec2.copy }); - map.set(Vec3, { createBuffer: () => new Vec3(), copy: Vec3.copy }); - map.set(Vec4, { createBuffer: () => new Vec4(), copy: Vec4.copy }); - map.set(Color, { createBuffer: () => new Color(), copy: Color.copy }); - map.set(Size, { createBuffer: () => new Size(), copy: SizeCopy }); - return (value: any) => map.get(value?.constructor); -})(); - -class PropertyAccessTarget implements IBoundTarget { - constructor (object: unknown, propertyName: string | number) { - this._object = object; - this._propertyName = propertyName; - } - - setValue (value: unknown): void { - (this._object as any)[this._propertyName] = value; - } - - getValue () { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return (this._object as any)[this._propertyName]; - } - - private _object: unknown; - private _propertyName: string | number; -} - -class ProxyTarget implements IBoundTarget { - constructor (proxy: IValueProxy) { - this._proxy = proxy; - } - - setValue (value: any): void { - this._proxy.set(value); - } - - getValue () { - const { _proxy: proxy } = this; - if (!proxy.get) { - error(`Target doesn't provide a get method.`); - return null; - } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return proxy.get(); - } - } - - private _proxy: IValueProxy; -} diff --git a/cocos/core/animation/compression/compressed-data.ts b/cocos/core/animation/compression/compressed-data.ts index 63c55c915d3..b17a099167f 100644 --- a/cocos/core/animation/compression/compressed-data.ts +++ b/cocos/core/animation/compression/compressed-data.ts @@ -5,9 +5,8 @@ import { Quat, Vec2, Vec3, Vec4 } from '../../math'; import { CLASS_NAME_PREFIX_ANIM } from '../define'; import { QuaternionTrack } from '../tracks/quat-track'; import { RealTrack } from '../tracks/real-track'; -import { Binder, isTargetingTRS, RuntimeBinding, TrackPath } from '../tracks/track'; +import { Binder, RuntimeBinding, TrackBinding, trackBindingTag, TrackPath } from '../tracks/track'; import { VectorTrack } from '../tracks/vector-track'; -import { IValueProxyFactory } from '../value-proxy'; @ccclass(`${CLASS_NAME_PREFIX_ANIM}CompressedData`) export class CompressedData { @@ -19,8 +18,7 @@ export class CompressedData { } this._tracks.push({ type: CompressedDataTrackType.FLOAT, - path: track.path, - setter: track.setter, + binding: track[trackBindingTag], components: [this._addRealCurve(curve)], }); return true; @@ -28,7 +26,7 @@ export class CompressedData { public compressVectorTrack (vectorTrack: VectorTrack) { const nComponents = vectorTrack.componentsCount; - const channels = vectorTrack.getChannels(); + const channels = vectorTrack.channels(); const mayBeCompressed = channels.every(({ curve }) => KeySharedRealCurves.allowedForCurve(curve)); if (!mayBeCompressed) { return false; @@ -45,8 +43,7 @@ export class CompressedData { : nComponents === 3 ? CompressedDataTrackType.VEC3 : CompressedDataTrackType.VEC4, - path: vectorTrack.path, - setter: vectorTrack.setter, + binding: vectorTrack[trackBindingTag], components, }); return true; @@ -59,8 +56,7 @@ export class CompressedData { return false; } this._quatTracks.push({ - path: track.path, - setter: track.setter, + binding: track[trackBindingTag], pointer: this._addQuaternionCurve(curve), }); return true; @@ -89,7 +85,7 @@ export class CompressedData { } for (const track of this._tracks) { - const trackTarget = binder(track.path, track.setter); + const trackTarget = binder(track.binding); if (!trackTarget) { continue; } @@ -124,7 +120,7 @@ export class CompressedData { } for (const track of this._quatTracks) { - const trackTarget = binder(track.path, track.setter); + const trackTarget = binder(track.binding); if (!trackTarget) { continue; } @@ -141,9 +137,9 @@ export class CompressedData { const joints: string[] = []; for (const track of this._tracks) { - if (!track.setter && isTargetingTRS(track.path)) { - const { path } = track.path[0]; - joints.push(path); + const trsPath = track.binding.parseTrsPath(); + if (trsPath) { + joints.push(trsPath.node); } } @@ -267,15 +263,13 @@ export class CompressedDataEvaluator { } interface CompressedTrack { - path: TrackPath; - setter: IValueProxyFactory | undefined; + binding: TrackBinding; type: CompressedDataTrackType; components: CompressedCurvePointer[]; } interface CompressedQuatTrack { - path: TrackPath; - setter: IValueProxyFactory | undefined; + binding: TrackBinding; pointer: CompressedQuatCurvePointer; } diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts index 16606884dbc..14b06be465f 100644 --- a/cocos/core/animation/legacy-clip-data.ts +++ b/cocos/core/animation/legacy-clip-data.ts @@ -1,4 +1,4 @@ -import { TargetPath } from './target-path'; +import { ComponentPath, HierarchyPath, isPropertyPath, TargetPath } from './target-path'; import { IValueProxyFactory } from './value-proxy'; import * as easing from './easing'; import { BezierControlPoints } from './bezier'; @@ -7,7 +7,7 @@ import { serializable } from '../data/decorators'; import { AnimCurve, RatioSampler } from './animation-curve'; import { QuaternionInterpMode, RealCurve, RealInterpMode, RealKeyframeValue, TangentWeightMode } from '../curves'; import { assertIsTrue } from '../data/utils/asserts'; -import { Track } from './tracks/track'; +import { Track, TrackPath } from './tracks/track'; import { UntypedTrack } from './tracks/untyped-track'; import { warn } from '../platform'; import { RealTrack } from './tracks/real-track'; @@ -164,10 +164,28 @@ export class AnimationClipLegacyData { commonTargets: legacyCommonTargets, } = this; + const convertTrackPath = (track: Track, modifiers: TargetPath[], valueAdapter: IValueProxyFactory | undefined) => { + const trackPath = new TrackPath(); + for (const modifier of modifiers) { + if (typeof modifier === 'string') { + trackPath.property(modifier); + } else if (typeof modifier === 'number') { + trackPath.element(modifier); + } else if (modifier instanceof HierarchyPath) { + trackPath.hierarchy(modifier.path); + } else if (modifier instanceof ComponentPath) { + trackPath.component(modifier.component); + } else { + trackPath.customized(modifier); + } + } + track.path = trackPath; + track.proxy = valueAdapter; + }; + const untypedTracks = legacyCommonTargets.map((legacyCommonTarget) => { const track = new UntypedTrack(); - track.path = legacyCommonTarget.modifiers; - track.setter = legacyCommonTarget.valueAdapter; + convertTrackPath(track, legacyCommonTarget.modifiers, legacyCommonTarget.valueAdapter); newTracks.push(track); return track; }); @@ -190,8 +208,7 @@ export class AnimationClipLegacyData { const legacyEasingMethodConverter = new LegacyEasingMethodConverter(legacyCurveData, times.length); const installPathAndSetter = (track: Track) => { - track.path = legacyCurve.modifiers; - track.setter = legacyCurve.valueAdapter; + convertTrackPath(track, legacyCurve.modifiers, legacyCurve.valueAdapter); }; let legacyCommonTargetCurve: RealCurve | undefined; @@ -228,7 +245,9 @@ export class AnimationClipLegacyData { realCurve = track.channel.curve; } const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - realCurve.assignSorted(times, (legacyValues as number[]).map((value) => new RealKeyframeValue({ value, interpMode: interpMethod }))); + realCurve.assignSorted(times, (legacyValues as number[]).map( + (value) => new RealKeyframeValue({ value, interpMode: interpMethod }), + )); legacyEasingMethodConverter.convert(realCurve); return; } else if (typeof firstValue === 'object') { @@ -245,7 +264,7 @@ export class AnimationClipLegacyData { const track = new VectorTrack(); installPathAndSetter(track); track.componentsCount = components; - const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.getChannels(); + const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.channels(); const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); switch (components) { @@ -282,7 +301,7 @@ export class AnimationClipLegacyData { case legacyValues.every((value) => value instanceof Color): { const track = new ColorTrack(); installPathAndSetter(track); - const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.getChannels(); + const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.channels(); const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); r.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.r))); @@ -321,7 +340,7 @@ export class AnimationClipLegacyData { const track = new VectorTrack(); installPathAndSetter(track); track.componentsCount = components; - const [x, y, z, w] = track.getChannels(); + const [x, y, z, w] = track.channels(); const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; const valueToFrame = (value: number, startTangent: number, endTangent: number): RealKeyframeValue => new RealKeyframeValue({ value, diff --git a/cocos/core/animation/target-path.ts b/cocos/core/animation/target-path.ts index 2b4ddc20ff8..3e0cadd3588 100644 --- a/cocos/core/animation/target-path.ts +++ b/cocos/core/animation/target-path.ts @@ -97,29 +97,3 @@ export class ComponentPath implements ICustomTargetPath { return result; } } - -/** - * Evaluate a sequence of paths, in order, from specified root. - * @param root The root object. - * @param path The path sequence. - */ -export function evaluatePath (root: any, ...paths: TargetPath[]) { - let result = root; - for (let iPath = 0; iPath < paths.length; ++iPath) { - const path = paths[iPath]; - if (isPropertyPath(path)) { - if (!(path in result)) { - warn(`Target object has no property "${path}"`); - return null; - } else { - result = result[path]; - } - } else { - result = path.get(result); - } - if (result === null) { - break; - } - } - return result; -} diff --git a/cocos/core/animation/tracks/color-track.ts b/cocos/core/animation/tracks/color-track.ts index 8effa1c593c..6d3ebd0c9bd 100644 --- a/cocos/core/animation/tracks/color-track.ts +++ b/cocos/core/animation/tracks/color-track.ts @@ -17,7 +17,7 @@ export class ColorTrack extends Track { } } - public getChannels () { + public channels () { return this._channels; } diff --git a/cocos/core/animation/tracks/quat-track.ts b/cocos/core/animation/tracks/quat-track.ts index 385e0f00dd5..d4758b1119b 100644 --- a/cocos/core/animation/tracks/quat-track.ts +++ b/cocos/core/animation/tracks/quat-track.ts @@ -11,7 +11,7 @@ export class QuaternionTrack extends SingleChannelTrack { } public [createEvalSymbol] () { - return new QuatTrackEval(this.getChannels()[0].curve); + return new QuatTrackEval(this.channels()[0].curve); } } diff --git a/cocos/core/animation/tracks/track.ts b/cocos/core/animation/tracks/track.ts index 7f34353252e..59ebfac2094 100644 --- a/cocos/core/animation/tracks/track.ts +++ b/cocos/core/animation/tracks/track.ts @@ -1,11 +1,21 @@ import { ccclass, serializable } from 'cc.decorator'; +import type { Component } from '../../components'; import type { IntegerCurve, ObjectCurve, QuaternionCurve, RealCurve } from '../../curves'; +import { assertIsTrue } from '../../data/utils/asserts'; +import { error, warn } from '../../platform'; +import { Node } from '../../scene-graph'; +import { js } from '../../utils/js'; import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; -import { HierarchyPath, TargetPath } from '../target-path'; +import { PoseOutput } from '../pose-output'; +import { ComponentPath, HierarchyPath, isPropertyPath, TargetPath } from '../target-path'; import { IValueProxyFactory } from '../value-proxy'; import { Range } from './utils'; -export type TrackPath = TargetPath[]; +const normalizedFollowTag = Symbol('NormalizedFollow'); + +const parseTrsPathTag = Symbol('ConvertAsTrsPath'); + +export const trackBindingTag = Symbol('TrackBinding'); /** * A track describes the path of animate a target. @@ -13,19 +23,33 @@ export type TrackPath = TargetPath[]; */ @ccclass(`${CLASS_NAME_PREFIX_ANIM}Track`) export class Track { - @serializable - public path: TrackPath = []; + get path () { + return this._binding.path; + } - @serializable - public setter!: IValueProxyFactory | undefined; + set path (value) { + this._binding.path = value; + } - public getChannels (): Channel[] { + get proxy () { + return this._binding.proxy; + } + + set proxy (value) { + this._binding.proxy = value; + } + + get [trackBindingTag] () { + return this._binding; + } + + public channels (): Iterable { return []; } - public getRange (): Range { + public range (): Range { const range: Range = { min: Infinity, max: -Infinity }; - for (const channel of this.getChannels()) { + for (const channel of this.channels()) { range.min = Math.min(range.min, channel.curve.rangeMin); range.max = Math.max(range.max, channel.curve.rangeMax); } @@ -35,6 +59,9 @@ export class Track { public [createEvalSymbol] (runtimeBinding: RuntimeBinding): TrackEval { throw new Error(`No Impl`); } + + @serializable + private _binding = new TrackBinding(); } export interface TrackEval { @@ -81,7 +108,7 @@ export abstract class SingleChannelTrack extends Track { return this._channel; } - public getChannels () { + public channels () { return [this._channel]; } @@ -106,29 +133,243 @@ export type RuntimeBinding = { getValue?(): unknown; }; -export type Binder = (path: TrackPath, setter: IValueProxyFactory | undefined) => undefined | RuntimeBinding; +export type Binder = (binding: TrackBinding) => undefined | RuntimeBinding; export type TrsTrackPath = [HierarchyPath, 'position' | 'rotation' | 'scale' | 'eulerAngles']; -export function isTargetingTRS (path: TargetPath[]): path is TrsTrackPath { - let prs: string | undefined; - if (path.length === 1 && typeof path[0] === 'string') { - prs = path[0]; - } else if (path.length > 1) { - for (let i = 0; i < path.length - 1; ++i) { - if (!(path[i] instanceof HierarchyPath)) { - return false; +@ccclass(`${CLASS_NAME_PREFIX_ANIM}TrackPath`) +class TrackPath { + get length () { + return this._paths.length; + } + + public property (name: string) { + this._paths.push(name); + return this; + } + + public element (index: number) { + this._paths.push(index); + return this; + } + + public hierarchy (nodePath: string) { + this._paths.push(new HierarchyPath(nodePath)); + return this; + } + + public component (constructor: Constructor | string) { + const path = new ComponentPath(typeof constructor === 'string' ? constructor : js.getClassName(constructor)); + this._paths.push(path); + return this; + } + + public customized (resolver: CustomizedTrackPathResolver) { + this._paths.push(resolver); + return this; + } + + public append (...trackPaths: TrackPath[]) { + const paths = ([] as TargetPath[]).concat(...trackPaths.map((trackPath) => trackPath._paths)); + this._paths = paths; + return this; + } + + public isPropertyAt (index: number) { + return typeof (this._paths[index]) === 'string'; + } + + public parsePropertyAt (index: number): string { + return this._paths[index] as string; + } + + public isElementAt (index: number) { + return typeof this._paths[index] === 'number'; + } + + public parseElementAt (index: number): number { + return this._paths[index] as number; + } + + public isHierarchyAt (index: number) { + return this._paths[index] instanceof HierarchyPath; + } + + public parseHierarchyAt (index: number) { + assertIsTrue(this.isHierarchyAt(index)); + return (this._paths[index] as HierarchyPath).path; + } + + public isComponentAt (index: number) { + return this._paths[index] instanceof ComponentPath; + } + + public parseComponentAt (index: number) { + assertIsTrue(this.isHierarchyAt(index)); + return (this._paths[index] as ComponentPath).component; + } + + public slice (beginIndex?: number, endIndex?: number) { + const trackPath = new TrackPath(); + trackPath._paths = this._paths.slice(beginIndex, endIndex); + return trackPath; + } + + public follow (object: unknown, beginIndex?: number, endIndex?: number) { + beginIndex ??= 0; + endIndex ??= this._paths.length; + return this[normalizedFollowTag](object, beginIndex, endIndex, undefined, false); + } + + public [parseTrsPathTag] () { + const { _paths: paths } = this; + const nPaths = paths.length; + + let iPath = 0; + + let nodePath = ''; + for (; iPath < nPaths; ++iPath) { + const path = paths[iPath]; + if (!(path instanceof HierarchyPath)) { + break; + } else if (!path.path) { + continue; + } else if (nodePath) { + nodePath += `/${path.path}`; + } else { + nodePath = path.path; } } - prs = path[path.length - 1] as string; + + if (iPath === nPaths) { + return null; + } + + let prs: 'position' | 'scale' | 'rotation' | 'eulerAngles'; + if (iPath !== nPaths - 1) { + return null; + } + + switch (paths[iPath]) { + case 'position': + case 'scale': + case 'rotation': + case 'eulerAngles': + prs = paths[iPath] as typeof prs; + break; + default: + return null; + } + + return { node: nodePath, property: prs }; + } + + public [normalizedFollowTag] (root: unknown, beginIndex: number, endIndex: number, poseOutput: PoseOutput | undefined, isConstant: boolean) { + const { _paths: paths } = this; + let result = root; + for (let iPath = beginIndex; iPath < endIndex; ++iPath) { + const path = paths[iPath]; + if (isPropertyPath(path)) { + if (!(path in (result as any))) { + warn(`Target object has no property "${path}"`); + return null; + } else { + if (poseOutput && iPath === endIndex - 1 && result instanceof Node && isTrsPropertyName(path)) { + const blendStateWriter = poseOutput.createPoseWriter(result, path, isConstant); + return blendStateWriter; + } + result = (result as any)[path]; + } + } else { + result = path.get(result); + } + if (result === null) { + break; + } + } + return result; + } + + @serializable + private _paths: TargetPath[] = []; +} + +/** + * Composite of track path and value proxy. + * Not exposed to external. If there is any reason it should be exposed, + * please redesign the public interfaces. + */ +@ccclass(`${CLASS_NAME_PREFIX_ANIM}TrackBinding`) +export class TrackBinding { + @serializable + public path: Readonly = new TrackPath(); + + @serializable + public proxy: IValueProxyFactory | undefined; + + public parseTrsPath () { + if (this.proxy) { + return null; + } else { + return this.path[parseTrsPathTag](); + } } - switch (prs) { - case 'position': - case 'scale': - case 'rotation': - case 'eulerAngles': - return true; - default: - return false; + + public createRuntimeBinding (target: unknown, poseOutput: PoseOutput | undefined, isConstant: boolean) { + const { path, proxy } = this; + const nPaths = path.length; + const iLastPath = nPaths - 1; + if (nPaths !== 0 && (path.isPropertyAt(iLastPath) || path.isElementAt(iLastPath)) && !proxy) { + const lastPropertyKey = path.isPropertyAt(iLastPath) + ? path.parsePropertyAt(iLastPath) + : path.parseElementAt(iLastPath); + const resultTarget = path[normalizedFollowTag](target, 0, nPaths - 1, poseOutput, isConstant) as any; + if (resultTarget === null) { + return null; + } + return { + setValue: (value: unknown) => { + resultTarget[lastPropertyKey] = value; + }, + // eslint-disable-next-line arrow-body-style + getValue: () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return resultTarget[lastPropertyKey]; + }, + }; + } else if (!proxy) { + error( + `You provided a ill-formed track path.` + + `The last component of track path should be property key, or the setter should not be empty.`, + ); + return null; + } else { + const resultTarget = path[normalizedFollowTag](target, 0, nPaths, poseOutput, isConstant); + if (resultTarget === null) { + return null; + } + const runtimeProxy = proxy.forTarget(resultTarget); + const binding: RuntimeBinding = { + setValue: (value) => { + runtimeProxy.set(value); + }, + }; + const proxyGet = runtimeProxy.get; + if (proxyGet) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + binding.getValue = () => proxyGet.call(runtimeProxy); + } + return binding; + } } } + +function isTrsPropertyName (name: string | number): name is 'position' | 'rotation' | 'scale' | 'eulerAngles' { + return name === 'position' || name === 'rotation' || name === 'scale' || name === 'eulerAngles'; +} + +interface CustomizedTrackPathResolver { + get (target: unknown): unknown; +} + +export { TrackPath }; diff --git a/cocos/core/animation/tracks/untyped-track.ts b/cocos/core/animation/tracks/untyped-track.ts index d313d95214c..fd72f902322 100644 --- a/cocos/core/animation/tracks/untyped-track.ts +++ b/cocos/core/animation/tracks/untyped-track.ts @@ -22,7 +22,7 @@ export class UntypedTrack extends Track { @serializable private _channels: UntypedTrackChannel[] = []; - public getChannels () { + public channels () { return this._channels; } @@ -74,7 +74,7 @@ export class UntypedTrack extends Track { public upgrade (refine: UntypedTrackRefine): Track | null { const trySearchChannel = (property: string, outChannel: RealChannel) => { - const untypedChannel = this.getChannels().find((channel) => channel.property === property); + const untypedChannel = this.channels().find((channel) => channel.property === property); if (untypedChannel) { outChannel.name = untypedChannel.name; outChannel.curve.assignSorted( @@ -83,14 +83,14 @@ export class UntypedTrack extends Track { ); } }; - const kind = refine(this.path, this.setter); + const kind = refine(this.path, this.proxy); switch (kind) { default: break; case 'vec2': case 'vec3': case 'vec4': { const track = new VectorTrack(); track.componentsCount = kind === 'vec2' ? 2 : kind === 'vec3' ? 3 : 4; - const [x, y, z, w] = track.getChannels(); + const [x, y, z, w] = track.channels(); switch (kind) { case 'vec4': trySearchChannel('w', w); @@ -107,7 +107,7 @@ export class UntypedTrack extends Track { } case 'color': { const track = new ColorTrack(); - const [r, g, b, a] = track.getChannels(); + const [r, g, b, a] = track.channels(); trySearchChannel('r', r); trySearchChannel('g', g); trySearchChannel('b', b); @@ -127,4 +127,4 @@ export class UntypedTrack extends Track { } } -export type UntypedTrackRefine = (path: TrackPath, setter?: IValueProxyFactory) => 'vec2' | 'vec3' | 'vec4' | 'color' | 'size'; +export type UntypedTrackRefine = (path: Readonly, proxy: IValueProxyFactory | undefined) => 'vec2' | 'vec3' | 'vec4' | 'color' | 'size'; diff --git a/cocos/core/animation/tracks/vector-track.ts b/cocos/core/animation/tracks/vector-track.ts index 41c57120348..913a7ebe1ff 100644 --- a/cocos/core/animation/tracks/vector-track.ts +++ b/cocos/core/animation/tracks/vector-track.ts @@ -25,7 +25,7 @@ export class VectorTrack extends Track { this._nComponents = value; } - public getChannels () { + public channels () { return this._channels; } diff --git a/tests/animation/animaion-clip-migration-3.x.test.ts b/tests/animation/animaion-clip-migration-3.x.test.ts index d2224403164..fc182792089 100644 --- a/tests/animation/animaion-clip-migration-3.x.test.ts +++ b/tests/animation/animaion-clip-migration-3.x.test.ts @@ -49,7 +49,7 @@ describe('Animation Clip Migration 3.x', () => { const track = clip.getTrack(0) as animation.VectorTrack; expect(track).toBeInstanceOf(animation.VectorTrack); expect(track.componentsCount).toBe(2); - const [{ curve: x }, { curve: y }] = track.getChannels(); + const [{ curve: x }, { curve: y }] = track.channels(); expect(Array.from(x.times())).toStrictEqual([0.0, 0.2, 0.8]); expect(Array.from(x.values())).toStrictEqual( createRealKeyframesWithoutTangent([1.0, 2.0, 3.0], RealInterpMode.LINEAR), @@ -80,7 +80,7 @@ describe('Animation Clip Migration 3.x', () => { const track = clip.getTrack(0) as animation.VectorTrack; expect(track).toBeInstanceOf(animation.VectorTrack); expect(track.componentsCount).toBe(3); - const [{ curve: x }, { curve: y }, { curve: z }] = track.getChannels(); + const [{ curve: x }, { curve: y }, { curve: z }] = track.channels(); expect(Array.from(x.times())).toStrictEqual([0.0, 0.2, 0.8]); expect(Array.from(x.values())).toStrictEqual( createRealKeyframesWithoutTangent([1.0, 2.0, 3.0], RealInterpMode.LINEAR), @@ -114,7 +114,7 @@ describe('Animation Clip Migration 3.x', () => { const track = clip.getTrack(0) as animation.VectorTrack; expect(track).toBeInstanceOf(animation.VectorTrack); expect(track.componentsCount).toBe(4); - const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.getChannels(); + const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.channels(); expect(Array.from(x.times())).toStrictEqual([0.0, 0.2, 0.8]); expect(Array.from(x.values())).toStrictEqual( createRealKeyframesWithoutTangent([1.0, 2.0, 3.0], RealInterpMode.LINEAR), @@ -151,7 +151,7 @@ describe('Animation Clip Migration 3.x', () => { expect(clip.tracksCount).toBe(1); const track = clip.getTrack(0) as animation.ColorTrack; expect(track).toBeInstanceOf(animation.ColorTrack); - const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.getChannels(); + const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.channels(); expect(Array.from(r.times())).toStrictEqual([0.0, 0.2, 0.8]); expect(Array.from(r.values())).toStrictEqual( createRealKeyframesWithoutTangent([10, 20, 30], RealInterpMode.LINEAR), diff --git a/tests/animation/animation-clip-3.x.test.ts b/tests/animation/animation-clip-3.x.test.ts index 83b6dba78d1..eda9b5e2ee2 100644 --- a/tests/animation/animation-clip-3.x.test.ts +++ b/tests/animation/animation-clip-3.x.test.ts @@ -1,5 +1,6 @@ import { Mat4, Node, RealKeyframeValue, Vec3 } from '../../cocos/core'; -import { AnimationClip, RealTrack, searchForRootBonePathSymbol, VectorTrack } from '../../cocos/core/animation/animation-clip'; +import { RealTrack, TrackPath, VectorTrack } from '../../cocos/core/animation/animation'; +import { AnimationClip, searchForRootBonePathSymbol } from '../../cocos/core/animation/animation-clip'; import { ComponentPath, HierarchyPath, TargetPath } from '../../cocos/core/animation/target-path'; describe('Animation Clip', () => { @@ -35,7 +36,7 @@ describe('Animation Clip', () => { function createClipWithPath (path: TargetPath[]) { const clip = new AnimationClip(); const track = new RealTrack(); - track.path = [new HierarchyPath('Foo')]; + track.path = new TrackPath().hierarchy('Foo'); clip.addTrack(track); return clip; } @@ -46,7 +47,7 @@ describe('Animation Clip', () => { const clip = new AnimationClip(); for (const path of paths) { const track = new VectorTrack(); - track.path = [new HierarchyPath(path), 'position']; + track.path = new TrackPath().hierarchy(path).property('position'); clip.addTrack(track); } return clip[searchForRootBonePathSymbol](); @@ -69,8 +70,8 @@ describe('Animation Clip', () => { const rootBoneTranslationTrack = new VectorTrack(); { rootBoneTranslationTrack.componentsCount = 3; - rootBoneTranslationTrack.path = [new HierarchyPath(rootJointName), 'position']; - const [x, _y, _z] = rootBoneTranslationTrack.getChannels(); + rootBoneTranslationTrack.path = new TrackPath().hierarchy(rootJointName).property('position'); + const [x, _y, _z] = rootBoneTranslationTrack.channels(); x.curve.assignSorted([ [0.4, new RealKeyframeValue({ value: 0.4 })], [0.6, new RealKeyframeValue({ value: 0.6 })], diff --git a/tests/animation/animation-clip.test.ts b/tests/animation/animation-clip.test.ts index a4481e02463..12df416d452 100644 --- a/tests/animation/animation-clip.test.ts +++ b/tests/animation/animation-clip.test.ts @@ -145,8 +145,8 @@ describe('Custom track setter', () => { const mockSetValue = target.setValue = jest.fn(target.setValue); const track = new VectorTrack(); - track.setter = valueProxyWithGetSet; - track.getChannels().forEach(({ curve }) => { + track.proxy = valueProxyWithGetSet; + track.channels().forEach(({ curve }) => { curve.assignSorted([[0.0, new RealKeyframeValue({ value: 0.0 })]]); }); @@ -166,7 +166,7 @@ describe('Custom track setter', () => { const mockSetValue = target.setValue = jest.fn(target.setValue); const track = new VectorTrack(); - track.setter = valueProxyWithGetSet; + track.proxy = valueProxyWithGetSet; const clip = new AnimationClip(); clip.addTrack(track); const clipEval = clip.createEvaluator({ @@ -182,7 +182,7 @@ describe('Custom track setter', () => { const mockSetValue = target.setValue = jest.fn(target.setValue); const track = new VectorTrack(); - track.setter = valueProxyWithOnlySet; + track.proxy = valueProxyWithOnlySet; const clip = new AnimationClip(); clip.addTrack(track); const clipEval = clip.createEvaluator({ From 75157cdb9f110d3c2a572b2cf7770de3ce6eb765 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Fri, 2 Jul 2021 15:07:00 +0800 Subject: [PATCH 07/35] Update --- cocos/core/animation/tracks/track.ts | 3 ++- cocos/core/curves/quat-curve.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cocos/core/animation/tracks/track.ts b/cocos/core/animation/tracks/track.ts index 59ebfac2094..87181671222 100644 --- a/cocos/core/animation/tracks/track.ts +++ b/cocos/core/animation/tracks/track.ts @@ -1,4 +1,4 @@ -import { ccclass, serializable } from 'cc.decorator'; +import { ccclass, serializable, uniquelyReferenced } from 'cc.decorator'; import type { Component } from '../../components'; import type { IntegerCurve, ObjectCurve, QuaternionCurve, RealCurve } from '../../curves'; import { assertIsTrue } from '../../data/utils/asserts'; @@ -300,6 +300,7 @@ class TrackPath { * please redesign the public interfaces. */ @ccclass(`${CLASS_NAME_PREFIX_ANIM}TrackBinding`) +@uniquelyReferenced export class TrackBinding { @serializable public path: Readonly = new TrackPath(); diff --git a/cocos/core/curves/quat-curve.ts b/cocos/core/curves/quat-curve.ts index 664f31a73a6..ab512505ccb 100644 --- a/cocos/core/curves/quat-curve.ts +++ b/cocos/core/curves/quat-curve.ts @@ -18,13 +18,13 @@ export class QuaternionKeyframeValue { /** * Value of the keyframe. */ - @serializable + @serializable public value: IQuatLike = Quat.clone(Quat.IDENTITY); constructor ({ value, interpMode, - }: Partial) { + }: Partial = {}) { // TODO: shall we normalize it? this.value = value ? Quat.clone(value) : this.value; this.interpMode = interpMode ?? this.interpMode; From 9f028559d922b6e73148b7076595fdc39fd1a3c7 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Fri, 2 Jul 2021 16:34:19 +0800 Subject: [PATCH 08/35] Fix BUG --- cocos/core/animation/tracks/track.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocos/core/animation/tracks/track.ts b/cocos/core/animation/tracks/track.ts index 87181671222..6cb8c2677f7 100644 --- a/cocos/core/animation/tracks/track.ts +++ b/cocos/core/animation/tracks/track.ts @@ -170,7 +170,7 @@ class TrackPath { } public append (...trackPaths: TrackPath[]) { - const paths = ([] as TargetPath[]).concat(...trackPaths.map((trackPath) => trackPath._paths)); + const paths = this._paths.concat(...trackPaths.map((trackPath) => trackPath._paths)); this._paths = paths; return this; } From 8e1c3292f106595e9a1a16696f7415457ddcef5d Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Fri, 2 Jul 2021 16:48:36 +0800 Subject: [PATCH 09/35] Fix --- cocos/core/animation/tracks/track.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocos/core/animation/tracks/track.ts b/cocos/core/animation/tracks/track.ts index 6cb8c2677f7..45908326e78 100644 --- a/cocos/core/animation/tracks/track.ts +++ b/cocos/core/animation/tracks/track.ts @@ -205,7 +205,7 @@ class TrackPath { } public parseComponentAt (index: number) { - assertIsTrue(this.isHierarchyAt(index)); + assertIsTrue(this.isComponentAt(index)); return (this._paths[index] as ComponentPath).component; } From 9a0d2d1e55ab466d5bed9bc14145b92d5c3fff8f Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Mon, 12 Jul 2021 12:05:09 +0800 Subject: [PATCH 10/35] Add size track --- cocos/core/animation/animation.ts | 1 + cocos/core/animation/legacy-clip-data.ts | 35 +++++++++++- cocos/core/animation/tracks/color-track.ts | 4 +- cocos/core/animation/tracks/size-track.ts | 63 +++++++++++++++++++++ cocos/core/animation/tracks/track.ts | 2 +- cocos/core/animation/tracks/vector-track.ts | 4 +- tests/animation/animation-clip-3.x.test.ts | 33 ++++++++++- 7 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 cocos/core/animation/tracks/size-track.ts diff --git a/cocos/core/animation/animation.ts b/cocos/core/animation/animation.ts index 7ef6e342f9d..56b7db1a453 100644 --- a/cocos/core/animation/animation.ts +++ b/cocos/core/animation/animation.ts @@ -39,4 +39,5 @@ export { IntegerTrack } from './tracks/integer-track'; export { VectorTrack } from './tracks/vector-track'; export { QuaternionTrack } from './tracks/quat-track'; export { ColorTrack } from './tracks/color-track'; +export { SizeTrack } from './tracks/size-track'; export { ObjectTrack } from './tracks/object-track'; diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts index 14b06be465f..ab86e45add1 100644 --- a/cocos/core/animation/legacy-clip-data.ts +++ b/cocos/core/animation/legacy-clip-data.ts @@ -11,12 +11,13 @@ import { Track, TrackPath } from './tracks/track'; import { UntypedTrack } from './tracks/untyped-track'; import { warn } from '../platform'; import { RealTrack } from './tracks/real-track'; -import { Color, Quat, Vec2, Vec3, Vec4 } from '../math'; +import { Color, Quat, Size, Vec2, Vec3, Vec4 } from '../math'; import { CubicSplineNumberValue, CubicSplineVec2Value, CubicSplineVec3Value, CubicSplineVec4Value } from './cubic-spline-value'; import { ColorTrack } from './tracks/color-track'; import { VectorTrack } from './tracks/vector-track'; import { QuaternionTrack } from './tracks/quat-track'; import { ObjectTrack } from './tracks/object-track'; +import { SizeTrack } from './tracks/size-track'; /** * 表示曲线值,曲线值可以是任意类型,但必须符合插值方式的要求。 @@ -111,6 +112,25 @@ export type LegacyRuntimeCurve = Pick { +// valueConstructor: Constructor; +// trackConstructor: Constructor; +// properties: [keyof TValue, number][]; +// } + +// const VECTOR_LIKE_CURVE_CONVERT_TABLE = [ +// { +// valueConstructor: Size, +// trackConstructor: SizeTrack, +// properties: [['width', 0], ['height', 1]], +// } as ConvertMap, +// { +// valueConstructor: Color, +// trackConstructor: ColorTrack, +// properties: [['r', 0], ['g', 1], ['b', 2], ['a', 3]], +// } as ConvertMap, +// ]; + export class AnimationClipLegacyData { constructor (duration: number) { this._duration = duration; @@ -315,6 +335,19 @@ export class AnimationClipLegacyData { newTracks.push(track); return; } + case legacyValues.every((value) => value instanceof Size): { + const track = new SizeTrack(); + installPathAndSetter(track); + const [{ curve: width }, { curve: height }] = track.channels(); + const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); + width.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.r))); + legacyEasingMethodConverter.convert(width); + height.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.g))); + legacyEasingMethodConverter.convert(height); + newTracks.push(track); + return; + } case legacyValues.every((value) => value instanceof CubicSplineNumberValue): { assertIsTrue(legacyEasingMethodConverter.nil); const track = new RealTrack(); diff --git a/cocos/core/animation/tracks/color-track.ts b/cocos/core/animation/tracks/color-track.ts index 6d3ebd0c9bd..a302aadd5b4 100644 --- a/cocos/core/animation/tracks/color-track.ts +++ b/cocos/core/animation/tracks/color-track.ts @@ -5,6 +5,8 @@ import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; import { Channel, IntegerChannel, RuntimeBinding, Track } from './track'; import { maskIfEmpty } from './utils'; +const CHANNEL_NAMES: ReadonlyArray = ['Red', 'Green', 'Blue', 'Alpha']; + @ccclass(`${CLASS_NAME_PREFIX_ANIM}ColorTrack`) export class ColorTrack extends Track { constructor () { @@ -12,7 +14,7 @@ export class ColorTrack extends Track { this._channels = new Array(4) as ColorTrack['_channels']; for (let i = 0; i < this._channels.length; ++i) { const channel = new Channel(new IntegerCurve()); - channel.name = 'R'; + channel.name = CHANNEL_NAMES[i]; this._channels[i] = channel; } } diff --git a/cocos/core/animation/tracks/size-track.ts b/cocos/core/animation/tracks/size-track.ts new file mode 100644 index 00000000000..dd748f203fe --- /dev/null +++ b/cocos/core/animation/tracks/size-track.ts @@ -0,0 +1,63 @@ +import { ccclass, serializable } from 'cc.decorator'; +import { RealCurve } from '../../curves'; +import { Size } from '../../math'; +import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; +import { Channel, RealChannel, RuntimeBinding, Track } from './track'; +import { maskIfEmpty } from './utils'; + +const CHANNEL_NAMES: ReadonlyArray = ['Width', 'Height']; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}SizeTrack`) +export class SizeTrack extends Track { + constructor () { + super(); + this._channels = new Array(2) as SizeTrack['_channels']; + for (let i = 0; i < this._channels.length; ++i) { + const channel = new Channel(new RealCurve()); + channel.name = CHANNEL_NAMES[i]; + this._channels[i] = channel; + } + } + + public channels () { + return this._channels; + } + + public [createEvalSymbol] () { + return new SizeTrackEval( + maskIfEmpty(this._channels[0].curve), + maskIfEmpty(this._channels[1].curve), + ); + } + + @serializable + private _channels: [RealChannel, RealChannel]; +} + +class SizeTrackEval { + constructor ( + private _width: RealCurve | undefined, + private _height: RealCurve | undefined, + ) { + + } + + public evaluate (time: number, runtimeBinding: RuntimeBinding) { + if ((!this._width || !this._height) && runtimeBinding.getValue) { + const size = runtimeBinding.getValue() as Size; + this._result.x = size.x; + this._result.y = size.y; + } + + if (this._width) { + this._result.width = this._width.evaluate(time); + } + if (this._height) { + this._result.height = this._height.evaluate(time); + } + + return this._result; + } + + private _result: Size = new Size(); +} diff --git a/cocos/core/animation/tracks/track.ts b/cocos/core/animation/tracks/track.ts index 45908326e78..a77f9c251c8 100644 --- a/cocos/core/animation/tracks/track.ts +++ b/cocos/core/animation/tracks/track.ts @@ -80,7 +80,7 @@ export class Channel { this._curve = curve; } - @serializable + // @serializable public name = ''; get curve () { diff --git a/cocos/core/animation/tracks/vector-track.ts b/cocos/core/animation/tracks/vector-track.ts index 913a7ebe1ff..0608912bca1 100644 --- a/cocos/core/animation/tracks/vector-track.ts +++ b/cocos/core/animation/tracks/vector-track.ts @@ -5,6 +5,8 @@ import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; import { Channel, RealChannel, RuntimeBinding, Track } from './track'; import { maskIfEmpty } from './utils'; +const CHANNEL_NAMES: ReadonlyArray = ['X', 'Y', 'Z', 'W']; + @ccclass(`${CLASS_NAME_PREFIX_ANIM}VectorTrack`) export class VectorTrack extends Track { constructor () { @@ -12,7 +14,7 @@ export class VectorTrack extends Track { this._channels = new Array(4) as VectorTrack['_channels']; for (let i = 0; i < this._channels.length; ++i) { const channel = new Channel(new RealCurve()); - channel.name = 'X'; + channel.name = CHANNEL_NAMES[i]; this._channels[i] = channel; } } diff --git a/tests/animation/animation-clip-3.x.test.ts b/tests/animation/animation-clip-3.x.test.ts index eda9b5e2ee2..cbc1ffe6a44 100644 --- a/tests/animation/animation-clip-3.x.test.ts +++ b/tests/animation/animation-clip-3.x.test.ts @@ -1,7 +1,7 @@ -import { Mat4, Node, RealKeyframeValue, Vec3 } from '../../cocos/core'; -import { RealTrack, TrackPath, VectorTrack } from '../../cocos/core/animation/animation'; +import { Node, RealKeyframeValue, Vec3 } from '../../cocos/core'; +import { ColorTrack, RealTrack, SizeTrack, TrackPath, VectorTrack } from '../../cocos/core/animation/animation'; import { AnimationClip, searchForRootBonePathSymbol } from '../../cocos/core/animation/animation-clip'; -import { ComponentPath, HierarchyPath, TargetPath } from '../../cocos/core/animation/target-path'; +import { TargetPath } from '../../cocos/core/animation/target-path'; describe('Animation Clip', () => { describe('Evaluation', () => { @@ -123,4 +123,31 @@ describe('Animation Clip', () => { }); }); }); + + describe(`Tracks`, () => { + test('Vector track', () => { + const vectorTrack = new VectorTrack(); + expect(vectorTrack.channels()).toHaveLength(4); + expect(vectorTrack.channels()[0].name).toBe('X'); + expect(vectorTrack.channels()[1].name).toBe('Y'); + expect(vectorTrack.channels()[2].name).toBe('Z'); + expect(vectorTrack.channels()[3].name).toBe('W'); + }); + + test('Color track', () => { + const colorTrack = new ColorTrack(); + expect(colorTrack.channels()).toHaveLength(4); + expect(colorTrack.channels()[0].name).toBe('Red'); + expect(colorTrack.channels()[1].name).toBe('Green'); + expect(colorTrack.channels()[2].name).toBe('Blue'); + expect(colorTrack.channels()[3].name).toBe('Alpha'); + }); + + test('Size track', () => { + const sizeTrack = new SizeTrack(); + expect(sizeTrack.channels()).toHaveLength(2); + expect(sizeTrack.channels()[0].name).toBe('Width'); + expect(sizeTrack.channels()[1].name).toBe('Height'); + }); + }); }); \ No newline at end of file From 154c1a3f6a77a54ad2936be33a67cb0dfc2e47f4 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Mon, 12 Jul 2021 17:26:06 +0800 Subject: [PATCH 11/35] Update --- cocos/core/animation/legacy-clip-data.ts | 35 ++++++++++++---------- editor/exports/animation-clip-migration.ts | 2 ++ 2 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 editor/exports/animation-clip-migration.ts diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts index ab86e45add1..700f70b752c 100644 --- a/cocos/core/animation/legacy-clip-data.ts +++ b/cocos/core/animation/legacy-clip-data.ts @@ -12,7 +12,7 @@ import { UntypedTrack } from './tracks/untyped-track'; import { warn } from '../platform'; import { RealTrack } from './tracks/real-track'; import { Color, Quat, Size, Vec2, Vec3, Vec4 } from '../math'; -import { CubicSplineNumberValue, CubicSplineVec2Value, CubicSplineVec3Value, CubicSplineVec4Value } from './cubic-spline-value'; +import { CubicSplineNumberValue, CubicSplineQuatValue, CubicSplineVec2Value, CubicSplineVec3Value, CubicSplineVec4Value } from './cubic-spline-value'; import { ColorTrack } from './tracks/color-track'; import { VectorTrack } from './tracks/vector-track'; import { QuaternionTrack } from './tracks/quat-track'; @@ -274,9 +274,9 @@ export class AnimationClipLegacyData { switch (true) { default: break; - case legacyValues.every((value) => value instanceof Vec2): - case legacyValues.every((value) => value instanceof Vec3): - case legacyValues.every((value) => value instanceof Vec4): { + case everyInstanceOf(legacyValues, Vec2): + case everyInstanceOf(legacyValues, Vec3): + case everyInstanceOf(legacyValues, Vec4): { type Vec4plus = Vec4[]; type Vec3plus = (Vec3 | Vec4)[]; type Vec2plus = (Vec2 | Vec3 | Vec4)[]; @@ -306,7 +306,7 @@ export class AnimationClipLegacyData { newTracks.push(track); return; } - case legacyValues.every((value) => value instanceof Quat): { + case everyInstanceOf(legacyValues, Quat): { assertIsTrue(legacyEasingMethodConverter.nil); const track = new QuaternionTrack(); installPathAndSetter(track); @@ -318,7 +318,7 @@ export class AnimationClipLegacyData { newTracks.push(track); return; } - case legacyValues.every((value) => value instanceof Color): { + case everyInstanceOf(legacyValues, Color): { const track = new ColorTrack(); installPathAndSetter(track); const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.channels(); @@ -335,7 +335,7 @@ export class AnimationClipLegacyData { newTracks.push(track); return; } - case legacyValues.every((value) => value instanceof Size): { + case everyInstanceOf(legacyValues, Size): { const track = new SizeTrack(); installPathAndSetter(track); const [{ curve: width }, { curve: height }] = track.channels(); @@ -348,7 +348,7 @@ export class AnimationClipLegacyData { newTracks.push(track); return; } - case legacyValues.every((value) => value instanceof CubicSplineNumberValue): { + case everyInstanceOf(legacyValues, CubicSplineNumberValue): { assertIsTrue(legacyEasingMethodConverter.nil); const track = new RealTrack(); installPathAndSetter(track); @@ -362,9 +362,9 @@ export class AnimationClipLegacyData { newTracks.push(track); return; } - case legacyValues.every((value) => value instanceof CubicSplineVec2Value): - case legacyValues.every((value) => value instanceof CubicSplineVec3Value): - case legacyValues.every((value) => value instanceof CubicSplineVec4Value): { + case everyInstanceOf(legacyValues, CubicSplineVec2Value): + case everyInstanceOf(legacyValues, CubicSplineVec3Value): + case everyInstanceOf(legacyValues, CubicSplineVec4Value): { assertIsTrue(legacyEasingMethodConverter.nil); type Vec4plus = CubicSplineVec4Value[]; type Vec3plus = (CubicSplineVec3Value | CubicSplineVec4Value)[]; @@ -404,6 +404,10 @@ export class AnimationClipLegacyData { newTracks.push(track); return; } + case legacyValues.every((value) => value instanceof CubicSplineQuatValue): { + warn(`We don't currently support conversion of \`CubicSplineQuatValue\`.`); + break; + } } // End switch } @@ -419,13 +423,10 @@ export class AnimationClipLegacyData { return newTracks; } - @serializable private _keys: number[][] = []; - @serializable private _curves: LegacyClipCurve[] = []; - @serializable private _commonTargets: LegacyCommonTarget[] = []; private _ratioSamplers: RatioSampler[] = []; @@ -455,6 +456,10 @@ export class AnimationClipLegacyData { } } +function everyInstanceOf (array: unknown[], constructor: Constructor): array is T[] { + return array.every((element) => element instanceof constructor); +} + // #region Legacy data structures prior to 1.2 export interface LegacyObjectCurveData { @@ -488,7 +493,7 @@ class LegacyEasingMethodConverter { } get nil () { - return !this._easingMethods; + return !this._easingMethods || this._easingMethods.every((easingMethod) => easingMethod === null || easingMethod === undefined); } public convert (curve: RealCurve) { diff --git a/editor/exports/animation-clip-migration.ts b/editor/exports/animation-clip-migration.ts new file mode 100644 index 00000000000..72d4c500cbf --- /dev/null +++ b/editor/exports/animation-clip-migration.ts @@ -0,0 +1,2 @@ + +export { AnimationClipLegacyData } from '../../cocos/core/animation/legacy-clip-data'; From e579f8434a94f2b20c1ecc52a4d6e85cdbdb08d7 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Mon, 12 Jul 2021 20:12:18 +0800 Subject: [PATCH 12/35] Fix size; unit tests --- cocos/core/animation/legacy-clip-data.ts | 24 +- cocos/core/animation/tracks/object-track.ts | 2 +- cocos/core/animation/tracks/track.ts | 2 +- .../animaion-clip-migration-3.x.test.ts | 632 +++++++++++++----- 4 files changed, 484 insertions(+), 176 deletions(-) diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts index 700f70b752c..4251405b047 100644 --- a/cocos/core/animation/legacy-clip-data.ts +++ b/cocos/core/animation/legacy-clip-data.ts @@ -222,7 +222,7 @@ export class AnimationClipLegacyData { const times = legacyKeysIndex < 0 ? [0.0] : legacyKeys[legacyCurveData.keys]; const firstValue = legacyValues[0]; // Rule: default to true. - const interpolate = legacyCurveData ?? true; + const interpolate = legacyCurveData.interpolate ?? true; // Rule: _arrayLength only used for morph target, internally. assertIsTrue(typeof legacyCurveData._arrayLength !== 'number' || typeof firstValue === 'number'); const legacyEasingMethodConverter = new LegacyEasingMethodConverter(legacyCurveData, times.length); @@ -341,9 +341,9 @@ export class AnimationClipLegacyData { const [{ curve: width }, { curve: height }] = track.channels(); const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); - width.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.r))); + width.assignSorted(times, (legacyValues as Size[]).map((value) => valueToFrame(value.width))); legacyEasingMethodConverter.convert(width); - height.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.g))); + height.assignSorted(times, (legacyValues as Size[]).map((value) => valueToFrame(value.height))); legacyEasingMethodConverter.convert(height); newTracks.push(track); return; @@ -519,21 +519,21 @@ class LegacyEasingMethodConverter { } if (Array.isArray(easingMethod)) { // Time bezier points - const previousKeyframeValue = curve.getKeyframeValue(iKeyframe); + const currentKeyframeValue = curve.getKeyframeValue(iKeyframe); const nextKeyframeValue = curve.getKeyframeValue(iKeyframe + 1); - const [endTangent, endTangentWeight, startTangent, startTangentWeight] = timeBezierToTangents( + const [previousTangent, previousTangentWeight, nextTangent, nextTangentWeight] = timeBezierToTangents( easingMethod, curve.getKeyframeTime(iKeyframe), - previousKeyframeValue.value, + currentKeyframeValue.value, curve.getKeyframeTime(iKeyframe + 1), nextKeyframeValue.value, ); - previousKeyframeValue.interpMode = RealInterpMode.CUBIC; - previousKeyframeValue.endTangent = endTangent; - previousKeyframeValue.endTangentWeight = endTangentWeight; - nextKeyframeValue.startTangent = startTangent; - nextKeyframeValue.startTangentWeight = startTangentWeight; - nextKeyframeValue.tangentWeightMode = TangentWeightMode.BOTH; + currentKeyframeValue.interpMode = RealInterpMode.CUBIC; + currentKeyframeValue.tangentWeightMode = TangentWeightMode.BOTH; + currentKeyframeValue.startTangent = previousTangent; + currentKeyframeValue.startTangentWeight = previousTangentWeight; + currentKeyframeValue.endTangent = nextTangent; + currentKeyframeValue.endTangentWeight = nextTangentWeight; } else { const bernstein = new Array(4).fill(0); // Easing methods in `easing` diff --git a/cocos/core/animation/tracks/object-track.ts b/cocos/core/animation/tracks/object-track.ts index baed5686cf2..9d3b9a73c71 100644 --- a/cocos/core/animation/tracks/object-track.ts +++ b/cocos/core/animation/tracks/object-track.ts @@ -4,7 +4,7 @@ import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; import { SingleChannelTrack } from './track'; @ccclass(`${CLASS_NAME_PREFIX_ANIM}ObjectTrack`) -export class ObjectTrack extends SingleChannelTrack> { +export class ObjectTrack extends SingleChannelTrack> { protected createCurve () { return new ObjectCurve(); } diff --git a/cocos/core/animation/tracks/track.ts b/cocos/core/animation/tracks/track.ts index a77f9c251c8..9b11934a389 100644 --- a/cocos/core/animation/tracks/track.ts +++ b/cocos/core/animation/tracks/track.ts @@ -108,7 +108,7 @@ export abstract class SingleChannelTrack extends Track { return this._channel; } - public channels () { + public channels (): Iterable> { return [this._channel]; } diff --git a/tests/animation/animaion-clip-migration-3.x.test.ts b/tests/animation/animaion-clip-migration-3.x.test.ts index fc182792089..1ac16c10875 100644 --- a/tests/animation/animaion-clip-migration-3.x.test.ts +++ b/tests/animation/animaion-clip-migration-3.x.test.ts @@ -1,181 +1,466 @@ +import { SpriteFrame } from "../../cocos/2d/assets"; import { math, RealInterpMode } from "../../cocos/core"; import { AnimationClip, animation, BezierControlPoints, bezierByTime } from "../../cocos/core/animation"; -import { timeBezierToTangents } from "../../cocos/core/animation/legacy-clip-data"; -import { RealCurve, RealKeyframeValue, TangentWeightMode } from "../../cocos/core/curves/curve"; +import { ColorTrack, IValueProxyFactory, RealTrack, Track, TrackPath, VectorTrack } from "../../cocos/core/animation/animation"; +import { LegacyClipCurve, LegacyCommonTarget, LegacyEasingMethod, timeBezierToTangents } from "../../cocos/core/animation/legacy-clip-data"; +import { ComponentPath, HierarchyPath, ICustomTargetPath, TargetPath } from "../../cocos/core/animation/target-path"; +import { RealChannel } from "../../cocos/core/animation/tracks/track"; +import { UntypedTrack } from "../../cocos/core/animation/tracks/untyped-track"; +import { ExtrapMode, RealCurve, RealKeyframeValue, TangentWeightMode } from "../../cocos/core/curves/curve"; +class ValueProxyFactorFoo implements IValueProxyFactory { + forTarget(_target: any): animation.IValueProxy { + throw new Error("Method not implemented."); + } +} describe('Animation Clip Migration 3.x', () => { - test('Numeric curves', () => { - const clip = new AnimationClip(); - clip.keys = [ - [0.0, 0.2, 0.8], - ]; - clip.curves = [ - { - modifiers: ['p'], - data: { - keys: 0, - values: [3.14, 6.18, 8.9], - }, - }, - ] - clip.syncLegacyData(); - expect(clip.tracksCount).toBe(1); - const track = clip.getTrack(0) as animation.RealTrack; - expect(track).toBeInstanceOf(animation.RealTrack); - const curve = track.channel.curve; - expect(Array.from(curve.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(curve.values())).toStrictEqual( - createRealKeyframesWithoutTangent([3.14, 6.18, 8.9], RealInterpMode.LINEAR), - ); + test(`Zero curve clip`, () => { + const clip = createClipWithLegacyData({}); + expect(clip.tracksCount).toBe(0); }); - test('Vec2 curves', () => { - const clip = new AnimationClip(); - clip.keys = [[0.0, 0.2, 0.8]]; - clip.curves = [{ - modifiers: ['p'], - data: { - keys: 0, - values: [ - new math.Vec2(1.0, 4.0), - new math.Vec2(2.0, 5.0), - new math.Vec2(3.0, 6.0), - ], - }, - }]; - clip.syncLegacyData(); - expect(clip.tracksCount).toBe(1); - const track = clip.getTrack(0) as animation.VectorTrack; - expect(track).toBeInstanceOf(animation.VectorTrack); - expect(track.componentsCount).toBe(2); - const [{ curve: x }, { curve: y }] = track.channels(); - expect(Array.from(x.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(x.values())).toStrictEqual( - createRealKeyframesWithoutTangent([1.0, 2.0, 3.0], RealInterpMode.LINEAR), - ); - expect(Array.from(y.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(y.values())).toStrictEqual( - createRealKeyframesWithoutTangent([4.0, 5.0, 6.0], RealInterpMode.LINEAR), - ); - - }); + describe(`Modifiers & proxies`, () => { + test(`Empty path`, () => { + const clip = createClipWithLegacyData({ + times: [[0.1]], + curves: [{ modifiers: [], data: { keys: 0, values: [0.2] } }], + }); + expect(clip.tracksCount).toBe(1); + expect(Array.from(clip.tracks)[0].path.length).toBe(0); + }); - test('Vec3 curves', () => { - const clip = new AnimationClip(); - clip.keys = [[0.0, 0.2, 0.8]]; - clip.curves = [{ - modifiers: ['p'], - data: { - keys: 0, - values: [ - new math.Vec3(1.0, 4.0, 7.0), - new math.Vec3(2.0, 5.0, 8.0), - new math.Vec3(3.0, 6.0, 9.0), - ], - }, - }]; - clip.syncLegacyData(); - expect(clip.tracksCount).toBe(1); - const track = clip.getTrack(0) as animation.VectorTrack; - expect(track).toBeInstanceOf(animation.VectorTrack); - expect(track.componentsCount).toBe(3); - const [{ curve: x }, { curve: y }, { curve: z }] = track.channels(); - expect(Array.from(x.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(x.values())).toStrictEqual( - createRealKeyframesWithoutTangent([1.0, 2.0, 3.0], RealInterpMode.LINEAR), - ); - expect(Array.from(y.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(y.values())).toStrictEqual( - createRealKeyframesWithoutTangent([4.0, 5.0, 6.0], RealInterpMode.LINEAR), - ); - expect(Array.from(z.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(z.values())).toStrictEqual( - createRealKeyframesWithoutTangent([7.0, 8.0, 9.0], RealInterpMode.LINEAR), - ); - }); + const TRACK_PATH_PROTOTYPE = TrackPath.prototype; + + test.each([ + [`Property path`, { + value: 'p', + expected: 'p', + is: TRACK_PATH_PROTOTYPE.isPropertyAt, + parse: TRACK_PATH_PROTOTYPE.parsePropertyAt, + }], + [`Element path`, { + value: 2, + expected: 2, + is: TRACK_PATH_PROTOTYPE.isPropertyAt, + parse: TRACK_PATH_PROTOTYPE.parseElementAt, + }], + [`Hierarchy path`, { + value: new HierarchyPath('foo'), + expected: 'foo', + is: TRACK_PATH_PROTOTYPE.isPropertyAt, + parse: TRACK_PATH_PROTOTYPE.parseHierarchyAt, + }], + [`Component path`, { + value: new ComponentPath('bar'), + expected: 'bar', + is: TRACK_PATH_PROTOTYPE.isPropertyAt, + parse: TRACK_PATH_PROTOTYPE.parseComponentAt, + }], + ] as [ + title: string, options: { + value: TargetPath; + expected: unknown; + is: (index: number) => boolean; + parse: (index: number) => unknown; + } + ][])(`%s`, (_, { value, expected, is, parse }) => { + const clip = createClipWithLegacyData({ + times: [[0.1]], + curves: [{ modifiers: [value], data: { keys: 0, values: [0.2] } }], + }); + expect(clip.tracksCount).toBe(1); + const path = Array.from(clip.tracks)[0].path; + expect(path.length).toBe(1); + expect(is.call(path, 0)); + expect(parse.call(path, 0)).toBe(expected); + }); + + test(`Customized path`, () => { + // TODO + }); + + test(`Compound path`, () => { + const clip = createClipWithLegacyData({ + times: [[0.1]], + curves: [{ modifiers: [ + new HierarchyPath('foo'), + new ComponentPath('bar'), + 'baz', + 1, + ], data: { keys: 0, values: [0.2] } }], + }); + expect(clip.tracksCount).toBe(1); + const path = Array.from(clip.tracks)[0].path; + expect(path.length).toBe(4); + expect(path.isHierarchyAt(0)); + expect(path.parseHierarchyAt(0)).toBe('foo'); + expect(path.isComponentAt(1)); + expect(path.parseComponentAt(1)).toBe('bar'); + expect(path.isPropertyAt(2)); + expect(path.parsePropertyAt(2)).toBe('baz'); + expect(path.isElementAt(3)); + expect(path.parseElementAt(3)).toBe(1); + }); - test('Vec4 curves', () => { - const clip = new AnimationClip(); - clip.keys = [[0.0, 0.2, 0.8]]; - clip.curves = [{ - modifiers: ['p'], - data: { - keys: 0, - values: [ - new math.Vec4(1.0, 4.0, 7.0, 10.0), - new math.Vec4(2.0, 5.0, 8.0, 11.0), - new math.Vec4(3.0, 6.0, 9.0, 12.0), - ], - }, - }]; - clip.syncLegacyData(); - expect(clip.tracksCount).toBe(1); - const track = clip.getTrack(0) as animation.VectorTrack; - expect(track).toBeInstanceOf(animation.VectorTrack); - expect(track.componentsCount).toBe(4); - const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.channels(); - expect(Array.from(x.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(x.values())).toStrictEqual( - createRealKeyframesWithoutTangent([1.0, 2.0, 3.0], RealInterpMode.LINEAR), - ); - expect(Array.from(y.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(y.values())).toStrictEqual( - createRealKeyframesWithoutTangent([4.0, 5.0, 6.0], RealInterpMode.LINEAR), - ); - expect(Array.from(z.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(z.values())).toStrictEqual( - createRealKeyframesWithoutTangent([7.0, 8.0, 9.0], RealInterpMode.LINEAR), - ); - expect(Array.from(w.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(w.values())).toStrictEqual( - createRealKeyframesWithoutTangent([10.0, 11.0, 12.0], RealInterpMode.LINEAR), - ); + test(`Value proxy`, () => { + const valueProxy = new ValueProxyFactorFoo(); + const clip = createClipWithLegacyData({ + times: [[0.1]], + curves: [{ modifiers: [], valueAdapter: valueProxy, data: { keys: 0, values: [0.2] } }], + }); + expect(clip.tracksCount).toBe(1); + expect(Array.from(clip.tracks)[0].proxy).toBe(valueProxy); + }); }); - test('Color curves', () => { - const clip = new AnimationClip(); - clip.keys = [[0.0, 0.2, 0.8]]; - clip.curves = [{ - modifiers: ['p'], - data: { - keys: 0, - values: [ - new math.Color(10, 40, 70, 100), - new math.Color(20, 50, 80, 110), - new math.Color(30, 60, 90, 120), - ], - }, - }]; - clip.syncLegacyData(); - expect(clip.tracksCount).toBe(1); - const track = clip.getTrack(0) as animation.ColorTrack; - expect(track).toBeInstanceOf(animation.ColorTrack); - const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.channels(); - expect(Array.from(r.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(r.values())).toStrictEqual( - createRealKeyframesWithoutTangent([10, 20, 30], RealInterpMode.LINEAR), - ); - expect(Array.from(g.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(g.values())).toStrictEqual( - createRealKeyframesWithoutTangent([40, 50, 60], RealInterpMode.LINEAR), - ); - expect(Array.from(b.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(b.values())).toStrictEqual( - createRealKeyframesWithoutTangent([70, 80, 90], RealInterpMode.LINEAR), - ); - expect(Array.from(a.times())).toStrictEqual([0.0, 0.2, 0.8]); - expect(Array.from(a.values())).toStrictEqual( - createRealKeyframesWithoutTangent([100, 110, 120], RealInterpMode.LINEAR), - ); + describe(`Curves`, () => { + + test(`Empty curve`, () => { + const clip = createClipWithLegacyData({ + times: [[]], + curves: [{ modifiers: ['p'], data: { keys: 0, values: [] } }], + }); + // We did not convert a empty curve since it's meaningless. + expect(clip.tracksCount).toBe(0); + }); + + interface RealLikeTrackTestOptions { + valueType: NumberConstructor | { new (...args: number[]): TValueType }; + numberOfValueTypeComponents: number; + expectedTrackType: { new (...args: any[]): T }; + numberFactor?: number; + componentNames?: string[]; + } + + describe.each([ + [`Numeric curves`, { + valueType: Number, + numberOfValueTypeComponents: 1, + expectedTrackType: RealTrack, + }], + [`Vec2 curves`, { + valueType: math.Vec2, + numberOfValueTypeComponents: 2, + expectedTrackType: VectorTrack, + }], + [`Vec3 curves`, { + valueType: math.Vec3, + numberOfValueTypeComponents: 3, + expectedTrackType: VectorTrack, + }], + [`Vec4 curves`, { + valueType: math.Vec4, + numberOfValueTypeComponents: 4, + expectedTrackType: VectorTrack, + }], + [`Color curves`, { + valueType: math.Color, + numberOfValueTypeComponents: 4, + expectedTrackType: ColorTrack, + numberFactor: 10, + componentNames: ['r', 'g', 'b', 'a'], + }], + ] as Array<[ + title: string, + options: RealLikeTrackTestOptions, + ]>)(`%s`, (_, { + valueType, + numberOfValueTypeComponents, + expectedTrackType, + numberFactor = 0.1, + componentNames, + }) => { + if (!componentNames) { + componentNames = ['x', 'y', 'z', 'w']; + } + const times = [0.1, 0.2, 0.8]; + const nKeyframes = times.length; + const genValueAt = (keyframeIndex: number, componentIndex: number) => + numberFactor * (keyframeIndex * numberOfValueTypeComponents + componentIndex); + const values = Array.from( + { length: nKeyframes }, + (_, iKeyframe) => valueType === Number + ? genValueAt(iKeyframe, 0) + : new valueType(...Array.from( + { length: numberOfValueTypeComponents }, + (_, iComponent) => genValueAt(iKeyframe, iComponent)), + ), + ); + const clip = createClipWithLegacyData({ + times: [times], + curves: [{ + modifiers: ['p'], + data: { + keys: 0, + values: values, + interpolate: true, + }, + }, { + modifiers: ['p'], + data: { + keys: 0, + values: values, + interpolate: false, + }, + }], + }); + const INTERPOLATE_TRUE_CURVE_INDEX = 0; + const INTERPOLATE_FALSE_CURVE_INDEX = 1; + expect(clip.tracksCount).toBe(2); + test.each([ + [true, [INTERPOLATE_TRUE_CURVE_INDEX, RealInterpMode.LINEAR]], + [false, [INTERPOLATE_FALSE_CURVE_INDEX, RealInterpMode.CONSTANT]], + ])(`with .interpolate: %s`, (_interpolate, [trackIndex, interpMode]) => { + const track = clip.getTrack(trackIndex); + expect(track).toBeInstanceOf(expectedTrackType); + const channels = Array.from(track.channels()); + if (expectedTrackType === VectorTrack) { + expect((track as VectorTrack).componentsCount).toBe(numberOfValueTypeComponents); + } else { + expect(channels).toHaveLength(numberOfValueTypeComponents); + } + const getComponentNameOfChannel = (channelIndex: number) => { + if (channelIndex >= componentNames.length) { + throw new Error(`Unknown component name at channel ${channelIndex}`); + } else { + return componentNames[channelIndex]; + } + }; + for (let iChannel = 0; iChannel < numberOfValueTypeComponents; ++iChannel) { + const { curve } = channels[iChannel] as RealChannel; + expect(curve.preExtrap).toBe(ExtrapMode.CLAMP); + expect(curve.postExtrap).toBe(ExtrapMode.CLAMP); + // Each curve's times are obtained from original times + expect(Array.from(curve.times())).toStrictEqual(times); + const valuesAtChannel = valueType === Number + ? values as number[] + : values.map((value) => (value as Record)[getComponentNameOfChannel(iChannel)]); + expect(Array.from(curve.values())).toStrictEqual(valuesAtChannel.map((value) => new RealKeyframeValue({ + value, + interpMode, + }))); + } + }); + }); + + test('Size curves', () => { + const clip = createClipWithLegacyData({ + times: [[0.0, 0.2, 0.8]], + curves: [{ + modifiers: ['p'], + data: { + keys: 0, + values: [ + new math.Size(10.8, -1.3), + new math.Size(20, 50), + new math.Size(30, 60), + ], + }, + }], + }); + expect(clip.tracksCount).toBe(1); + const track = clip.getTrack(0) as animation.SizeTrack; + expect(track).toBeInstanceOf(animation.SizeTrack); + const [{ curve: width }, { curve: height }] = track.channels(); + expect(Array.from(width.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(width.values())).toStrictEqual( + createRealKeyframesWithoutTangent([10.8, 20, 30], RealInterpMode.LINEAR), + ); + expect(Array.from(height.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(height.values())).toStrictEqual( + createRealKeyframesWithoutTangent([-1.3, 50, 60], RealInterpMode.LINEAR), + ); + }); + + test('Sprite frame curves', () => { + const spriteFrames = [ + new SpriteFrame(), + new SpriteFrame(), + new SpriteFrame(), + ]; + const clip = createClipWithLegacyData({ + times: [[0.0, 0.2, 0.8]], + curves: [{ + modifiers: ['p'], + data: { + keys: 0, + values: spriteFrames, + }, + }], + }); + expect(clip.tracksCount).toBe(1); + const track = clip.getTrack(0) as animation.ObjectTrack; + expect(track).toBeInstanceOf(animation.ObjectTrack); + const { curve } = track.channel; + expect(Array.from(curve.times())).toStrictEqual([0.0, 0.2, 0.8]); + expect(Array.from(curve.values())).toStrictEqual(spriteFrames); + }); }); - test('Common target: Color', () => { + test(`Common targets are converted into internal concept: UntypedTracks`, () => { + const valueProxy = new ValueProxyFactorFoo(); + + const clip = createClipWithLegacyData({ + times: [[1.2]], + commonTargets: [{ + modifiers: ['p1'], + }, { + modifiers: ['p2'], + valueAdapter: valueProxy, + }], + curves: [{ + commonTarget: 0, + modifiers: ['x'], + data: { keys: 0, values: [0.1] }, + }, { + commonTarget: 0, + modifiers: ['y'], + data: { keys: 0, values: [0.2] }, + }, { + commonTarget: 1, + modifiers: ['z'], + data: { keys: 0, values: [0.3] }, + }], + }); + + expect(clip.tracksCount).toBe(2); + const tracks = Array.from(clip.tracks); + const [ track1, track2 ] = tracks as [UntypedTrack, UntypedTrack]; + + expect(track1).toBeInstanceOf(UntypedTrack); + expect(track1.path.length).toBe(1); + expect(track1.path.isPropertyAt(0)).toBe(true); + expect(track1.path.parsePropertyAt(0)).toBe('p1'); + expect(track1.channels()).toHaveLength(2); + expect(Array.from(track1.channels()[0].curve.keyframes())).toStrictEqual([[1.2, new RealKeyframeValue({ value: 0.1 })]]); + expect(Array.from(track1.channels()[1].curve.keyframes())).toStrictEqual([[1.2, new RealKeyframeValue({ value: 0.2 })]]); + expect(track2).toBeInstanceOf(UntypedTrack); + expect(track2.path.length).toBe(1); + expect(track2.path.isPropertyAt(0)).toBe(true); + expect(track2.path.parsePropertyAt(0)).toBe('p2'); + expect(track2.proxy).toBe(valueProxy); + expect(track2.channels()).toHaveLength(1); + expect(Array.from(track2.channels()[0].curve.keyframes())).toStrictEqual([[1.2, new RealKeyframeValue({ value: 0.3 })]]); }); - test('Common target: Color with components as floats', () => { - + describe(`Easing methods`, () => { + function createClipWithEasingMethodsAndConvert ( + times: number[], + values: number[], + easingMethod: undefined | LegacyEasingMethod, + easingMethods: undefined | LegacyEasingMethod[] | Record, + interpolate = true, + ) { + const clip = createClipWithLegacyData({ + times: [times], + curves: [{ + modifiers: [], + data: { + keys: 0, + values: values, + easingMethod, + easingMethods, + interpolate, + }, + }], + }); + expect(clip.tracksCount).toBe(1); + const track = Array.from(clip.tracks)[0] as RealTrack; + expect(track).toBeInstanceOf(RealTrack); + return track.channel.curve; + } + + test(`Easing methods: not specified`, () => { + const curve = createClipWithEasingMethodsAndConvert( + [0.1, 0.3, 0.5], + [1, 3, 5], + undefined, + undefined, + true, + ); + expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.5]); + expect(Array.from(curve.values())).toStrictEqual(createRealKeyframesWithoutTangent([1, 3, 5], RealInterpMode.LINEAR)); + }); + + describe(`Specified through ".easingMethod"`, () => { + test(`null(linear)`, () => { + const curve = createClipWithEasingMethodsAndConvert( + [0.1, 0.3, 0.8], + [1, 3, 5], + null, + undefined, + true, + ); + expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); + expect(Array.from(curve.values())).toStrictEqual(createRealKeyframesWithoutTangent([1, 3, 5], RealInterpMode.LINEAR)); + }); + + test(`Time bezier`, () => { + const curve = createClipWithEasingMethodsAndConvert( + [0.1, 0.3, 0.8], + [1, 3, 5], + [0.2, 0.3, 0.4, 0.5], + undefined, + true, + ); + expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); + expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.BOTH, + value: 1, + startTangent: 14.999999999999998, + startTangentWeight: 0.6013318551349163, + endTangent: 8.333333333333334, + endTangentWeight: 1.0071742649611337, + }), new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.BOTH, + value: 3, + startTangent: 5.999999999999998, + startTangentWeight: 0.6082762530298218, + endTangent: 3.3333333333333335, + endTangentWeight: 1.044030650891055, + }), new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, + value: 5, + })]); + }); + }); + + describe(`Specified through ".easingMethods"`, () => { + test.each([ + [`Full array`, [null, [0.2, 0.3, 0.4, 0.5], null] as (LegacyEasingMethod | null)[]], + [`Sparse array`, { + 1: [0.2, 0.3, 0.4, 0.5], + } as Record], + ])(`%s`, (_, easingMethods) => { + const curve = createClipWithEasingMethodsAndConvert( + [0.1, 0.3, 0.5], + [1, 3, 5], + undefined, + easingMethods, + true, + ); + expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.5]); + expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, + value: 1, + }), new RealKeyframeValue({ + interpMode: RealInterpMode.CUBIC, + tangentWeightMode: TangentWeightMode.BOTH, + value: 3, + startTangent: 14.999999999999996, + startTangentWeight: 0.6013318551349163, + endTangent: 8.333333333333332, + endTangentWeight: 1.0071742649611337, + }), new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, + value: 5, + })]); + }); + }); }); test('Time bezier to tangent', () => { @@ -236,6 +521,29 @@ describe('Animation Clip Migration 3.x', () => { }); }); +function createClipWithLegacyData ({ + times, + curves, + commonTargets, +}: { + times?: number[][]; + curves?: LegacyClipCurve[]; + commonTargets?: LegacyCommonTarget[]; +}) { + const clip = new AnimationClip(); + if (times) { + clip.keys = times; + } + if (curves) { + clip.curves = curves; + } + if (commonTargets) { + clip.commonTargets = commonTargets; + } + clip.syncLegacyData(); + return clip; +} + function createRealKeyframesWithoutTangent (values: number[], interpMode: RealInterpMode): RealKeyframeValue[] { return values.map((value) => { return new RealKeyframeValue({ From b756cd692e6f63ccd1e7152521ce667dd6cc6814 Mon Sep 17 00:00:00 2001 From: zheng han Date: Wed, 14 Jul 2021 14:00:59 +0800 Subject: [PATCH 13/35] fix split animation (#24) --- editor/inspector/assets/fbx/animation.js | 3 ++- editor/inspector/assets/fbx/preview.js | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/editor/inspector/assets/fbx/animation.js b/editor/inspector/assets/fbx/animation.js index 42c91d6a995..f4ed375bca2 100644 --- a/editor/inspector/assets/fbx/animation.js +++ b/editor/inspector/assets/fbx/animation.js @@ -695,7 +695,7 @@ exports.methods = { const splitInfo = animInfo.splits[this.splitClipIndex]; if (!animInfo) { - return; + return null; } const rawClipUUID = this.animationNameToUUIDMap.get(animInfo.name); @@ -719,6 +719,7 @@ exports.methods = { return { rawClipUUID, + rawClipIndex: this.rawClipIndex, clipUUID, duration, fps, diff --git a/editor/inspector/assets/fbx/preview.js b/editor/inspector/assets/fbx/preview.js index cf5316b7b00..914237df3a5 100644 --- a/editor/inspector/assets/fbx/preview.js +++ b/editor/inspector/assets/fbx/preview.js @@ -333,19 +333,19 @@ const Elements = { const timeline = this.$.animationTime; timeline.addEventListener('change', this.onAnimationTimeChange.bind(this)); timeline.addEventListener('transform', this.updateEventInfo.bind(this)); - } + }, }, currentTime: { ready() { const currentTime = this.$.currentTime; currentTime.addEventListener('confirm', this.onAnimationTimeChange.bind(this)); - } + }, }, timeCtrl: { ready() { this.$.timeCtrl.addEventListener('click', this.onTimeCtrlClick.bind(this)); - } - } + }, + }, }; exports.update = async function(assetList, metaList) { @@ -389,7 +389,7 @@ exports.ready = function() { this.onEditClipInfoChanged = async (clipInfo) => { if (clipInfo) { - await Editor.Message.request('scene', 'execute-model-preview-animation-operation', 'setEditClip', clipInfo.rawClipUUID); + await Editor.Message.request('scene', 'execute-model-preview-animation-operation', 'setEditClip', clipInfo.rawClipUUID, clipInfo.rawClipIndex); this.setCurEditClipInfo(clipInfo); } }; @@ -438,11 +438,11 @@ exports.close = function() { }; exports.methods = { - async apply () { + async apply() { // save animation event info await this.events.apply.call(this); }, - async refreshPreview () { + async refreshPreview() { const panel = this; // After await, the panel no longer exists @@ -497,7 +497,7 @@ exports.methods = { if (!name || !this.curEditClipInfo) { return; } - switch(name) { + switch (name) { case 'play': this.onPlayButtonClick(); break; @@ -537,14 +537,14 @@ exports.methods = { this.events.update.call(this, eventInfos); }, - async stopAnimation () { + async stopAnimation() { if (!this.curEditClipInfo) { return; } await Editor.Message.request('scene', 'execute-model-preview-animation-operation', 'stop'); }, - async onPlayButtonClick () { + async onPlayButtonClick() { if (!this.curEditClipInfo) { return; } @@ -564,7 +564,7 @@ exports.methods = { this.isPreviewDataDirty = true; }, - async onAnimationTimeChange (event) { + async onAnimationTimeChange(event) { event.stopPropagation(); if (!this.curEditClipInfo) { return; @@ -582,7 +582,7 @@ exports.methods = { await Editor.Message.request('scene', 'execute-model-preview-animation-operation', 'setCurEditTime', curTime); }, - onModelAnimationUpdate (time) { + onModelAnimationUpdate(time) { if (!this.curEditClipInfo) { return; } From 79fdb28871f35149c51105697f6261ffe1e3b49f Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Fri, 16 Jul 2021 10:05:19 +0800 Subject: [PATCH 14/35] CCONB animation clip; Remove integer track/channel/curve --- cocos/core/animation/animation.ts | 1 - cocos/core/animation/tracks/color-track.ts | 18 +++---- cocos/core/animation/tracks/integer-track.ts | 11 ---- cocos/core/animation/tracks/track.ts | 10 ++-- cocos/core/curves/curve.ts | 26 ++++++--- cocos/core/curves/index.ts | 4 -- cocos/core/curves/integer-curve.ts | 55 -------------------- cocos/core/curves/quat-curve.ts | 24 +++++++-- cocos/core/data/serialization-symbols.ts | 5 -- tests/curves/curve.test.ts | 1 - tests/curves/quat-curve.test.ts | 1 - 11 files changed, 53 insertions(+), 103 deletions(-) delete mode 100644 cocos/core/animation/tracks/integer-track.ts delete mode 100644 cocos/core/curves/integer-curve.ts delete mode 100644 cocos/core/data/serialization-symbols.ts diff --git a/cocos/core/animation/animation.ts b/cocos/core/animation/animation.ts index 56b7db1a453..59ffc9312c2 100644 --- a/cocos/core/animation/animation.ts +++ b/cocos/core/animation/animation.ts @@ -35,7 +35,6 @@ export { MorphWeightValueProxy, MorphWeightsValueProxy, MorphWeightsAllValueProx export * from './cubic-spline-value'; export { Track, TrackPath } from './tracks/track'; export { RealTrack } from './tracks/real-track'; -export { IntegerTrack } from './tracks/integer-track'; export { VectorTrack } from './tracks/vector-track'; export { QuaternionTrack } from './tracks/quat-track'; export { ColorTrack } from './tracks/color-track'; diff --git a/cocos/core/animation/tracks/color-track.ts b/cocos/core/animation/tracks/color-track.ts index a302aadd5b4..0e756891e05 100644 --- a/cocos/core/animation/tracks/color-track.ts +++ b/cocos/core/animation/tracks/color-track.ts @@ -1,8 +1,8 @@ import { ccclass, serializable } from 'cc.decorator'; -import { IntegerCurve, RealCurve } from '../../curves'; +import { RealCurve } from '../../curves'; import { Color } from '../../math'; import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; -import { Channel, IntegerChannel, RuntimeBinding, Track } from './track'; +import { Channel, RealChannel, RuntimeBinding, Track } from './track'; import { maskIfEmpty } from './utils'; const CHANNEL_NAMES: ReadonlyArray = ['Red', 'Green', 'Blue', 'Alpha']; @@ -13,7 +13,7 @@ export class ColorTrack extends Track { super(); this._channels = new Array(4) as ColorTrack['_channels']; for (let i = 0; i < this._channels.length; ++i) { - const channel = new Channel(new IntegerCurve()); + const channel = new Channel(new RealCurve()); channel.name = CHANNEL_NAMES[i]; this._channels[i] = channel; } @@ -33,15 +33,15 @@ export class ColorTrack extends Track { } @serializable - private _channels: [IntegerChannel, IntegerChannel, IntegerChannel, IntegerChannel]; + private _channels: [RealChannel, RealChannel, RealChannel, RealChannel]; } -export class ColorTrackEval { +export class ColorTrackEval { constructor ( - private _x: TCurve | undefined, - private _y: TCurve | undefined, - private _z: TCurve | undefined, - private _w: TCurve | undefined, + private _x: RealCurve | undefined, + private _y: RealCurve | undefined, + private _z: RealCurve | undefined, + private _w: RealCurve | undefined, ) { } diff --git a/cocos/core/animation/tracks/integer-track.ts b/cocos/core/animation/tracks/integer-track.ts deleted file mode 100644 index 18a6f3aeef2..00000000000 --- a/cocos/core/animation/tracks/integer-track.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ccclass } from 'cc.decorator'; -import { IntegerCurve } from '../../curves'; -import { CLASS_NAME_PREFIX_ANIM } from '../define'; -import { SingleChannelTrack } from './track'; - -@ccclass(`${CLASS_NAME_PREFIX_ANIM}IntegerTrack`) -export class IntegerTrack extends SingleChannelTrack { - protected createCurve () { - return new IntegerCurve(); - } -} diff --git a/cocos/core/animation/tracks/track.ts b/cocos/core/animation/tracks/track.ts index 9b11934a389..7c5cd698fc5 100644 --- a/cocos/core/animation/tracks/track.ts +++ b/cocos/core/animation/tracks/track.ts @@ -1,6 +1,6 @@ import { ccclass, serializable, uniquelyReferenced } from 'cc.decorator'; import type { Component } from '../../components'; -import type { IntegerCurve, ObjectCurve, QuaternionCurve, RealCurve } from '../../curves'; +import type { ObjectCurve, QuaternionCurve, RealCurve } from '../../curves'; import { assertIsTrue } from '../../data/utils/asserts'; import { error, warn } from '../../platform'; import { Node } from '../../scene-graph'; @@ -72,7 +72,7 @@ export interface TrackEval { evaluate(time: number, runtimeBinding: RuntimeBinding): unknown; } -export type Curve = RealCurve | IntegerCurve | QuaternionCurve | ObjectCurve; +export type Curve = RealCurve | QuaternionCurve | ObjectCurve; @ccclass(`${CLASS_NAME_PREFIX_ANIM}Channel`) export class Channel { @@ -80,7 +80,9 @@ export class Channel { this._curve = curve; } - // @serializable + /** + * Not used for now. + */ public name = ''; get curve () { @@ -93,8 +95,6 @@ export class Channel { export type RealChannel = Channel; -export type IntegerChannel = Channel; - export type QuaternionChannel = Channel; @ccclass(`${CLASS_NAME_PREFIX_ANIM}SingleChannelTrack`) diff --git a/cocos/core/curves/curve.ts b/cocos/core/curves/curve.ts index 31bd4413986..dec34dfcfc0 100644 --- a/cocos/core/curves/curve.ts +++ b/cocos/core/curves/curve.ts @@ -2,11 +2,12 @@ import { assertIsTrue } from '../data/utils/asserts'; import { approx, lerp, pingPong, repeat } from '../math'; import { KeyframeCurve } from './keyframe-curve'; import { ccclass, serializable, uniquelyReferenced } from '../data/decorators'; -import { deserializeSymbol, serializeSymbol } from '../data/serialization-symbols'; import { RealInterpMode, ExtrapMode, TangentWeightMode } from './real-curve-param'; import { binarySearchEpsilon } from '../algorithm/binary-search'; import { solveCubic } from './solve-cubic'; import { EditorExtendableMixin } from '../data/editor-extendable'; +import { deserializeTag, SerializationContext, SerializationInput, SerializationOutput, serializeTag } from '../data'; +import { DeserializationContext } from '../data/custom-serializable'; export { RealInterpMode, ExtrapMode, TangentWeightMode }; @@ -216,7 +217,12 @@ export class RealCurve extends EditorExtendableMixin approx(frame.value, firstVal, tolerance)); } - public [serializeSymbol] () { + public [serializeTag] (output: SerializationOutput, context: SerializationContext) { + if (!context.toCCON) { + output.writeThis(); + return; + } + const { _times: times, _values: keyframeValues, @@ -249,11 +255,19 @@ export class RealCurve extends EditorExtendableMixin) { - const dataView = new DataView(serialized.buffer, serialized.byteOffset, serialized.byteLength); + public [deserializeTag] (input: SerializationInput, context: DeserializationContext) { + if (!context.fromCCON) { + input.readThis(); + return; + } + + const bytes = input.readProperty('bytes') as Uint8Array; + + const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); let currentOffset = 0; // Overflow operations @@ -276,7 +290,7 @@ export class RealCurve extends EditorExtendableMixin) { - this.truncType = serialized[0]; - super[deserializeSymbol](new Uint8Array(serialized, 1)); - } -} diff --git a/cocos/core/curves/quat-curve.ts b/cocos/core/curves/quat-curve.ts index ab512505ccb..de6747975b0 100644 --- a/cocos/core/curves/quat-curve.ts +++ b/cocos/core/curves/quat-curve.ts @@ -4,7 +4,8 @@ import { KeyframeCurve } from './keyframe-curve'; import { ExtrapMode } from './curve'; import { binarySearchEpsilon } from '../algorithm/binary-search'; import { ccclass, serializable, uniquelyReferenced } from '../data/decorators'; -import { deserializeSymbol, serializeSymbol } from '../data/serialization-symbols'; +import { deserializeTag, SerializationContext, SerializationInput, SerializationOutput, serializeTag } from '../data'; +import { DeserializationContext } from '../data/custom-serializable'; @ccclass('cc.QuaternionKeyframeValue') @uniquelyReferenced @@ -176,7 +177,12 @@ export class QuaternionCurve extends KeyframeCurve { return super.addKeyFrame(time, keyframeValue); } - public [serializeSymbol] () { + public [serializeTag] (output: SerializationOutput, context: SerializationContext) { + if (!context.toCCON) { + output.writeThis(); + return; + } + const { _times: times, _values: keyframeValues, @@ -237,11 +243,19 @@ export class QuaternionCurve extends KeyframeCurve { if (!interpModeRepeated) { pInterpMode += INTERP_MODE_BYTES; } }); - return new Uint8Array(dataView.buffer); + const bytes = new Uint8Array(dataView.buffer); + output.writeProperty('bytes', bytes); } - public [deserializeSymbol] (serialized: ReturnType) { - const dataView = new DataView(serialized.buffer, serialized.byteOffset, serialized.byteLength); + public [deserializeTag] (input: SerializationInput, context: DeserializationContext) { + if (!context.fromCCON) { + input.readThis(); + return; + } + + const bytes = input.readProperty('bytes') as Uint8Array; + + const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); let P = 0; // Flags diff --git a/cocos/core/data/serialization-symbols.ts b/cocos/core/data/serialization-symbols.ts deleted file mode 100644 index ec28fea7324..00000000000 --- a/cocos/core/data/serialization-symbols.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { TEST } from 'internal:constants'; - -export const serializeSymbol: unique symbol = (TEST ? Symbol.for('serialize') : Symbol('[[deserialize]]')) as any; - -export const deserializeSymbol: unique symbol = (TEST ? Symbol.for('deserialize') : Symbol('[[deserialize]]')) as any; diff --git a/tests/curves/curve.test.ts b/tests/curves/curve.test.ts index 7a5f46c6818..94a5ee21dad 100644 --- a/tests/curves/curve.test.ts +++ b/tests/curves/curve.test.ts @@ -2,7 +2,6 @@ import { toRadian } from '../../cocos/core'; import { RealCurve, RealInterpMode } from '../../cocos/core/curves'; import { RealKeyframeValue } from '../../cocos/core/curves/curve'; import { ExtrapMode, TangentWeightMode } from '../../cocos/core/curves/real-curve-param'; -import { deserializeSymbol, serializeSymbol } from '../../cocos/core/data/serialization-symbols'; describe('Curve', () => { test('assign sorted', () => { diff --git a/tests/curves/quat-curve.test.ts b/tests/curves/quat-curve.test.ts index 249f2fe396c..26c6f9c5e41 100644 --- a/tests/curves/quat-curve.test.ts +++ b/tests/curves/quat-curve.test.ts @@ -1,6 +1,5 @@ import { Quat, QuaternionCurve, QuaternionInterpMode, QuaternionKeyframeValue } from '../../cocos/core'; -import { deserializeSymbol, serializeSymbol } from '../../cocos/core/data/serialization-symbols'; describe('Curve', () => { test('Evaluate an empty curve', () => { From 0ef781924d55756c9be9856762786e5b2d42d6b7 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Fri, 16 Jul 2021 18:00:58 +0800 Subject: [PATCH 15/35] Optimize key shared curves --- cocos/core/algorithm/binary-search.ts | 2 +- cocos/core/curves/keys-shared-curves.ts | 39 ++++++++++++++++--------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/cocos/core/algorithm/binary-search.ts b/cocos/core/algorithm/binary-search.ts index 7879cafe7ca..26a52540742 100644 --- a/cocos/core/algorithm/binary-search.ts +++ b/cocos/core/algorithm/binary-search.ts @@ -45,7 +45,7 @@ export function binarySearch (array: number[], value: number) { * otherwise, a negative number that is the bitwise complement of the index of the next element that is large than the searched value or, * if there is no larger element(include the case that the array is empty), the bitwise complement of array's length. */ -export function binarySearchEpsilon (array: readonly number[], value: number, EPSILON = 1e-6) { +export function binarySearchEpsilon (array: Readonly>, value: number, EPSILON = 1e-6) { let low = 0; let high = array.length - 1; let middle = high >>> 1; diff --git a/cocos/core/curves/keys-shared-curves.ts b/cocos/core/curves/keys-shared-curves.ts index 61ff32b549d..330a76a2cf8 100644 --- a/cocos/core/curves/keys-shared-curves.ts +++ b/cocos/core/curves/keys-shared-curves.ts @@ -8,6 +8,10 @@ import { RealInterpMode } from './real-curve-param'; const DEFAULT_EPSILON = 1e-5; +const DefaultFloatArray = Float32Array; + +type DefaultFloatArray = InstanceType; + /** * Considering most of model animations are baked and most of its curves share same times, * we do not have to do time searching for many times. @@ -23,14 +27,14 @@ class KeysSharedCurves { constructor (times?: number[]) { if (!times) { - this._times = []; + this._times = new DefaultFloatArray(); return; } const nKeyframes = times.length; this._keyframesCount = nKeyframes; - this._times = times; + this._times = DefaultFloatArray.from(times); if (nKeyframes > 1) { const EPSILON = 1e-6; @@ -47,7 +51,7 @@ class KeysSharedCurves { } if (mayBeOptimized) { this._optimized = true; - this._times = [this._times[0], this._times[1]]; + this._times = new DefaultFloatArray([this._times[0], this._times[1]]); } } } @@ -111,7 +115,7 @@ class KeysSharedCurves { } @serializable - private _times: number[]; + private _times: DefaultFloatArray; @serializable private _optimized = false; @@ -153,7 +157,7 @@ export class KeySharedRealCurves extends KeysSharedCurves { public addCurve (curve: RealCurve) { assertIsTrue(curve.keyFramesCount === this.keyframesCount); this._curves.push({ - values: Array.from(curve.values()).map(({ value }) => value), + values: DefaultFloatArray.from(Array.from(curve.values()).map(({ value }) => value)), }); } @@ -203,10 +207,13 @@ export class KeySharedRealCurves extends KeysSharedCurves { @serializable private _curves: { - values: number[]; + values: DefaultFloatArray; }[] = []; } +const cacheQuat1 = new Quat(); +const cacheQuat2 = new Quat(); + @ccclass('cc.KeySharedQuaternionCurves') export class KeySharedQuaternionCurves extends KeysSharedCurves { public static allowedForCurve (curve: QuaternionCurve) { @@ -229,8 +236,13 @@ export class KeySharedQuaternionCurves extends KeysSharedCurves { public addCurve (curve: QuaternionCurve) { assertIsTrue(curve.keyFramesCount === this.keyframesCount); + const values = new DefaultFloatArray(curve.keyFramesCount * 4); + const nKeyframes = curve.keyFramesCount; + for (let iKeyframe = 0; iKeyframe < nKeyframes; ++iKeyframe) { + Quat.toArray(values, curve.getKeyframeValue(iKeyframe).value, 4 * iKeyframe); + } this._curves.push({ - values: Array.from(curve.values()).map(({ value }) => Quat.clone(value)), + values, }); } @@ -250,7 +262,7 @@ export class KeySharedQuaternionCurves extends KeysSharedCurves { const firstTime = super.getFirstTime(); if (time <= firstTime) { for (let iCurve = 0; iCurve < nCurves; ++iCurve) { - Quat.copy(values[iCurve], this._curves[iCurve].values[0]); + Quat.fromArray(values[iCurve], this._curves[iCurve].values, 0); } return; } @@ -259,7 +271,7 @@ export class KeySharedQuaternionCurves extends KeysSharedCurves { if (time >= lastTime) { const iLastFrame = nKeyframes - 1; for (let iCurve = 0; iCurve < nCurves; ++iCurve) { - Quat.copy(values[iCurve], this._curves[iCurve].values[iLastFrame]); + Quat.fromArray(values[iCurve], this._curves[iCurve].values, iLastFrame * 4); } return; } @@ -268,18 +280,19 @@ export class KeySharedQuaternionCurves extends KeysSharedCurves { if (ratio !== 0.0) { for (let iCurve = 0; iCurve < nCurves; ++iCurve) { const { values: curveValues } = this._curves[iCurve]; - Quat.slerp(values[iCurve], curveValues[previous], curveValues[previous + 1], ratio); + const q1 = Quat.fromArray(cacheQuat1, curveValues, previous * 4); + const q2 = Quat.fromArray(cacheQuat2, curveValues, (previous + 1) * 4); + Quat.slerp(values[iCurve], q1, q2, ratio); } } else { for (let iCurve = 0; iCurve < nCurves; ++iCurve) { - const { values: curveValues } = this._curves[iCurve]; - Quat.copy(values[iCurve], curveValues[previous]); + Quat.fromArray(values[iCurve], this._curves[iCurve].values, previous * 4); } } } @serializable private _curves: { - values: Quat[]; + values: DefaultFloatArray; }[] = []; } From e8a3b03f3b1c280eb0b709d3a3097f228f236321 Mon Sep 17 00:00:00 2001 From: Leslie Leigh Date: Sat, 17 Jul 2021 12:43:37 +0800 Subject: [PATCH 16/35] Remove keyframecurve.empty --- cocos/core/animation/animation-clip.ts | 4 +++- cocos/core/animation/tracks/utils.ts | 2 +- cocos/core/curves/keyframe-curve.ts | 7 ------- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/cocos/core/animation/animation-clip.ts b/cocos/core/animation/animation-clip.ts index 2e1e837ecec..e669820a982 100644 --- a/cocos/core/animation/animation-clip.ts +++ b/cocos/core/animation/animation-clip.ts @@ -173,6 +173,7 @@ export class AnimationClip extends Asset { * When compression is enabled, * both space and performance may be optimized at production phase. * The price is that you can not flexible edit the animation at run time. + * @internal Do not use this in your code. */ get compressionEnabled () { return this._compressionEnabled; @@ -282,7 +283,8 @@ export class AnimationClip extends Asset { /** * Creates an event evaluator for this animation. * @param targetNode Target node used to fire events. - * @returns @internal Do not use this in your code. + * @returns + * @internal Do not use this in your code. */ public createEventEvaluator (targetNode: Node) { return new EventEvaluator( diff --git a/cocos/core/animation/tracks/utils.ts b/cocos/core/animation/tracks/utils.ts index dee6f2050ce..b2ca4e78b8a 100644 --- a/cocos/core/animation/tracks/utils.ts +++ b/cocos/core/animation/tracks/utils.ts @@ -1,7 +1,7 @@ import type { Curve } from './track'; export function maskIfEmpty (curve: T) { - return curve.empty ? undefined : curve; + return curve.keyFramesCount === 0 ? undefined : curve; } export interface Range { diff --git a/cocos/core/curves/keyframe-curve.ts b/cocos/core/curves/keyframe-curve.ts index af4e3a928c1..30b5c6a7953 100644 --- a/cocos/core/curves/keyframe-curve.ts +++ b/cocos/core/curves/keyframe-curve.ts @@ -18,13 +18,6 @@ export class KeyframeCurve implements CurveBase, Iterable Date: Sat, 17 Jul 2021 23:02:53 +0800 Subject: [PATCH 17/35] Exotic animation --- cocos/core/animation/animation-clip.ts | 115 +--- .../exotic-animation/exotic-animation.ts | 543 ++++++++++++++++++ editor/exports/exotic-animation.ts | 8 + 3 files changed, 578 insertions(+), 88 deletions(-) create mode 100644 cocos/core/animation/exotic-animation/exotic-animation.ts create mode 100644 editor/exports/exotic-animation.ts diff --git a/cocos/core/animation/animation-clip.ts b/cocos/core/animation/animation-clip.ts index e669820a982..b0c4a1e92e7 100644 --- a/cocos/core/animation/animation-clip.ts +++ b/cocos/core/animation/animation-clip.ts @@ -46,13 +46,11 @@ import * as legacy from './legacy-clip-data'; import { BAKE_SKELETON_CURVE_SYMBOL } from './internal-symbols'; import { Binder, RuntimeBinding, Track, TrackBinding, trackBindingTag, TrackEval, TrackPath, TrsTrackPath } from './tracks/track'; import { createEvalSymbol } from './define'; -import { VectorTrack } from './tracks/vector-track'; import { UntypedTrack, UntypedTrackRefine } from './tracks/untyped-track'; import { Range } from './tracks/utils'; import { ObjectTrack } from './tracks/object-track'; -import { CompressedData, CompressedDataEvaluator } from './compression/compressed-data'; -import { RealTrack } from './tracks/real-track'; -import { QuaternionTrack } from './tracks/quat-track'; +import type { ExoticAnimation } from './exotic-animation/exotic-animation'; +import './exotic-animation/exotic-animation'; export declare namespace AnimationClip { export interface IEvent { @@ -81,6 +79,8 @@ interface SkeletonAnimationBakeInfo { }>; } +export const exoticAnimationTag = Symbol('ExoticAnimation'); + /** * @zh 动画剪辑表示一段使用动画编辑器编辑的关键帧动画或是外部美术工具生产的骨骼动画。 * 它的数据主要被分为几层:轨道、关键帧和曲线。 @@ -168,21 +168,6 @@ export class AnimationClip extends Asset { return this._tracks; } - /** - * Gets or sets if compression is enabled for this animation. - * When compression is enabled, - * both space and performance may be optimized at production phase. - * The price is that you can not flexible edit the animation at run time. - * @internal Do not use this in your code. - */ - get compressionEnabled () { - return this._compressionEnabled; - } - - set compressionEnabled (value) { - this._compressionEnabled = value; - } - get hash () { // hashes should already be computed offline, but if not, make one if (this._hash) { return this._hash; } @@ -227,6 +212,14 @@ export class AnimationClip extends Asset { }; } + get [exoticAnimationTag] () { + return this._exoticAnimation; + } + + set [exoticAnimationTag] (value) { + this._exoticAnimation = value; + } + public onLoaded () { this.frameRate = this.sample; } @@ -319,48 +312,6 @@ export class AnimationClip extends Asset { return this._createEvalWithBinder(target, binder, context.rootMotion); } - /** - * Compresses this animation. - * @internal Do not use this in your code. - */ - public compress () { - const compressedData = new CompressedData(); - const compressedTracks = new Set(); - for (const track of this._tracks) { - let mayBeCompressed = false; - if (track instanceof RealTrack) { - mayBeCompressed = compressedData.compressRealTrack(track); - } else if (track instanceof VectorTrack) { - mayBeCompressed = compressedData.compressVectorTrack(track); - } else if (track instanceof QuaternionTrack) { - mayBeCompressed = compressedData.compressQuatTrack(track); - } - if (mayBeCompressed) { - compressedTracks.add(track); - } - } - this._compression = { - data: compressedData, - compressedTracks: Array.from(compressedTracks), - }; - } - - /** - * @internal Do not use this in your code. - */ - public purgeCompressedTracks () { - const { _compression: compression } = this; - if (!compression) { - return; - } - const compressedTracks = compression.compressedTracks; - if (!compressedTracks) { - return; - } - this._tracks = this._tracks.filter((track) => !compressedTracks.includes(track)); - compression.compressedTracks = undefined; - } - public destroy () { if (legacyCC.director.root.dataPoolManager) { (legacyCC.director.root.dataPoolManager as DataPoolManager).releaseAnimationClip(this); @@ -583,13 +534,7 @@ export class AnimationClip extends Asset { private _tracks: Track[] = []; @serializable - private _compressionEnabled = false; - - @serializable - private _compression: { - data: CompressedData; - compressedTracks: Track[] | undefined; - } | undefined = undefined; + private _exoticAnimation: ExoticAnimation | null = null; private _legacyData: legacy.AnimationClipLegacyData | undefined = undefined; @@ -623,12 +568,9 @@ export class AnimationClip extends Asset { } const trackEvalStatues: TrackEvalStatus[] = []; - let compressedDataEvaluator: CompressedDataEvaluator | undefined; + let exoticAnimationEvaluator: ExoticAnimationEvaluator | undefined; for (const track of this._tracks) { - if (this._compression?.compressedTracks?.includes(track)) { - continue; - } if (rootMotionTrackExcludes.includes(track)) { continue; } @@ -643,13 +585,13 @@ export class AnimationClip extends Asset { }); } - if (this._compression) { - compressedDataEvaluator = this._compression.data.createEval(binder); + if (this._exoticAnimation) { + exoticAnimationEvaluator = this._exoticAnimation.createEvaluator(binder); } const evaluation = new AnimationClipEvaluation( trackEvalStatues, - compressedDataEvaluator, + exoticAnimationEvaluator, rootMotionEvaluation, ); @@ -791,8 +733,8 @@ export class AnimationClip extends Asset { } } - if (this._compression) { - for (const joint of this._compression.data.collectAnimatedJoints()) { + if (this._exoticAnimation) { + for (const joint of this._exoticAnimation.collectAnimatedJoints()) { joints.add(joint); } } @@ -803,14 +745,11 @@ export class AnimationClip extends Asset { legacyCC.AnimationClip = AnimationClip; -// #region Data compression - interface TrackEvalStatus { binding: RuntimeBinding; trackEval: TrackEval; } -// #endregion interface AnimationClipEvalContext { /** * The output pose. @@ -832,19 +771,19 @@ interface AnimationClipEvalContext { interface RootMotionOptions { } +type ExoticAnimationEvaluator = ReturnType; + class AnimationClipEvaluation { /** * @internal - * @param trackEvalStatuses - * @param compressedDataEvaluator */ constructor ( trackEvalStatuses: TrackEvalStatus[], - compressedDataEvaluator: CompressedDataEvaluator | undefined, + exoticAnimationEvaluator: ExoticAnimationEvaluator | undefined, rootMotionEvaluation: RootMotionEvaluation | undefined, ) { this._trackEvalStatues = trackEvalStatuses; - this._compressedDataEvaluator = compressedDataEvaluator; + this._exoticAnimationEvaluator = exoticAnimationEvaluator; this._rootMotionEvaluation = rootMotionEvaluation; } @@ -855,7 +794,7 @@ class AnimationClipEvaluation { public evaluate (time: number) { const { _trackEvalStatues: trackEvalStatuses, - _compressedDataEvaluator: compressedDataEvaluator, + _exoticAnimationEvaluator: exoticAnimationEvaluator, } = this; for (const trackEvalStatus of trackEvalStatuses) { @@ -863,8 +802,8 @@ class AnimationClipEvaluation { trackEvalStatus.binding.setValue(value); } - if (compressedDataEvaluator) { - compressedDataEvaluator.evaluate(time); + if (exoticAnimationEvaluator) { + exoticAnimationEvaluator.evaluate(time); } } @@ -880,7 +819,7 @@ class AnimationClipEvaluation { } } - private _compressedDataEvaluator: CompressedDataEvaluator | undefined; + private _exoticAnimationEvaluator: ExoticAnimationEvaluator | undefined; private _trackEvalStatues:TrackEvalStatus[] = []; private _rootMotionEvaluation: RootMotionEvaluation | undefined = undefined; } diff --git a/cocos/core/animation/exotic-animation/exotic-animation.ts b/cocos/core/animation/exotic-animation/exotic-animation.ts new file mode 100644 index 00000000000..ea13eedcf27 --- /dev/null +++ b/cocos/core/animation/exotic-animation/exotic-animation.ts @@ -0,0 +1,543 @@ +import { EDITOR } from 'internal:constants'; +import { binarySearchEpsilon } from '../../algorithm/binary-search'; +import { ccclass, serializable } from '../../data/decorators'; +import { assertIsTrue } from '../../data/utils/asserts'; +import { Quat, Vec3 } from '../../math'; +import { error } from '../../platform/debug'; +import { CLASS_NAME_PREFIX_ANIM } from '../define'; +import { Binder, RuntimeBinding, TrackBinding, TrackPath } from '../tracks/track'; + +/** + * Animation that: + * - does not exposed by users; + * - does not compatible with regular animation; + * - non-editable; + * - currently only generated imported from model file. + */ +@ccclass(`${CLASS_NAME_PREFIX_ANIM}ExoticAnimation`) +export class ExoticAnimation { + public createEvaluator (binder: Binder) { + return new ExoticTrsAnimationEvaluator(this._nodeAnimations, binder); + } + + public addNodeAnimation (path: string) { + const nodeAnimation = new ExoticNodeAnimation(path); + this._nodeAnimations.push(nodeAnimation); + return nodeAnimation; + } + + public collectAnimatedJoints () { + return Array.from(new Set(this._nodeAnimations.map(({ path }) => path))); + } + + public split (from: number, to: number) { + if (!EDITOR) { + // TODO: better handling + error(`split() only valid in Editor.`); + return this; + } + const newAnimation = new ExoticAnimation(); + newAnimation._nodeAnimations = this._nodeAnimations.map((nodeAnimation) => nodeAnimation.split(from, to)); + return newAnimation; + } + + @serializable + private _nodeAnimations: ExoticNodeAnimation[] = []; +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}ExoticNodeAnimation`) +class ExoticNodeAnimation { + constructor (path: string) { + this._path = path; + } + + public createPosition (times: FloatArray, values: FloatArray) { + this._position = new ExoticVec3Track(times, values); + } + + public createRotation (times: FloatArray, values: FloatArray) { + this._rotation = new ExoticQuatTrack(times, values); + } + + public createScale (times: FloatArray, values: FloatArray) { + this._scale = new ExoticVec3Track(times, values); + } + + public createEvaluator (binder: Binder) { + return new ExoticNodeAnimationEvaluator( + this._path, + this._position, + this._rotation, + this._scale, + binder, + ); + } + + public split (from: number, to: number) { + if (!EDITOR) { + // TODO: better handling + error(`split() only valid in Editor.`); + return this; + } + const newAnimation = new ExoticNodeAnimation(this._path); + const { + _position: position, + _rotation: rotation, + _scale: scale, + } = this; + if (position) { + newAnimation._position = position.split(from, to); + } + if (rotation) { + newAnimation._rotation = rotation.split(from, to); + } + if (scale) { + newAnimation._scale = scale.split(from, to); + } + return newAnimation; + } + + get path () { + return this._path; + } + + @serializable + private _path = ''; + + @serializable + private _position: ExoticVec3Track | null = null; + + @serializable + private _rotation: ExoticQuatTrack | null = null; + + @serializable + private _scale: ExoticVec3Track | null = null; +} + +interface ExoticTrackValues { + get (index: number, resultValue: TValue): void; + + lerp(prevIndex: number, + nextIndex: number, + ratio: number, + prevValue: TValue, + nextValue: TValue, + resultValue: TValue): void; +} + +type MayBeQuantized = FloatArray | QuantizedFloatArray; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}ExoticVec3TrackValues`) +class ExoticVec3TrackValues implements ExoticTrackValues { + constructor (values: FloatArray) { + this._values = values; + this._isQuantized = false; + } + + public quantize (type: QuantizationType) { + assertIsTrue(!this._isQuantized); + this._values = quantize(this._values as FloatArray, type); + this._isQuantized = true; + } + + public get (index: number, resultValue: Vec3): void { + const { + _values: values, + _isQuantized: isQuantized, + } = this; + if (isQuantized) { + loadVec3FromQuantized(values as QuantizedFloatArray, index, resultValue); + } else { + Vec3.fromArray(resultValue, values as FloatArray, index * 3); + } + } + + public lerp ( + prevIndex: number, + nextIndex: number, + ratio: number, + prevValue: Vec3, + nextValue: Vec3, + resultValue: Vec3, + ): void { + const { + _values: values, + _isQuantized: isQuantized, + } = this; + if (isQuantized) { + loadVec3FromQuantized(values as QuantizedFloatArray, prevIndex, prevValue); + loadVec3FromQuantized(values as QuantizedFloatArray, nextIndex, nextValue); + } else { + Vec3.fromArray(prevValue, values as FloatArray, prevIndex * 3); + Vec3.fromArray(nextValue, values as FloatArray, nextIndex * 3); + } + Vec3.lerp(resultValue, prevValue, nextValue, ratio); + } + + @serializable + private _values: MayBeQuantized; + + @serializable + private _isQuantized: boolean; +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}ExoticVec3Track`) +class ExoticVec3Track { + constructor (times: FloatArray, values: FloatArray) { + this._times = times; + this._values = new ExoticVec3TrackValues(values); + } + + public createEvaluator () { + return new ExoticTrackEvaluator( + this._times, + this._values, + Vec3, + ); + } + + public split (from: number, to: number) { + if (!EDITOR) { + // TODO: better handling + error(`split() only valid in Editor.`); + return this; + } + return this; + } + + @serializable + private _times: FloatArray; + + @serializable + private _values: ExoticVec3TrackValues; +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}ExoticQuatTrackValues`) +class ExoticQuatTrackValues implements ExoticTrackValues { + constructor (values: FloatArray) { + this._values = values; + this._isQuantized = false; + } + + public quantize (type: QuantizationType) { + assertIsTrue(!this._isQuantized); + this._values = quantize(this._values as FloatArray, type); + this._isQuantized = true; + } + + public get (index: number, resultValue: Quat): void { + const { + _values: values, + _isQuantized: isQuantized, + } = this; + if (isQuantized) { + loadQuatFromQuantized(values as QuantizedFloatArray, index, resultValue); + } else { + Quat.fromArray(resultValue, values as FloatArray, index * 4); + } + } + + public lerp ( + prevIndex: number, + nextIndex: number, + ratio: number, + prevValue: Quat, + nextValue: Quat, + resultValue: Quat, + ): void { + const { + _values: values, + _isQuantized: isQuantized, + } = this; + if (isQuantized) { + loadQuatFromQuantized(values as QuantizedFloatArray, prevIndex, prevValue); + loadQuatFromQuantized(values as QuantizedFloatArray, nextIndex, nextValue); + } else { + Quat.fromArray(prevValue, values as FloatArray, prevIndex * 4); + Quat.fromArray(nextValue, values as FloatArray, nextIndex * 4); + } + Quat.slerp(resultValue, prevValue, nextValue, ratio); + } + + @serializable + private _values: FloatArray | QuantizedFloatArray; + + @serializable + private _isQuantized: boolean; +} + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}ExoticQuatTrack`) +class ExoticQuatTrack { + constructor (times: FloatArray, values: FloatArray) { + this._times = times; + this._values = new ExoticQuatTrackValues(values); + } + + public createEvaluator () { + return new ExoticTrackEvaluator( + this._times, + this._values, + Quat, + ); + } + + public split (from: number, to: number) { + if (!EDITOR) { + // TODO: better handling + error(`split() only valid in Editor.`); + return this; + } + return this; + } + + @serializable + private _times: FloatArray; + + @serializable + private _values: ExoticQuatTrackValues; +} + +class ExoticTrsAnimationEvaluator { + constructor (nodeAnimations: ExoticNodeAnimation[], binder: Binder) { + this._nodeEvaluations = nodeAnimations.map((nodeAnimation) => nodeAnimation.createEvaluator(binder)); + } + + public evaluate (time: number) { + this._nodeEvaluations.forEach((nodeEvaluator) => { + nodeEvaluator.evaluate(time); + }); + } + + private _nodeEvaluations: ExoticNodeAnimationEvaluator[]; +} + +class ExoticNodeAnimationEvaluator { + constructor ( + path: string, + position: ExoticVec3Track | null, + rotation: ExoticQuatTrack | null, + scale: ExoticVec3Track | null, + binder: Binder, + ) { + if (position) { + this._position = createExoticTrackEvaluationRecord(position, path, 'position', binder); + } + if (rotation) { + this._rotation = createExoticTrackEvaluationRecord(rotation, path, 'rotation', binder); + } + if (scale) { + this._scale = createExoticTrackEvaluationRecord(scale, path, 'scale', binder); + } + } + + public evaluate (time: number) { + if (this._position) { + const value = this._position.evaluator.evaluate(time); + this._position.runtimeBinding.setValue(value); + } + if (this._rotation) { + const value = this._rotation.evaluator.evaluate(time); + this._rotation.runtimeBinding.setValue(value); + } + if (this._scale) { + const value = this._scale.evaluator.evaluate(time); + this._scale.runtimeBinding.setValue(value); + } + } + + private _position: ExoticTrackEvaluationRecord | null = null; + private _rotation: ExoticTrackEvaluationRecord | null = null; + private _scale: ExoticTrackEvaluationRecord | null = null; +} + +class ExoticTrackEvaluator { + constructor (times: FloatArray, values: ExoticTrackValues, ValueConstructor: new () => TValue) { + this._times = times; + this._values = values; + this._prevValue = new ValueConstructor(); + this._nextValue = new ValueConstructor(); + this._resultValue = new ValueConstructor(); + } + + public evaluate (time: number) { + const { + _times: times, + _values: values, + _resultValue: resultValue, + } = this; + + const nFrames = times.length; + + if (nFrames === 0) { + return resultValue; + } + + const inputSampleResult = sampleInput(times, time, this._inputSampleResultCache); + if (inputSampleResult.just) { + values.get(inputSampleResult.index, resultValue); + } else { + values.lerp( + inputSampleResult.index, + inputSampleResult.nextIndex, + inputSampleResult.ratio, + this._prevValue, + this._nextValue, + resultValue, + ); + } + + return resultValue; + } + + private _times: FloatArray; + private _inputSampleResultCache: InputSampleResult = { + just: false, + index: -1, + nextIndex: -1, + ratio: 0.0, + }; + private _values: ExoticTrackValues; + private _prevValue: TValue; + private _nextValue: TValue; + private _resultValue: TValue; +} + +interface ExoticTrackEvaluationRecord { + runtimeBinding: RuntimeBinding; + evaluator: ExoticTrackEvaluator; +} + +interface InputSampleResult { + just: boolean; + index: number; + nextIndex: number; + ratio: number; +} + +function sampleInput (values: FloatArray, time: number, result: InputSampleResult) { + const nFrames = values.length; + assertIsTrue(nFrames !== 0); + + const firstTime = values[0]; + const lastTime = values[nFrames - 1]; + if (time < firstTime) { + result.just = true; + result.index = 0; + } else if (time > lastTime) { + result.just = true; + result.index = nFrames - 1; + } else { + const index = binarySearchEpsilon(values, time); + if (index >= 0) { + result.just = true; + result.index = index; + } else { + const nextIndex = ~index; + assertIsTrue(nextIndex !== 0 && nextIndex !== nFrames && nFrames > 1); + const prevIndex = nextIndex - 1; + const prevTime = values[prevIndex]; + const nextTime = values[nextIndex]; + const ratio = (time - values[prevIndex]) / (nextTime - prevTime); + result.just = false; + result.index = prevIndex; + result.nextIndex = nextIndex; + result.ratio = ratio; + } + } + + return result; +} + +type UintArray = Uint8Array | Uint16Array | Uint32Array; + +type FloatArray = Float32Array | Float64Array; + +type UintArrayConstructor = Uint8ArrayConstructor | Uint16ArrayConstructor | Uint32ArrayConstructor; + +type IntArrayConstructor = Int8ArrayConstructor | Int16ArrayConstructor | Int32ArrayConstructor; + +type QuantizationType = 'uint8' | 'uint16'; + +type QuantizationArrayConstructor = Uint8ArrayConstructor | Uint16ArrayConstructor; + +const QUANTIZATION_TYPE_TO_ARRAY_VIEW_CONSTRUCTOR_MAP: Record = { + uint8: Uint8Array, + uint16: Uint16Array, +}; + +@ccclass(`${CLASS_NAME_PREFIX_ANIM}QuantizedFloatArray`) +class QuantizedFloatArray { + @serializable + public min!: number; + + @serializable + public extent!: number; + + @serializable + public values!: UintArray; + + constructor (values: UintArray, extent: number, min = 0.0) { + this.values = values; + this.extent = extent; + this.min = min; + } +} + +function quantize (values: FloatArray, type: QuantizationType) { + const TypedArrayViewConstructor = QUANTIZATION_TYPE_TO_ARRAY_VIEW_CONSTRUCTOR_MAP[type]; + const MAX = 1 << TypedArrayViewConstructor.BYTES_PER_ELEMENT; + let min = Number.POSITIVE_INFINITY; + let max = Number.NEGATIVE_INFINITY; + values.forEach((value) => { + min = Math.min(value, min); + max = Math.max(value, max); + }); + const extent = max - min; + // Should consider `extent === 0.0`. + const normalized = TypedArrayViewConstructor.from(values, (value) => (value - min) / extent * MAX); + return new QuantizedFloatArray(normalized, extent, min); +} + +function indexQuantized (quantized: QuantizedFloatArray, index: number) { + const quantizedValue = quantized.values[index]; + const MAX_VALUE = 1 << quantized.values.BYTES_PER_ELEMENT; + return quantizedValue / MAX_VALUE * quantized.extent + quantized.min; +} + +function createExoticTrackEvaluationRecord ( + track: T, + path: string, + property: 'position' | 'scale' | 'rotation', + binder: Binder, +) { + const trackBinding = new TrackBinding(); + trackBinding.path = new TrackPath().hierarchy(path).property(property); + const runtimeBinding = binder(trackBinding); + if (!runtimeBinding) { + return null; + } + const evaluator = track.createEvaluator(); + return { + runtimeBinding, + evaluator, + } as (T extends ExoticVec3Track ? ExoticTrackEvaluationRecord : ExoticTrackEvaluationRecord); +} + +function loadVec3FromQuantized (values: QuantizedFloatArray, index: number, out: Vec3) { + Vec3.set( + out, + indexQuantized(values, 3 * index + 0), + indexQuantized(values, 3 * index + 1), + indexQuantized(values, 3 * index + 2), + ); +} + +function loadQuatFromQuantized (values: QuantizedFloatArray, index: number, out: Quat) { + Quat.set( + out, + indexQuantized(values, 4 * index + 0), + indexQuantized(values, 4 * index + 1), + indexQuantized(values, 4 * index + 2), + indexQuantized(values, 4 * index + 3), + ); +} diff --git a/editor/exports/exotic-animation.ts b/editor/exports/exotic-animation.ts new file mode 100644 index 00000000000..e14efecc50c --- /dev/null +++ b/editor/exports/exotic-animation.ts @@ -0,0 +1,8 @@ + +export { + exoticAnimationTag, +} from '../../cocos/core/animation/animation-clip'; + +export { + ExoticAnimation, +} from '../../cocos/core/animation/exotic-animation/exotic-animation'; From b53f4ddb5f0c021c4c405b6289985fbbcce2419f Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Mon, 19 Jul 2021 10:39:14 +0800 Subject: [PATCH 18/35] Fix unit tests --- tests/core/components/mesh-renderer.test.ts | 46 +++++++++---------- tests/curves/curve.test.ts | 14 ++---- tests/curves/quat-curve.test.ts | 14 ++---- .../curves/serialize-and-deserialize-curve.ts | 39 ++++++++++++++++ 4 files changed, 70 insertions(+), 43 deletions(-) create mode 100644 tests/curves/serialize-and-deserialize-curve.ts diff --git a/tests/core/components/mesh-renderer.test.ts b/tests/core/components/mesh-renderer.test.ts index f4e8e2b7519..b292ee85f2f 100644 --- a/tests/core/components/mesh-renderer.test.ts +++ b/tests/core/components/mesh-renderer.test.ts @@ -117,27 +117,27 @@ describe('Mesh Renderer', () => { // } // }); - function createMeshRenderer (): MeshRenderer { - const node = new Node(); - const meshRenderer = node.addComponent(MeshRenderer); - // @ts-expect-error Error - return meshRenderer; - } - - function createMeshWithShapeCounts (shapesCounts: number[]): Mesh { - const mesh = new Mesh(); - mesh.struct.morph = { - subMeshMorphs: shapesCounts.map((shapeCount) => { - return { - targets: Array.from({ length: shapeCount }, (_, index) => { - return { - displacements: [], - }; - }), - attributes: [], - }; - }), - }; - return mesh; - } + // function createMeshRenderer (): MeshRenderer { + // const node = new Node(); + // const meshRenderer = node.addComponent(MeshRenderer); + // // @ts-expect-error Error + // return meshRenderer; + // } + + // function createMeshWithShapeCounts (shapesCounts: number[]): Mesh { + // const mesh = new Mesh(); + // mesh.struct.morph = { + // subMeshMorphs: shapesCounts.map((shapeCount) => { + // return { + // targets: Array.from({ length: shapeCount }, (_, index) => { + // return { + // displacements: [], + // }; + // }), + // attributes: [], + // }; + // }), + // }; + // return mesh; + // } }); \ No newline at end of file diff --git a/tests/curves/curve.test.ts b/tests/curves/curve.test.ts index 94a5ee21dad..b16749fac78 100644 --- a/tests/curves/curve.test.ts +++ b/tests/curves/curve.test.ts @@ -2,6 +2,7 @@ import { toRadian } from '../../cocos/core'; import { RealCurve, RealInterpMode } from '../../cocos/core/curves'; import { RealKeyframeValue } from '../../cocos/core/curves/curve'; import { ExtrapMode, TangentWeightMode } from '../../cocos/core/curves/real-curve-param'; +import { serializeAndDeserialize } from './serialize-and-deserialize-curve'; describe('Curve', () => { test('assign sorted', () => { @@ -52,7 +53,7 @@ describe('Curve', () => { tangentWeightMode: TangentWeightMode.BOTH, }), ]); - compareCurves(serializeAndDeserialize(curve), curve); + compareCurves(serializeAndDeserialize(curve, RealCurve), curve); }); test('Optimized for linear curve', () => { @@ -62,7 +63,7 @@ describe('Curve', () => { realKeyframeWithoutTangent(0.5), realKeyframeWithoutTangent(0.6), ]); - compareCurves(serializeAndDeserialize(curve), curve); + compareCurves(serializeAndDeserialize(curve, RealCurve), curve); }); test('Optimized for constant curve', () => { @@ -72,7 +73,7 @@ describe('Curve', () => { realKeyframeWithoutTangent(0.5, RealInterpMode.CONSTANT), realKeyframeWithoutTangent(0.6, RealInterpMode.CONSTANT), ]); - compareCurves(serializeAndDeserialize(curve), curve); + compareCurves(serializeAndDeserialize(curve, RealCurve), curve); }); }); @@ -251,13 +252,6 @@ function realKeyframeWithoutTangent (value: number, interpMode: RealInterpMode = }); } -function serializeAndDeserialize (curve: RealCurve) { - const serialized = curve[serializeSymbol](); - const newCurve = new RealCurve(); - newCurve[deserializeSymbol](serialized); - return newCurve; -} - function compareCurves (left: RealCurve, right: RealCurve, numDigits = 2) { expect(left.keyFramesCount).toBe(right.keyFramesCount); for (let iKeyframe = 0; iKeyframe < left.keyFramesCount; ++iKeyframe) { diff --git a/tests/curves/quat-curve.test.ts b/tests/curves/quat-curve.test.ts index 26c6f9c5e41..2c8ce45a0b9 100644 --- a/tests/curves/quat-curve.test.ts +++ b/tests/curves/quat-curve.test.ts @@ -1,5 +1,6 @@ import { Quat, QuaternionCurve, QuaternionInterpMode, QuaternionKeyframeValue } from '../../cocos/core'; +import { serializeAndDeserialize } from './serialize-and-deserialize-curve'; describe('Curve', () => { test('Evaluate an empty curve', () => { @@ -15,7 +16,7 @@ describe('Curve', () => { new QuaternionKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpMode: QuaternionInterpMode.SLERP }), new QuaternionKeyframeValue({ value: { x: 0.9, y: 0.1, z: 0.11, w: 0.12 }, interpMode: QuaternionInterpMode.CONSTANT }), ]); - compareCurves(serializeAndDeserialize(curve), curve); + compareCurves(serializeAndDeserialize(curve, QuaternionCurve), curve); }); test('Optimized for linear curve', () => { @@ -24,7 +25,7 @@ describe('Curve', () => { new QuaternionKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpMode: QuaternionInterpMode.SLERP }), new QuaternionKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpMode: QuaternionInterpMode.SLERP }), ]); - compareCurves(serializeAndDeserialize(curve), curve); + compareCurves(serializeAndDeserialize(curve, QuaternionCurve), curve); }); test('Optimized for constant curve', () => { @@ -33,7 +34,7 @@ describe('Curve', () => { new QuaternionKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpMode: QuaternionInterpMode.CONSTANT }), new QuaternionKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpMode: QuaternionInterpMode.CONSTANT }), ]); - compareCurves(serializeAndDeserialize(curve), curve); + compareCurves(serializeAndDeserialize(curve, QuaternionCurve), curve); }); }); @@ -44,13 +45,6 @@ describe('Curve', () => { }); }); -function serializeAndDeserialize (curve: QuaternionCurve) { - const serialized = curve[serializeSymbol](); - const newCurve = new QuaternionCurve(); - newCurve[deserializeSymbol](serialized); - return newCurve; -} - function compareCurves (left: QuaternionCurve, right: QuaternionCurve, numDigits = 2) { expect(left.keyFramesCount).toBe(right.keyFramesCount); for (let iKeyframe = 0; iKeyframe < left.keyFramesCount; ++iKeyframe) { diff --git a/tests/curves/serialize-and-deserialize-curve.ts b/tests/curves/serialize-and-deserialize-curve.ts new file mode 100644 index 00000000000..9684a01b85a --- /dev/null +++ b/tests/curves/serialize-and-deserialize-curve.ts @@ -0,0 +1,39 @@ + +import { deserializeTag, SerializationInput, SerializationOutput, serializeTag } from '../../cocos/core'; +import type { RealCurve } from '../../cocos/core/curves/curve'; +import type { QuaternionCurve } from '../../cocos/core/curves/quat-curve'; + +export function serializeAndDeserialize (curve: T, CurveConstructor: new () => T) { + class CurveOutput implements SerializationOutput, SerializationInput { + public readProperty(name: string): unknown { + return this._properties[name]; + } + + public writeProperty (name: string, value: unknown) { + this._properties[name] = value; + } + + public readThis(): void { + throw new Error('Method not implemented.'); + } + + public readSuper(): void { + throw new Error('Method not implemented.'); + } + + public writeThis(): void { + throw new Error('Method not implemented.'); + } + + public writeSuper(): void { + throw new Error('Method not implemented.'); + } + + private _properties: Record = {}; + } + const curveOutput = new CurveOutput(); + curve[serializeTag](curveOutput, { root: curve, toCCON: true, customArguments: {} }); + const newCurve = new CurveConstructor(); + newCurve[deserializeTag](curveOutput, { fromCCON: true }); + return newCurve; +} From 29e268e99211ccb4301cd50ae7b15ae204f7787b Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Mon, 19 Jul 2021 12:18:15 +0800 Subject: [PATCH 19/35] start/end -> left/right --- cocos/core/animation/legacy-clip-data.ts | 18 +-- cocos/core/curves/curve.ts | 144 +++++++++--------- cocos/core/curves/quat-curve.ts | 4 +- cocos/core/curves/real-curve-param.ts | 6 +- cocos/core/geometry/curve.ts | 28 ++-- .../animaion-clip-migration-3.x.test.ts | 38 ++--- tests/core/geometry/geometry-curve.test.ts | 32 ++-- tests/curves/curve.test.ts | 56 +++---- tests/curves/key-shared-curves.test.ts | 8 +- 9 files changed, 167 insertions(+), 167 deletions(-) diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts index 4251405b047..8a657963a07 100644 --- a/cocos/core/animation/legacy-clip-data.ts +++ b/cocos/core/animation/legacy-clip-data.ts @@ -355,8 +355,8 @@ export class AnimationClipLegacyData { const interpMethod = interpolate ? RealInterpMode.CUBIC : RealInterpMode.CONSTANT; track.channel.curve.assignSorted(times, (legacyValues as CubicSplineNumberValue[]).map((value) => new RealKeyframeValue({ value: value.dataPoint, - startTangent: value.inTangent, - endTangent: value.outTangent, + leftTangent: value.inTangent, + rightTangent: value.outTangent, interpMode: interpMethod, }))); newTracks.push(track); @@ -375,10 +375,10 @@ export class AnimationClipLegacyData { track.componentsCount = components; const [x, y, z, w] = track.channels(); const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - const valueToFrame = (value: number, startTangent: number, endTangent: number): RealKeyframeValue => new RealKeyframeValue({ + const valueToFrame = (value: number, inTangent: number, outTangent: number): RealKeyframeValue => new RealKeyframeValue({ value, - startTangent, - endTangent, + leftTangent: inTangent, + rightTangent: outTangent, interpMode: interpMethod, }); switch (components) { @@ -530,10 +530,10 @@ class LegacyEasingMethodConverter { ); currentKeyframeValue.interpMode = RealInterpMode.CUBIC; currentKeyframeValue.tangentWeightMode = TangentWeightMode.BOTH; - currentKeyframeValue.startTangent = previousTangent; - currentKeyframeValue.startTangentWeight = previousTangentWeight; - currentKeyframeValue.endTangent = nextTangent; - currentKeyframeValue.endTangentWeight = nextTangentWeight; + currentKeyframeValue.rightTangent = previousTangent; + currentKeyframeValue.rightTangentWeight = previousTangentWeight; + nextKeyframeValue.leftTangent = nextTangent; + nextKeyframeValue.leftTangentWeight = nextTangentWeight; } else { const bernstein = new Array(4).fill(0); // Easing methods in `easing` diff --git a/cocos/core/curves/curve.ts b/cocos/core/curves/curve.ts index dec34dfcfc0..2914e0d521a 100644 --- a/cocos/core/curves/curve.ts +++ b/cocos/core/curves/curve.ts @@ -18,16 +18,16 @@ export class RealKeyframeValue { interpMode, tangentWeightMode, value, - startTangent, - startTangentWeight, - endTangent, - endTangentWeight, + rightTangent, + rightTangentWeight, + leftTangent, + leftTangentWeight, }: Partial = { }) { this.value = value ?? this.value; - this.startTangent = startTangent ?? this.startTangent; - this.startTangentWeight = startTangentWeight ?? this.startTangentWeight; - this.endTangent = endTangent ?? this.endTangent; - this.endTangentWeight = endTangentWeight ?? this.endTangentWeight; + this.rightTangent = rightTangent ?? this.rightTangent; + this.rightTangentWeight = rightTangentWeight ?? this.rightTangentWeight; + this.leftTangent = leftTangent ?? this.leftTangent; + this.leftTangentWeight = leftTangentWeight ?? this.leftTangentWeight; this.interpMode = interpMode ?? this.interpMode; this.tangentWeightMode = tangentWeightMode ?? this.tangentWeightMode; } @@ -56,7 +56,7 @@ export class RealKeyframeValue { * Meaningless otherwise. */ @serializable - public startTangent = 0.0; + public rightTangent = 0.0; /** * The x component tangent of this keyframe @@ -64,7 +64,7 @@ export class RealKeyframeValue { * Meaningless otherwise. */ @serializable - public startTangentWeight = 0.0; + public rightTangentWeight = 0.0; /** * The (y component of) tangent of this keyframe @@ -72,7 +72,7 @@ export class RealKeyframeValue { * Meaningless otherwise. */ @serializable - public endTangent = 0.0; + public leftTangent = 0.0; /** * The x component of tangent of this keyframe @@ -80,7 +80,7 @@ export class RealKeyframeValue { * Meaningless otherwise. */ @serializable - public endTangentWeight = 0.0; + public leftTangentWeight = 0.0; } /** @@ -143,7 +143,7 @@ export class RealCurve extends EditorExtendableMixin { // Underflow const preValue = values[0]; switch (preExtrap) { - case ExtrapMode.REPEAT: + case ExtrapMode.LOOP: time = firstTime + repeat(time - firstTime, lastTime - firstTime); break; case ExtrapMode.PING_PONG: @@ -127,7 +127,7 @@ export class QuaternionCurve extends KeyframeCurve { // Overflow const preValue = values[nFrames - 1]; switch (postExtrap) { - case ExtrapMode.REPEAT: + case ExtrapMode.LOOP: time = firstTime + repeat(time - firstTime, lastTime - firstTime); break; case ExtrapMode.PING_PONG: diff --git a/cocos/core/curves/real-curve-param.ts b/cocos/core/curves/real-curve-param.ts index ba9d31c6b54..9e4080c65f3 100644 --- a/cocos/core/curves/real-curve-param.ts +++ b/cocos/core/curves/real-curve-param.ts @@ -41,7 +41,7 @@ export enum ExtrapMode { /** * Before evaluation, repeatedly mapping the input time into the allowed range. */ - REPEAT, + LOOP, /** * Before evaluation, mapping the input time into the allowed range like ping pong. @@ -52,9 +52,9 @@ export enum ExtrapMode { export enum TangentWeightMode { NONE = 0, - START = 1, + LEFT = 1, - END = 2, + RIGHT = 2, BOTH = 1 | 2, } diff --git a/cocos/core/geometry/curve.ts b/cocos/core/geometry/curve.ts index 99ab5069197..27c270cb131 100644 --- a/cocos/core/geometry/curve.ts +++ b/cocos/core/geometry/curve.ts @@ -139,8 +139,8 @@ export class AnimationCurve { const legacyKeyframe = new Keyframe(); legacyKeyframe.time = time; legacyKeyframe.value = value.value; - legacyKeyframe.inTangent = value.startTangent; - legacyKeyframe.outTangent = value.endTangent; + legacyKeyframe.inTangent = value.leftTangent; + legacyKeyframe.outTangent = value.rightTangent; return legacyKeyframe; }); } @@ -151,8 +151,8 @@ export class AnimationCurve { new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: legacyCurve.value, - startTangent: legacyCurve.inTangent, - endTangent: legacyCurve.outTangent, + leftTangent: legacyCurve.inTangent, + rightTangent: legacyCurve.outTangent, }), ])); } @@ -197,7 +197,7 @@ export class AnimationCurve { } else { const curve = new RealCurve(); this._curve = curve; - curve.preExtrap = ExtrapMode.REPEAT; + curve.preExtrap = ExtrapMode.LOOP; curve.postExtrap = ExtrapMode.CLAMP; if (!keyFrames) { curve.assignSorted([ @@ -208,8 +208,8 @@ export class AnimationCurve { curve.assignSorted(keyFrames.map((legacyKeyframe) => [legacyKeyframe.time, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: legacyKeyframe.value, - startTangent: legacyKeyframe.inTangent, - endTangent: legacyKeyframe.outTangent, + leftTangent: legacyKeyframe.inTangent, + rightTangent: legacyKeyframe.outTangent, })])); } } @@ -230,8 +230,8 @@ export class AnimationCurve { this._curve.addKeyFrame(keyFrame.time, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: keyFrame.value, - startTangent: keyFrame.inTangent, - endTangent: keyFrame.outTangent, + leftTangent: keyFrame.inTangent, + rightTangent: keyFrame.outTangent, })); } } @@ -260,7 +260,7 @@ export class AnimationCurve { const startTime = curve.getKeyframeTime(0); const endTime = curve.getKeyframeTime(lastKeyframeIndex); switch (extrapMode) { - case ExtrapMode.REPEAT: + case ExtrapMode.LOOP: wrappedTime = repeat(time - startTime, endTime - startTime) + startTime; break; case ExtrapMode.PING_PONG: @@ -289,8 +289,8 @@ export class AnimationCurve { public calcOptimizedKey (optKey: OptimizedKey, leftIndex: number, rightIndex: number) { const lhsTime = this._curve.getKeyframeTime(leftIndex); const rhsTime = this._curve.getKeyframeTime(rightIndex); - const { value: lhsValue, endTangent: lhsOutTangent } = this._curve.getKeyframeValue(leftIndex); - const { value: rhsValue, startTangent: rhsInTangent } = this._curve.getKeyframeValue(rightIndex); + const { value: lhsValue, leftTangent: lhsOutTangent } = this._curve.getKeyframeValue(leftIndex); + const { value: rhsValue, rightTangent: rhsInTangent } = this._curve.getKeyframeValue(rightIndex); optKey.index = leftIndex; optKey.time = lhsTime; optKey.endTime = rhsTime; @@ -356,7 +356,7 @@ function fromLegacyWrapMode (legacyWrapMode: WrapModeMask): ExtrapMode { case WrapModeMask.Normal: case WrapModeMask.Clamp: return ExtrapMode.CLAMP; case WrapModeMask.PingPong: return ExtrapMode.PING_PONG; - case WrapModeMask.Loop: return ExtrapMode.REPEAT; + case WrapModeMask.Loop: return ExtrapMode.LOOP; } } @@ -366,7 +366,7 @@ function toLegacyWrapMode (extrapMode: ExtrapMode): WrapModeMask { case ExtrapMode.LINEAR: case ExtrapMode.CLAMP: return WrapModeMask.Clamp; case ExtrapMode.PING_PONG: return WrapModeMask.PingPong; - case ExtrapMode.REPEAT: return WrapModeMask.Loop; + case ExtrapMode.LOOP: return WrapModeMask.Loop; } } diff --git a/tests/animation/animaion-clip-migration-3.x.test.ts b/tests/animation/animaion-clip-migration-3.x.test.ts index 1ac16c10875..3a609c00179 100644 --- a/tests/animation/animaion-clip-migration-3.x.test.ts +++ b/tests/animation/animaion-clip-migration-3.x.test.ts @@ -410,21 +410,21 @@ describe('Animation Clip Migration 3.x', () => { interpMode: RealInterpMode.CUBIC, tangentWeightMode: TangentWeightMode.BOTH, value: 1, - startTangent: 14.999999999999998, - startTangentWeight: 0.6013318551349163, - endTangent: 8.333333333333334, - endTangentWeight: 1.0071742649611337, + rightTangent: 14.999999999999998, + rightTangentWeight: 0.6013318551349163, }), new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, tangentWeightMode: TangentWeightMode.BOTH, value: 3, - startTangent: 5.999999999999998, - startTangentWeight: 0.6082762530298218, - endTangent: 3.3333333333333335, - endTangentWeight: 1.044030650891055, + leftTangent: 8.333333333333334, + leftTangentWeight: 1.0071742649611337, + rightTangent: 5.999999999999998, + rightTangentWeight: 0.6082762530298218, }), new RealKeyframeValue({ interpMode: RealInterpMode.LINEAR, value: 5, + leftTangent: 3.3333333333333335, + leftTangentWeight: 1.044030650891055, })]); }); }); @@ -451,13 +451,13 @@ describe('Animation Clip Migration 3.x', () => { interpMode: RealInterpMode.CUBIC, tangentWeightMode: TangentWeightMode.BOTH, value: 3, - startTangent: 14.999999999999996, - startTangentWeight: 0.6013318551349163, - endTangent: 8.333333333333332, - endTangentWeight: 1.0071742649611337, + rightTangent: 14.999999999999996, + rightTangentWeight: 0.6013318551349163, }), new RealKeyframeValue({ interpMode: RealInterpMode.LINEAR, value: 5, + leftTangent: 8.333333333333332, + leftTangentWeight: 1.0071742649611337, })]); }); }); @@ -485,7 +485,7 @@ describe('Animation Clip Migration 3.x', () => { }; function testTimeBezierCurveConversion (testCase: TimeBezierTestCase) { - const [endTangent, endTangentWeight, startTangent, startTangentWeight] = timeBezierToTangents( + const [rightTangent, rightTangentWeight, leftTangent, leftTangentWeight] = timeBezierToTangents( testCase.bezierPoints, testCase.t0, testCase.v0, @@ -496,17 +496,17 @@ describe('Animation Clip Migration 3.x', () => { curve.assignSorted([ [testCase.t0, new RealKeyframeValue({ value: testCase.v0, - endTangent, - endTangentWeight, + rightTangent, + rightTangentWeight, interpMode: RealInterpMode.CUBIC, - tangentWeightMode: TangentWeightMode.END, + tangentWeightMode: TangentWeightMode.RIGHT, })], [testCase.t1, new RealKeyframeValue({ value: testCase.v1, - startTangent, - startTangentWeight, + leftTangent, + leftTangentWeight, interpMode: RealInterpMode.CUBIC, - tangentWeightMode: TangentWeightMode.START, + tangentWeightMode: TangentWeightMode.LEFT, })], ]); diff --git a/tests/core/geometry/geometry-curve.test.ts b/tests/core/geometry/geometry-curve.test.ts index adfd80227a2..308a1993c5f 100644 --- a/tests/core/geometry/geometry-curve.test.ts +++ b/tests/core/geometry/geometry-curve.test.ts @@ -30,8 +30,8 @@ describe('geometry.AnimationCurve', () => { [0.1, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 0.1, - startTangent: 0.2, - endTangent: 0.3, + leftTangent: 0.2, + rightTangent: 0.3, })], // Non cubic keyframe [0.2, new RealKeyframeValue({ @@ -42,11 +42,11 @@ describe('geometry.AnimationCurve', () => { [0.3, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 0.1, - startTangent: 0.2, - endTangent: 0.3, - tangentWeightMode: TangentWeightMode.START, - startTangentWeight: 0.4, - endTangentWeight: 0.5, + leftTangent: 0.2, + rightTangent: 0.3, + tangentWeightMode: TangentWeightMode.RIGHT, + leftTangentWeight: 0.4, + rightTangentWeight: 0.5, })], ]); @@ -59,7 +59,7 @@ describe('geometry.AnimationCurve', () => { }); test.each([ - { extrapMode: ExtrapMode.REPEAT, expected: WrapModeMask.Loop }, + { extrapMode: ExtrapMode.LOOP, expected: WrapModeMask.Loop }, { extrapMode: ExtrapMode.PING_PONG, expected: WrapModeMask.PingPong }, { extrapMode: ExtrapMode.CLAMP, expected: WrapModeMask.Clamp }, { extrapMode: ExtrapMode.LINEAR, expected: WrapModeMask.Clamp }, @@ -75,7 +75,7 @@ describe('geometry.AnimationCurve', () => { test.each([ { wrapMode: WrapModeMask.Clamp, extrapMode: ExtrapMode.CLAMP, }, - { wrapMode: WrapModeMask.Loop, extrapMode: ExtrapMode.REPEAT, }, + { wrapMode: WrapModeMask.Loop, extrapMode: ExtrapMode.LOOP, }, { wrapMode: WrapModeMask.PingPong, extrapMode: ExtrapMode.PING_PONG, }, ])(`Wrap mode $wrapMode`, ({ wrapMode, extrapMode }) => { const curve = new AnimationCurve(); @@ -149,8 +149,8 @@ describe('geometry.AnimationCurve', () => { [0.1, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 0.1, - startTangent: 0.2, - endTangent: 0.3, + leftTangent: 0.2, + rightTangent: 0.3, })], // Non cubic keyframe [0.2, new RealKeyframeValue({ @@ -161,11 +161,11 @@ describe('geometry.AnimationCurve', () => { [0.3, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 0.1, - startTangent: 0.2, - endTangent: 0.3, - tangentWeightMode: TangentWeightMode.START, - startTangentWeight: 0.4, - endTangentWeight: 0.5, + leftTangent: 0.2, + rightTangent: 0.3, + tangentWeightMode: TangentWeightMode.RIGHT, + leftTangentWeight: 0.4, + rightTangentWeight: 0.5, })], ]); expect(curve.keyFrames).toStrictEqual([ diff --git a/tests/curves/curve.test.ts b/tests/curves/curve.test.ts index b16749fac78..0aede5a8f23 100644 --- a/tests/curves/curve.test.ts +++ b/tests/curves/curve.test.ts @@ -41,14 +41,14 @@ describe('Curve', () => { test('Normal', () => { const curve = new RealCurve(); curve.assignSorted([0.1, 0.2, 0.3], [ - new RealKeyframeValue({ value: 0.4, startTangent: 0.0, endTangent: 0.0, interpMode: RealInterpMode.CONSTANT }), - new RealKeyframeValue({ value: 0.5, startTangent: 0.0, endTangent: 0.0, interpMode: RealInterpMode.LINEAR }), + new RealKeyframeValue({ value: 0.4, rightTangent: 0.0, leftTangent: 0.0, interpMode: RealInterpMode.CONSTANT }), + new RealKeyframeValue({ value: 0.5, rightTangent: 0.0, leftTangent: 0.0, interpMode: RealInterpMode.LINEAR }), new RealKeyframeValue({ value: 0.6, - startTangent: 0.487, - startTangentWeight: 0.2, - endTangent: 0.4598, - endTangentWeight: 0.32, + rightTangent: 0.487, + rightTangentWeight: 0.2, + leftTangent: 0.4598, + leftTangentWeight: 0.32, interpMode: RealInterpMode.CUBIC, tangentWeightMode: TangentWeightMode.BOTH, }), @@ -81,10 +81,10 @@ describe('Curve', () => { const keyframeValue = new RealKeyframeValue({}); expect(keyframeValue.value).toBe(0.0); expect(keyframeValue.interpMode).toBe(RealInterpMode.LINEAR); - expect(keyframeValue.startTangent).toBe(0.0); - expect(keyframeValue.startTangentWeight).toBe(0.0); - expect(keyframeValue.endTangent).toBe(0.0); - expect(keyframeValue.endTangentWeight).toBe(0.0); + expect(keyframeValue.rightTangent).toBe(0.0); + expect(keyframeValue.rightTangentWeight).toBe(0.0); + expect(keyframeValue.leftTangent).toBe(0.0); + expect(keyframeValue.leftTangentWeight).toBe(0.0); }); describe('Evaluation', () => { @@ -118,16 +118,16 @@ describe('Curve', () => { interpMode: RealInterpMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.7, - endTangent: Math.tan(toRadian(30.0)), + rightTangent: Math.tan(toRadian(30.0)), })], [0.4, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.8, - startTangent: Math.tan(toRadian(30.0)), + leftTangent: Math.tan(toRadian(30.0)), })], ]); - expect(curve.evaluate(0.28)).toBeCloseTo(0.740742562, 5); + expect(curve.evaluate(0.28)).toBeCloseTo(0.74074, 5); }); test('Interpolation mode: cubic; Start weight is used', () => { @@ -135,18 +135,18 @@ describe('Curve', () => { curve.assignSorted([ [0.2, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, - tangentWeightMode: TangentWeightMode.END, + tangentWeightMode: TangentWeightMode.RIGHT, value: 0.7, - endTangent: Math.tan(toRadian(30.0)), + rightTangent: Math.tan(toRadian(30.0)), })], [0.4, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.8, - startTangent: Math.tan(toRadian(30.0)), + leftTangent: Math.tan(toRadian(30.0)), })], ]); - expect(curve.evaluate(0.28)).toBeCloseTo(0.737992646, 5); + expect(curve.evaluate(0.28)).toBeCloseTo(0.73799, 5); }); test('Interpolation mode: cubic; End weight is used', () => { @@ -156,16 +156,16 @@ describe('Curve', () => { interpMode: RealInterpMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.7, - endTangent: Math.tan(toRadian(30.0)), + rightTangent: Math.tan(toRadian(30.0)), })], [0.4, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, - tangentWeightMode: TangentWeightMode.START, + tangentWeightMode: TangentWeightMode.LEFT, value: 0.8, - startTangent: Math.tan(toRadian(30.0)), + leftTangent: Math.tan(toRadian(30.0)), })], ]); - expect(curve.evaluate(0.28)).toBeCloseTo(0.7422913, 5); + expect(curve.evaluate(0.28)).toBeCloseTo(0.74229, 5); }); test('Extrap mode: clamp', () => { @@ -202,10 +202,10 @@ describe('Curve', () => { expect(curve.evaluate(0.46)).toBeCloseTo(5.0); }); - test('Extrap mode: repeat', () => { + test('Extrap mode: loop', () => { const curve = new RealCurve(); - curve.preExtrap = ExtrapMode.REPEAT; - curve.postExtrap = ExtrapMode.REPEAT; + curve.preExtrap = ExtrapMode.LOOP; + curve.postExtrap = ExtrapMode.LOOP; curve.assignSorted([ [0.2, new RealKeyframeValue({ value: 5.0 })], @@ -259,10 +259,10 @@ function compareCurves (left: RealCurve, right: RealCurve, numDigits = 2) { const leftKeyframeValue = left.getKeyframeValue(iKeyframe); const rightKeyframeValue = right.getKeyframeValue(iKeyframe); expect(leftKeyframeValue.value).toBeCloseTo(rightKeyframeValue.value, numDigits); - expect(leftKeyframeValue.startTangent).toBeCloseTo(rightKeyframeValue.startTangent, numDigits); - expect(leftKeyframeValue.startTangentWeight).toBeCloseTo(rightKeyframeValue.startTangentWeight, numDigits); - expect(leftKeyframeValue.endTangent).toBeCloseTo(rightKeyframeValue.endTangent, numDigits); - expect(leftKeyframeValue.endTangentWeight).toBeCloseTo(rightKeyframeValue.endTangentWeight, numDigits); + expect(leftKeyframeValue.rightTangent).toBeCloseTo(rightKeyframeValue.rightTangent, numDigits); + expect(leftKeyframeValue.rightTangentWeight).toBeCloseTo(rightKeyframeValue.rightTangentWeight, numDigits); + expect(leftKeyframeValue.leftTangent).toBeCloseTo(rightKeyframeValue.leftTangent, numDigits); + expect(leftKeyframeValue.leftTangentWeight).toBeCloseTo(rightKeyframeValue.leftTangentWeight, numDigits); expect(leftKeyframeValue.interpMode).toStrictEqual(rightKeyframeValue.interpMode); } } \ No newline at end of file diff --git a/tests/curves/key-shared-curves.test.ts b/tests/curves/key-shared-curves.test.ts index e505a71d83e..63a31c572cf 100644 --- a/tests/curves/key-shared-curves.test.ts +++ b/tests/curves/key-shared-curves.test.ts @@ -19,7 +19,7 @@ describe('Keys shared real curves', () => { curve.assignSorted([[0.1, new RealKeyframeValue({ value: 0.1, })]]); - curve.postExtrap = ExtrapMode.REPEAT; + curve.postExtrap = ExtrapMode.LOOP; expect(KeySharedRealCurves.allowedForCurve(curve)).toBe(false); } @@ -28,7 +28,7 @@ describe('Keys shared real curves', () => { curve.assignSorted([[0.1, new RealKeyframeValue({ value: 0.1, })]]); - curve.preExtrap = ExtrapMode.REPEAT; + curve.preExtrap = ExtrapMode.LOOP; expect(KeySharedRealCurves.allowedForCurve(curve)).toBe(false); } @@ -133,7 +133,7 @@ describe('Keys shared quaternion curves', () => { curve.assignSorted([[0.1, new QuaternionKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, })]]); - curve.postExtrap = ExtrapMode.REPEAT; + curve.postExtrap = ExtrapMode.LOOP; expect(KeySharedQuaternionCurves.allowedForCurve(curve)).toBe(false); } @@ -142,7 +142,7 @@ describe('Keys shared quaternion curves', () => { curve.assignSorted([[0.1, new QuaternionKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, })]]); - curve.preExtrap = ExtrapMode.REPEAT; + curve.preExtrap = ExtrapMode.LOOP; expect(KeySharedQuaternionCurves.allowedForCurve(curve)).toBe(false); } From 04af6675824b9fdb8a9baa8c2efda79371125863 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Mon, 19 Jul 2021 13:41:40 +0800 Subject: [PATCH 20/35] Curve range rename --- cocos/particle/animator/curve-range.ts | 38 +++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/cocos/particle/animator/curve-range.ts b/cocos/particle/animator/curve-range.ts index b4266a46698..e8b65220b9c 100644 --- a/cocos/particle/animator/curve-range.ts +++ b/cocos/particle/animator/curve-range.ts @@ -38,10 +38,10 @@ import { PixelFormat, Filter, WrapMode } from '../../core/assets/asset-enum'; const SerializableTable = [ ['mode', 'constant', 'multiplier'], - ['mode', 'value', 'multiplier'], - ['mode', 'min', 'max', 'multiplier'], + ['mode', 'spline', 'multiplier'], + ['mode', 'splineMin', 'splineMax', 'multiplier'], ['mode', 'constantMin', 'constantMax', 'multiplier'], -]; +] as const; export const Mode = Enum({ Constant: 0, @@ -64,23 +64,23 @@ export default class CurveRange { * @zh 当mode为Curve时,使用的曲线。 */ @type(RealCurve) - public value = constructLegacyCurveAndConvert(); + public spline = constructLegacyCurveAndConvert(); /** * @zh 当mode为TwoCurves时,使用的曲线下限。 */ @type(RealCurve) - public min = constructLegacyCurveAndConvert(); + public splineMin = constructLegacyCurveAndConvert(); /** * @zh 当mode为TwoCurves时,使用的曲线上限。 */ @type(RealCurve) - public max = constructLegacyCurveAndConvert(); + public splineMax = constructLegacyCurveAndConvert(); /** * @zh 当mode为Curve时,使用的曲线。 - * @deprecated Since V3.2. Use `value` instead. + * @deprecated Since V3.3. Use `spline` instead. */ get curve () { return this._curve; @@ -88,12 +88,12 @@ export default class CurveRange { set curve (value) { this._curve = value; - this.value = value._internalCurve; + this.spline = value._internalCurve; } /** * @zh 当mode为TwoCurves时,使用的曲线下限。 - * @deprecated Since V3.2. Use `min` instead. + * @deprecated Since V3.3. Use `splineMin` instead. */ get curveMin () { return this._curveMin; @@ -101,12 +101,12 @@ export default class CurveRange { set curveMin (value) { this._curveMin = value; - this.min = value._internalCurve; + this.splineMin = value._internalCurve; } /** * @zh 当mode为TwoCurves时,使用的曲线上限。 - * @deprecated Since V3.2. Use `max` instead. + * @deprecated Since V3.3. Use `splineMax` instead. */ get curveMax () { return this._curveMax; @@ -114,7 +114,7 @@ export default class CurveRange { set curveMax (value) { this._curveMax = value; - this.max = value._internalCurve; + this.splineMax = value._internalCurve; } /** @@ -155,9 +155,9 @@ export default class CurveRange { case Mode.Constant: return this.constant; case Mode.Curve: - return this.value.evaluate(time) * this.multiplier; + return this.spline.evaluate(time) * this.multiplier; case Mode.TwoCurves: - return lerp(this.min.evaluate(time), this.max.evaluate(time), rndRatio) * this.multiplier; + return lerp(this.splineMin.evaluate(time), this.splineMax.evaluate(time), rndRatio) * this.multiplier; case Mode.TwoConstants: return lerp(this.constantMin, this.constantMax, rndRatio); } @@ -182,9 +182,9 @@ export default class CurveRange { return SerializableTable[this.mode]; } - private _curve = new AnimationCurve(this.value); - private _curveMin = new AnimationCurve(this.min); - private _curveMax = new AnimationCurve(this.max); + private _curve = new AnimationCurve(this.spline); + private _curveMin = new AnimationCurve(this.splineMin); + private _curveMax = new AnimationCurve(this.splineMax); } function evaluateCurve (cr: CurveRange, time: number, index: number) { @@ -192,9 +192,9 @@ function evaluateCurve (cr: CurveRange, time: number, index: number) { case Mode.Constant: return cr.constant; case Mode.Curve: - return cr.value.evaluate(time) * cr.multiplier; + return cr.spline.evaluate(time) * cr.multiplier; case Mode.TwoCurves: - return index === 0 ? cr.min.evaluate(time) * cr.multiplier : cr.max.evaluate(time) * cr.multiplier; + return index === 0 ? cr.splineMin.evaluate(time) * cr.multiplier : cr.splineMax.evaluate(time) * cr.multiplier; case Mode.TwoConstants: return index === 0 ? cr.constantMin : cr.constantMax; default: From 3dd06b76245e9d1b53251be915561e18525bd6bf Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Mon, 19 Jul 2021 19:05:09 +0800 Subject: [PATCH 21/35] Easing methods --- cocos/core/animation/legacy-clip-data.ts | 276 +++++++++++++----- cocos/core/curves/curve.ts | 128 +++++++- .../animaion-clip-migration-3.x.test.ts | 97 ++++-- tests/curves/curve.test.ts | 5 +- 4 files changed, 414 insertions(+), 92 deletions(-) diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts index 8a657963a07..6974c06ef8d 100644 --- a/cocos/core/animation/legacy-clip-data.ts +++ b/cocos/core/animation/legacy-clip-data.ts @@ -11,13 +11,14 @@ import { Track, TrackPath } from './tracks/track'; import { UntypedTrack } from './tracks/untyped-track'; import { warn } from '../platform'; import { RealTrack } from './tracks/real-track'; -import { Color, Quat, Size, Vec2, Vec3, Vec4 } from '../math'; +import { Color, lerp, Quat, Size, Vec2, Vec3, Vec4 } from '../math'; import { CubicSplineNumberValue, CubicSplineQuatValue, CubicSplineVec2Value, CubicSplineVec3Value, CubicSplineVec4Value } from './cubic-spline-value'; import { ColorTrack } from './tracks/color-track'; import { VectorTrack } from './tracks/vector-track'; import { QuaternionTrack } from './tracks/quat-track'; import { ObjectTrack } from './tracks/object-track'; import { SizeTrack } from './tracks/size-track'; +import { EasingMethod } from '../curves/curve'; /** * 表示曲线值,曲线值可以是任意类型,但必须符合插值方式的要求。 @@ -519,65 +520,19 @@ class LegacyEasingMethodConverter { } if (Array.isArray(easingMethod)) { // Time bezier points - const currentKeyframeValue = curve.getKeyframeValue(iKeyframe); - const nextKeyframeValue = curve.getKeyframeValue(iKeyframe + 1); - const [previousTangent, previousTangentWeight, nextTangent, nextTangentWeight] = timeBezierToTangents( + timeBezierToTangents( easingMethod, curve.getKeyframeTime(iKeyframe), - currentKeyframeValue.value, + curve.getKeyframeValue(iKeyframe), curve.getKeyframeTime(iKeyframe + 1), - nextKeyframeValue.value, + curve.getKeyframeValue(iKeyframe + 1), ); - currentKeyframeValue.interpMode = RealInterpMode.CUBIC; - currentKeyframeValue.tangentWeightMode = TangentWeightMode.BOTH; - currentKeyframeValue.rightTangent = previousTangent; - currentKeyframeValue.rightTangentWeight = previousTangentWeight; - nextKeyframeValue.leftTangent = nextTangent; - nextKeyframeValue.leftTangentWeight = nextTangentWeight; } else { - const bernstein = new Array(4).fill(0); - // Easing methods in `easing` - switch (easingMethod) { - case 'constant': - case 'linear': - break; - case 'quadIn': - // k * k - powerToBernstein([0.0, 0.0, 1.0, 0.0], bernstein); - break; - case 'quadOut': - // k * (2 - k) - powerToBernstein([0.0, 2.0, -1.0, 0.0], bernstein); - break; - case 'cubicIn': - // k * k * k - powerToBernstein([0.0, 0.0, 0.0, 1.0], bernstein); - break; - case 'cubicOut': - // --k * k * k + 1; - powerToBernstein([0.0, 0.0, 0.0, 1.0], bernstein); - break; - case 'backIn': { - // k * k * ((s + 1) * k - s) - const s = 1.70158; - powerToBernstein([1.0, 0.0, -s, s + 1.0], bernstein); - break; - } - case 'backOut': { - // k * k * ((s + 1) * k - s) - const s = 1.70158; - powerToBernstein([1.0, 0.0, s, s + 1.0], bernstein); - break; - } - case 'smooth': { - // k * k * (3 - 2 * k) - powerToBernstein([0.0, 0.0, 3.0, -2.0], bernstein); - break; - } - default: - // TODO: do sample - assertIsTrue(false); - } + applyLegacyEasingMethodName( + easingMethod, + curve, + iKeyframe, + ); } } } @@ -585,6 +540,73 @@ class LegacyEasingMethodConverter { private _easingMethods: LegacyEasingMethod[] | undefined; } +/** + * @returns Inserted keyframes count. + */ +function applyLegacyEasingMethodName ( + easingMethodName: LegacyEasingMethodName, + curve: RealCurve, + keyframeIndex: number, +) { + assertIsTrue(keyframeIndex !== curve.keyFramesCount - 1); + assertIsTrue(easingMethodName in easingMethodNameMap); + const keyframeValue = curve.getKeyframeValue(keyframeIndex); + const easingMethod = easingMethodNameMap[easingMethodName]; + if (easingMethod === EasingMethod.CONSTANT) { + keyframeValue.interpMode = RealInterpMode.CONSTANT; + } else { + keyframeValue.interpMode = RealInterpMode.LINEAR; + keyframeValue.easingMethod = easingMethod; + } +} + +const easingMethodNameMap: Record = { + constant: EasingMethod.CONSTANT, + linear: EasingMethod.LINEAR, + quadIn: EasingMethod.QUAD_IN, + quadOut: EasingMethod.QUAD_OUT, + quadInOut: EasingMethod.QUAD_IN_OUT, + quadOutIn: EasingMethod.QUAD_OUT_IN, + cubicIn: EasingMethod.CUBIC_IN, + cubicOut: EasingMethod.CUBIC_OUT, + cubicInOut: EasingMethod.CUBIC_IN_OUT, + cubicOutIn: EasingMethod.CUBIC_OUT_IN, + quartIn: EasingMethod.QUART_IN, + quartOut: EasingMethod.QUART_OUT, + quartInOut: EasingMethod.QUART_IN_OUT, + quartOutIn: EasingMethod.QUART_OUT_IN, + quintIn: EasingMethod.QUINT_IN, + quintOut: EasingMethod.QUINT_OUT, + quintInOut: EasingMethod.QUINT_IN_OUT, + quintOutIn: EasingMethod.QUINT_OUT_IN, + sineIn: EasingMethod.SINE_IN, + sineOut: EasingMethod.SINE_OUT, + sineInOut: EasingMethod.SINE_IN_OUT, + sineOutIn: EasingMethod.SINE_OUT_IN, + expoIn: EasingMethod.EXPO_IN, + expoOut: EasingMethod.EXPO_OUT, + expoInOut: EasingMethod.EXPO_IN_OUT, + expoOutIn: EasingMethod.EXPO_OUT_IN, + circIn: EasingMethod.CIRC_IN, + circOut: EasingMethod.CIRC_OUT, + circInOut: EasingMethod.CIRC_IN_OUT, + circOutIn: EasingMethod.CIRC_OUT_IN, + elasticIn: EasingMethod.ELASTIC_IN, + elasticOut: EasingMethod.ELASTIC_OUT, + elasticInOut: EasingMethod.ELASTIC_IN_OUT, + elasticOutIn: EasingMethod.ELASTIC_OUT_IN, + backIn: EasingMethod.BACK_IN, + backOut: EasingMethod.BACK_OUT, + backInOut: EasingMethod.BACK_IN_OUT, + backOutIn: EasingMethod.BACK_OUT_IN, + bounceIn: EasingMethod.BOUNCE_IN, + bounceOut: EasingMethod.BOUNCE_OUT, + bounceInOut: EasingMethod.BOUNCE_IN_OUT, + bounceOutIn: EasingMethod.BOUNCE_OUT_IN, + smooth: EasingMethod.SMOOTH, + fade: EasingMethod.FADE, +}; + /** * Legacy curve uses time based bezier curve interpolation. * That's, interpolate time 'x'(time ratio between two frames, eg.[0, 1]) @@ -599,11 +621,13 @@ class LegacyEasingMethodConverter { export function timeBezierToTangents ( timeBezierPoints: BezierControlPoints, previousTime: number, - previousValue: number, + previousKeyframe: RealKeyframeValue, nextTime: number, - nextValue: number, -): [previousTangent: number, previousTangentWeight: number, nextTangent: number, nextTangentWeight: number] { + nextKeyframe: RealKeyframeValue, +) { const [p1X, p1Y, p2X, p2Y] = timeBezierPoints; + const { value: previousValue } = previousKeyframe; + const { value: nextValue } = nextKeyframe; const dValue = nextValue - previousValue; const dTime = nextTime - previousTime; const fx = 3 * dTime; @@ -613,18 +637,130 @@ export function timeBezierToTangents ( const t2x = (1.0 - p2X) * fx; const t2y = (1.0 - p2Y) * fy; const ONE_THIRD = 1.0 / 3.0; - return [ - t1y / t1x, - Math.sqrt(t1x * t1x + t1y * t1y) * ONE_THIRD, - t2y / t2x, - Math.sqrt(t2x * t2x + t2y * t2y) * ONE_THIRD, - ]; + const previousTangent = t1y / t1x; + const previousTangentWeight = Math.sqrt(t1x * t1x + t1y * t1y) * ONE_THIRD; + const nextTangent = t2y / t2x; + const nextTangentWeight = Math.sqrt(t2x * t2x + t2y * t2y) * ONE_THIRD; + previousKeyframe.interpMode = RealInterpMode.CUBIC; + previousKeyframe.tangentWeightMode = ensureRightTangentWeightMode(previousKeyframe.tangentWeightMode); + previousKeyframe.rightTangent = previousTangent; + previousKeyframe.rightTangentWeight = previousTangentWeight; + nextKeyframe.tangentWeightMode = ensureLeftTangentWeightMode(nextKeyframe.tangentWeightMode); + nextKeyframe.leftTangent = nextTangent; + nextKeyframe.leftTangentWeight = nextTangentWeight; +} + +function ensureLeftTangentWeightMode (tangentWeightMode: TangentWeightMode) { + if (tangentWeightMode === TangentWeightMode.NONE) { + return TangentWeightMode.LEFT; + } else if (tangentWeightMode === TangentWeightMode.RIGHT) { + return TangentWeightMode.BOTH; + } else { + return tangentWeightMode; + } } -function powerToBernstein ([p0, p1, p2, p3]: [number, number, number, number], bernstein: number[]) { - // https://stackoverflow.com/questions/33859199/convert-polynomial-curve-to-bezier-curve-control-points - bernstein[0] = p0 + p1 + p2 + p3; - bernstein[1] = p1 / 3.0 + p2 * 2.0 / 3.0 + p3; - bernstein[2] = p2 / 3.0 + p3; - bernstein[3] = p3; +function ensureRightTangentWeightMode (tangentWeightMode: TangentWeightMode) { + if (tangentWeightMode === TangentWeightMode.NONE) { + return TangentWeightMode.RIGHT; + } else if (tangentWeightMode === TangentWeightMode.LEFT) { + return TangentWeightMode.BOTH; + } else { + return tangentWeightMode; + } } + +// #region TODO: convert power easing method + +// type Powers = [number, number, number, number]; + +// const POWERS_QUAD_IN: Powers = [0.0, 0.0, 1.0, 0.0]; // k * k +// const POWERS_QUAD_OUT: Powers = [0.0, 2.0, -1.0, 0.0]; // k * (2 - k) +// const POWERS_CUBIC_IN: Powers = [0.0, 0.0, 0.0, 1.0]; // k * k * k +// const POWERS_CUBIC_OUT: Powers = [0.0, 0.0, 0.0, 1.0]; // --k * k * k + 1 +// const BACK_S = 1.70158; +// const POWERS_BACK_IN: Powers = [1.0, 0.0, -BACK_S, BACK_S + 1.0]; // k * k * ((s + 1) * k - s) +// const POWERS_BACK_OUT: Powers = [1.0, 0.0, BACK_S, BACK_S + 1.0]; // k * k * ((s + 1) * k - s) +// const POWERS_SMOOTH: Powers = [0.0, 0.0, 3.0, -2.0]; // k * k * (3 - 2 * k) + +// function convertPowerMethod (curve: RealCurve, keyframeIndex: number, powers: Powers) { +// assertIsTrue(keyframeIndex !== curve.keyFramesCount - 1); +// const nextKeyframeIndex = keyframeIndex + 1; +// powerToTangents( +// powers, +// curve.getKeyframeTime(keyframeIndex), +// curve.getKeyframeValue(keyframeIndex), +// curve.getKeyframeTime(nextKeyframeIndex), +// curve.getKeyframeValue(nextKeyframeIndex), +// ); +// return 0; +// }; + +// function convertInOutPowersMethod (curve: RealCurve, keyframeIndex: number, inPowers: Powers, outPowers: Powers) { +// assertIsTrue(keyframeIndex !== curve.keyFramesCount - 1); +// const nextKeyframeIndex = keyframeIndex + 1; +// const previousTime = curve.getKeyframeTime(keyframeIndex); +// const nextTime = curve.getKeyframeTime(nextKeyframeIndex); +// const previousKeyframeValue = curve.getKeyframeValue(keyframeIndex); +// const nextKeyframeValue = curve.getKeyframeValue(nextKeyframeIndex); +// const middleTime = previousTime + (nextTime - previousTime); +// const middleValue = previousKeyframeValue.value + (nextKeyframeValue.value - previousKeyframeValue.value); +// const middleKeyframeValue = curve.getKeyframeValue(curve.addKeyFrame(middleTime, middleValue)); +// powerToTangents( +// inPowers, +// previousTime, +// previousKeyframeValue, +// middleTime, +// middleKeyframeValue, +// ); +// powerToTangents( +// outPowers, +// middleTime, +// middleKeyframeValue, +// nextTime, +// nextKeyframeValue, +// ); +// return 1; +// }; + +// function powerToTangents ( +// [a, b, c, d]: [number, number, number, number], +// previousTime: number, +// previousKeyframe: RealKeyframeValue, +// nextTime: number, +// nextKeyframe: RealKeyframeValue, +// ) { +// const bernstein = powerToBernstein([a, b, c, d]); +// const { value: previousValue } = previousKeyframe; +// const { value: nextValue } = nextKeyframe; +// timeBezierToTangents( +// [???????], +// previousTime, +// previousValue, +// nextTime, +// nextValue, +// ); +// } + +// function powerToBernstein ([p0, p1, p2, p3]: [number, number, number, number]) { +// // https://stackoverflow.com/questions/33859199/convert-polynomial-curve-to-bezier-curve-control-points +// // https://blog.demofox.org/2016/12/08/evaluating-polynomials-with-the-gpu-texture-sampler/ +// const m00 = p0; +// const m01 = p1 / 3.0; +// const m02 = p2 / 3.0; +// const m03 = p3; +// const m10 = m00 + m01; +// const m11 = m01 + m02; +// const m12 = m02 + m03; +// const m20 = m10 + m11; +// const m21 = m11 + m12; +// const m30 = m20 + m21; +// const bernstein = new Float64Array(4); +// bernstein[0] = m00; +// bernstein[1] = m10; +// bernstein[2] = m20; +// bernstein[3] = m30; +// return bernstein; +// } + +// #endregion diff --git a/cocos/core/curves/curve.ts b/cocos/core/curves/curve.ts index 2914e0d521a..6a2c70fc320 100644 --- a/cocos/core/curves/curve.ts +++ b/cocos/core/curves/curve.ts @@ -8,9 +8,57 @@ import { solveCubic } from './solve-cubic'; import { EditorExtendableMixin } from '../data/editor-extendable'; import { deserializeTag, SerializationContext, SerializationInput, SerializationOutput, serializeTag } from '../data'; import { DeserializationContext } from '../data/custom-serializable'; +import * as easing from '../animation/easing'; export { RealInterpMode, ExtrapMode, TangentWeightMode }; +export enum EasingMethod { + LINEAR, + CONSTANT, + QUAD_IN, + QUAD_OUT, + QUAD_IN_OUT, + QUAD_OUT_IN, + CUBIC_IN, + CUBIC_OUT, + CUBIC_IN_OUT, + CUBIC_OUT_IN, + QUART_IN, + QUART_OUT, + QUART_IN_OUT, + QUART_OUT_IN, + QUINT_IN, + QUINT_OUT, + QUINT_IN_OUT, + QUINT_OUT_IN, + SINE_IN, + SINE_OUT, + SINE_IN_OUT, + SINE_OUT_IN, + EXPO_IN, + EXPO_OUT, + EXPO_IN_OUT, + EXPO_OUT_IN, + CIRC_IN, + CIRC_OUT, + CIRC_IN_OUT, + CIRC_OUT_IN, + ELASTIC_IN, + ELASTIC_OUT, + ELASTIC_IN_OUT, + ELASTIC_OUT_IN, + BACK_IN, + BACK_OUT, + BACK_IN_OUT, + BACK_OUT_IN, + BOUNCE_IN, + BOUNCE_OUT, + BOUNCE_IN_OUT, + BOUNCE_OUT_IN, + SMOOTH, + FADE, +} + @ccclass('cc.RealKeyframeValue') @uniquelyReferenced export class RealKeyframeValue { @@ -22,6 +70,7 @@ export class RealKeyframeValue { rightTangentWeight, leftTangent, leftTangentWeight, + easingMethod, }: Partial = { }) { this.value = value ?? this.value; this.rightTangent = rightTangent ?? this.rightTangent; @@ -30,6 +79,7 @@ export class RealKeyframeValue { this.leftTangentWeight = leftTangentWeight ?? this.leftTangentWeight; this.interpMode = interpMode ?? this.interpMode; this.tangentWeightMode = tangentWeightMode ?? this.tangentWeightMode; + this.easingMethod = easingMethod ?? this.easingMethod; } /** @@ -81,6 +131,12 @@ export class RealKeyframeValue { */ @serializable public leftTangentWeight = 0.0; + + /** + * @deprecated Reserved for backward compatibility. Will be removed in future. + */ + @serializable + public easingMethod = EasingMethod.LINEAR; } /** @@ -304,6 +360,9 @@ export class RealCurve extends EditorExtendableMixin> FLAGS_EASING_METHOD_BITS_START) as EasingMethod; + keyframeValue.easingMethod = easingMethod; + return currentOffset; } @@ -480,8 +545,12 @@ function evalBetweenTwoKeyFrames ( default: case RealInterpMode.CONSTANT: return prevValue.value; - case RealInterpMode.LINEAR: - return lerp(prevValue.value, nextValue.value, ratio); + case RealInterpMode.LINEAR: { + const transformedRatio = prevValue.easingMethod === EasingMethod.LINEAR + ? ratio + : getEasingFn(prevValue.easingMethod)(ratio); + return lerp(prevValue.value, nextValue.value, transformedRatio); + } case RealInterpMode.CUBIC: { const ONE_THIRD = 1.0 / 3.0; const { @@ -598,3 +667,58 @@ function getParamFromCubicSolution (solutions: readonly [number, number, number] } return param; } + +type EasingMethodFn = (k: number) => number; + +const easingMethodFnMap: Record = { + [EasingMethod.CONSTANT]: easing.constant, + [EasingMethod.LINEAR]: easing.linear, + + [EasingMethod.QUAD_IN]: easing.quadIn, + [EasingMethod.QUAD_OUT]: easing.quadOut, + [EasingMethod.QUAD_IN_OUT]: easing.quadInOut, + [EasingMethod.QUAD_OUT_IN]: easing.quadOutIn, + [EasingMethod.CUBIC_IN]: easing.cubicIn, + [EasingMethod.CUBIC_OUT]: easing.cubicOut, + [EasingMethod.CUBIC_IN_OUT]: easing.cubicInOut, + [EasingMethod.CUBIC_OUT_IN]: easing.cubicOutIn, + [EasingMethod.QUART_IN]: easing.quartIn, + [EasingMethod.QUART_OUT]: easing.quartOut, + [EasingMethod.QUART_IN_OUT]: easing.quartInOut, + [EasingMethod.QUART_OUT_IN]: easing.quartOutIn, + [EasingMethod.QUINT_IN]: easing.quintIn, + [EasingMethod.QUINT_OUT]: easing.quintOut, + [EasingMethod.QUINT_IN_OUT]: easing.quintInOut, + [EasingMethod.QUINT_OUT_IN]: easing.quintOutIn, + [EasingMethod.SINE_IN]: easing.sineIn, + [EasingMethod.SINE_OUT]: easing.sineOut, + [EasingMethod.SINE_IN_OUT]: easing.sineInOut, + [EasingMethod.SINE_OUT_IN]: easing.sineOutIn, + [EasingMethod.EXPO_IN]: easing.expoIn, + [EasingMethod.EXPO_OUT]: easing.expoOut, + [EasingMethod.EXPO_IN_OUT]: easing.expoInOut, + [EasingMethod.EXPO_OUT_IN]: easing.expoOutIn, + [EasingMethod.CIRC_IN]: easing.circIn, + [EasingMethod.CIRC_OUT]: easing.circOut, + [EasingMethod.CIRC_IN_OUT]: easing.circInOut, + [EasingMethod.CIRC_OUT_IN]: easing.circOutIn, + [EasingMethod.ELASTIC_IN]: easing.elasticIn, + [EasingMethod.ELASTIC_OUT]: easing.elasticOut, + [EasingMethod.ELASTIC_IN_OUT]: easing.elasticInOut, + [EasingMethod.ELASTIC_OUT_IN]: easing.elasticOutIn, + [EasingMethod.BACK_IN]: easing.backIn, + [EasingMethod.BACK_OUT]: easing.backOut, + [EasingMethod.BACK_IN_OUT]: easing.backInOut, + [EasingMethod.BACK_OUT_IN]: easing.backOutIn, + [EasingMethod.BOUNCE_IN]: easing.bounceIn, + [EasingMethod.BOUNCE_OUT]: easing.bounceOut, + [EasingMethod.BOUNCE_IN_OUT]: easing.bounceInOut, + [EasingMethod.BOUNCE_OUT_IN]: easing.bounceOutIn, + [EasingMethod.SMOOTH]: easing.smooth, + [EasingMethod.FADE]: easing.fade, +}; + +function getEasingFn (easingMethod: EasingMethod): EasingMethodFn { + assertIsTrue(easingMethod in easingMethodFnMap); + return easingMethodFnMap[easingMethod]; +} diff --git a/tests/animation/animaion-clip-migration-3.x.test.ts b/tests/animation/animaion-clip-migration-3.x.test.ts index 3a609c00179..498086cbd29 100644 --- a/tests/animation/animaion-clip-migration-3.x.test.ts +++ b/tests/animation/animaion-clip-migration-3.x.test.ts @@ -6,7 +6,7 @@ import { LegacyClipCurve, LegacyCommonTarget, LegacyEasingMethod, timeBezierToTa import { ComponentPath, HierarchyPath, ICustomTargetPath, TargetPath } from "../../cocos/core/animation/target-path"; import { RealChannel } from "../../cocos/core/animation/tracks/track"; import { UntypedTrack } from "../../cocos/core/animation/tracks/untyped-track"; -import { ExtrapMode, RealCurve, RealKeyframeValue, TangentWeightMode } from "../../cocos/core/curves/curve"; +import { EasingMethod, ExtrapMode, RealCurve, RealKeyframeValue, TangentWeightMode } from "../../cocos/core/curves/curve"; class ValueProxyFactorFoo implements IValueProxyFactory { forTarget(_target: any): animation.IValueProxy { @@ -408,7 +408,7 @@ describe('Animation Clip Migration 3.x', () => { expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, - tangentWeightMode: TangentWeightMode.BOTH, + tangentWeightMode: TangentWeightMode.RIGHT, value: 1, rightTangent: 14.999999999999998, rightTangentWeight: 0.6013318551349163, @@ -423,10 +423,76 @@ describe('Animation Clip Migration 3.x', () => { }), new RealKeyframeValue({ interpMode: RealInterpMode.LINEAR, value: 5, + tangentWeightMode: TangentWeightMode.LEFT, leftTangent: 3.3333333333333335, leftTangentWeight: 1.044030650891055, })]); - }); + }); + + test(`Easing method name: constant`, () => { + const curve = createClipWithEasingMethodsAndConvert( + [0.1, 0.3, 0.8], + [1, 3, 5], + 'constant', + undefined, + true, + ); + expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); + expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ + interpMode: RealInterpMode.CONSTANT, + value: 1, + }), new RealKeyframeValue({ + interpMode: RealInterpMode.CONSTANT, + value: 3, + }), new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, // Last frame never converted + value: 5, + })]); + }); + + test(`Easing method name: linear`, () => { + const curve = createClipWithEasingMethodsAndConvert( + [0.1, 0.3, 0.8], + [1, 3, 5], + 'linear', + undefined, + true, + ); + expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); + expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, + value: 1, + }), new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, + value: 3, + }), new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, + value: 5, + })]); + }); + + test(`Easing method name: any other`, () => { + const curve = createClipWithEasingMethodsAndConvert( + [0.1, 0.3, 0.8], + [1, 3, 5], + 'cubicInOut', + undefined, + true, + ); + expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); + expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, + value: 1, + easingMethod: EasingMethod.CUBIC_IN_OUT, + }), new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, + value: 3, + easingMethod: EasingMethod.CUBIC_IN_OUT, + }), new RealKeyframeValue({ + interpMode: RealInterpMode.LINEAR, // Last frame never converted + value: 5, + })]); + }); }); describe(`Specified through ".easingMethods"`, () => { @@ -449,13 +515,14 @@ describe('Animation Clip Migration 3.x', () => { value: 1, }), new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, - tangentWeightMode: TangentWeightMode.BOTH, + tangentWeightMode: TangentWeightMode.RIGHT, value: 3, rightTangent: 14.999999999999996, rightTangentWeight: 0.6013318551349163, }), new RealKeyframeValue({ interpMode: RealInterpMode.LINEAR, value: 5, + tangentWeightMode: TangentWeightMode.LEFT, leftTangent: 8.333333333333332, leftTangentWeight: 1.0071742649611337, })]); @@ -485,30 +552,22 @@ describe('Animation Clip Migration 3.x', () => { }; function testTimeBezierCurveConversion (testCase: TimeBezierTestCase) { - const [rightTangent, rightTangentWeight, leftTangent, leftTangentWeight] = timeBezierToTangents( - testCase.bezierPoints, - testCase.t0, - testCase.v0, - testCase.t1, - testCase.v1, - ); const curve = new RealCurve(); curve.assignSorted([ [testCase.t0, new RealKeyframeValue({ value: testCase.v0, - rightTangent, - rightTangentWeight, - interpMode: RealInterpMode.CUBIC, - tangentWeightMode: TangentWeightMode.RIGHT, })], [testCase.t1, new RealKeyframeValue({ value: testCase.v1, - leftTangent, - leftTangentWeight, - interpMode: RealInterpMode.CUBIC, - tangentWeightMode: TangentWeightMode.LEFT, })], ]); + timeBezierToTangents( + testCase.bezierPoints, + curve.getKeyframeTime(0), + curve.getKeyframeValue(0), + curve.getKeyframeTime(1), + curve.getKeyframeValue(1), + ); for (let inputRatio = 0.0; inputRatio <= 1.0; inputRatio += 0.01) { const ratio = bezierByTime(testCase.bezierPoints, inputRatio); diff --git a/tests/curves/curve.test.ts b/tests/curves/curve.test.ts index 0aede5a8f23..4bb46542937 100644 --- a/tests/curves/curve.test.ts +++ b/tests/curves/curve.test.ts @@ -1,6 +1,6 @@ import { toRadian } from '../../cocos/core'; import { RealCurve, RealInterpMode } from '../../cocos/core/curves'; -import { RealKeyframeValue } from '../../cocos/core/curves/curve'; +import { EasingMethod, RealKeyframeValue } from '../../cocos/core/curves/curve'; import { ExtrapMode, TangentWeightMode } from '../../cocos/core/curves/real-curve-param'; import { serializeAndDeserialize } from './serialize-and-deserialize-curve'; @@ -51,6 +51,7 @@ describe('Curve', () => { leftTangentWeight: 0.32, interpMode: RealInterpMode.CUBIC, tangentWeightMode: TangentWeightMode.BOTH, + easingMethod: EasingMethod.QUAD_OUT, }), ]); compareCurves(serializeAndDeserialize(curve, RealCurve), curve); @@ -85,6 +86,7 @@ describe('Curve', () => { expect(keyframeValue.rightTangentWeight).toBe(0.0); expect(keyframeValue.leftTangent).toBe(0.0); expect(keyframeValue.leftTangentWeight).toBe(0.0); + expect(keyframeValue.easingMethod).toBe(EasingMethod.LINEAR); }); describe('Evaluation', () => { @@ -264,5 +266,6 @@ function compareCurves (left: RealCurve, right: RealCurve, numDigits = 2) { expect(leftKeyframeValue.leftTangent).toBeCloseTo(rightKeyframeValue.leftTangent, numDigits); expect(leftKeyframeValue.leftTangentWeight).toBeCloseTo(rightKeyframeValue.leftTangentWeight, numDigits); expect(leftKeyframeValue.interpMode).toStrictEqual(rightKeyframeValue.interpMode); + expect(leftKeyframeValue.easingMethod).toStrictEqual(rightKeyframeValue.easingMethod); } } \ No newline at end of file From 28019dea5d4162df2a1a841e31589f93a9bd864d Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Tue, 20 Jul 2021 12:24:16 +0800 Subject: [PATCH 22/35] Fix untyped track --- cocos/core/animation/tracks/untyped-track.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocos/core/animation/tracks/untyped-track.ts b/cocos/core/animation/tracks/untyped-track.ts index fd72f902322..c07f7b362ee 100644 --- a/cocos/core/animation/tracks/untyped-track.ts +++ b/cocos/core/animation/tracks/untyped-track.ts @@ -10,7 +10,7 @@ import { Vec2TrackEval, Vec3TrackEval, Vec4TrackEval, VectorTrack } from './vect @ccclass(`${CLASS_NAME_PREFIX_ANIM}UntypedTrackChannel`) class UntypedTrackChannel extends Channel { @serializable - public property!: string; + public property = ''; constructor () { super(new RealCurve()); From bef3e82d1cc373b17629265ab3c8af440e4f1cda Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Tue, 20 Jul 2021 13:01:30 +0800 Subject: [PATCH 23/35] Fix --- cocos/core/animation/animation-state.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cocos/core/animation/animation-state.ts b/cocos/core/animation/animation-state.ts index 70dd0411d55..050a6257b6f 100644 --- a/cocos/core/animation/animation-state.ts +++ b/cocos/core/animation/animation-state.ts @@ -368,10 +368,15 @@ export class AnimationState extends Playable { } public destroy () { + if (!this.isMotionless) { + legacyCC.director.getAnimationManager().removeAnimation(this); + } if (this._poseOutput) { this._poseOutput.destroy(); this._poseOutput = null; } + // TODO: destroy? + this._clipEval = undefined!; } /** From a954431dd53a086bccb2f50197fbb5b4881124f7 Mon Sep 17 00:00:00 2001 From: zheng han Date: Tue, 20 Jul 2021 16:27:49 +0800 Subject: [PATCH 24/35] fix upgradUnTypedTracks (#26) --- cocos/core/animation/animation-clip.ts | 7 +++++++ cocos/core/animation/tracks/untyped-track.ts | 2 ++ 2 files changed, 9 insertions(+) diff --git a/cocos/core/animation/animation-clip.ts b/cocos/core/animation/animation-clip.ts index b0c4a1e92e7..ce121e4374b 100644 --- a/cocos/core/animation/animation-clip.ts +++ b/cocos/core/animation/animation-clip.ts @@ -51,6 +51,7 @@ import { Range } from './tracks/utils'; import { ObjectTrack } from './tracks/object-track'; import type { ExoticAnimation } from './exotic-animation/exotic-animation'; import './exotic-animation/exotic-animation'; +import { array } from '../utils/js'; export declare namespace AnimationClip { export interface IEvent { @@ -398,6 +399,7 @@ export class AnimationClip extends Asset { */ public upgradeUntypedTracks (refine: UntypedTrackRefine) { const newTracks: Track[] = []; + const removals: Track[] = []; for (const track of this._tracks) { if (!(track instanceof UntypedTrack)) { continue; @@ -405,8 +407,13 @@ export class AnimationClip extends Asset { const newTrack = track.upgrade(refine); if (newTrack) { newTracks.push(newTrack); + removals.push(track); } } + for (const removal of removals) { + array.remove(this._tracks, removal); + } + this._tracks.push(...newTracks); } /** diff --git a/cocos/core/animation/tracks/untyped-track.ts b/cocos/core/animation/tracks/untyped-track.ts index c07f7b362ee..ee5c7a93c5d 100644 --- a/cocos/core/animation/tracks/untyped-track.ts +++ b/cocos/core/animation/tracks/untyped-track.ts @@ -89,6 +89,8 @@ export class UntypedTrack extends Track { break; case 'vec2': case 'vec3': case 'vec4': { const track = new VectorTrack(); + track.path = this.path; + track.proxy = this.proxy; track.componentsCount = kind === 'vec2' ? 2 : kind === 'vec3' ? 3 : 4; const [x, y, z, w] = track.channels(); switch (kind) { From 36a315c464d2ba0559c6c7a8936bdb43afbacefb Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Wed, 21 Jul 2021 10:28:45 +0800 Subject: [PATCH 25/35] Rename .interpMode -> .interpolationMode RealInterpMode -> RealInterpolationMode QuaternionInterpMode -> QuaternionInterpolationMode ExtrapMode -> ExtrapolationMode .preExtrap -> .preExtrapolation .postExtrap -> .postExtrapolation QuaternionCurve -> QuatCurve QuaternionKeyframeValue -> QuatKeyframeValue --- .vscode/cSpell.json | 2 - cocos/core/animation/animation.ts | 2 +- .../animation/compression/compressed-data.ts | 20 ++-- cocos/core/animation/legacy-clip-data.ts | 40 +++---- cocos/core/animation/tracks/quat-track.ts | 10 +- cocos/core/animation/tracks/track.ts | 6 +- cocos/core/curves/curve.ts | 104 ++++++++-------- cocos/core/curves/index.ts | 10 +- cocos/core/curves/keys-shared-curves.ts | 28 ++--- cocos/core/curves/quat-curve.ts | 112 +++++++++--------- cocos/core/curves/real-curve-param.ts | 4 +- cocos/core/geometry/curve.ts | 58 ++++----- .../animaion-clip-migration-3.x.test.ts | 58 ++++----- tests/core/geometry/geometry-curve.test.ts | 40 +++---- tests/curves/curve.test.ts | 68 +++++------ tests/curves/key-shared-curves.test.ts | 76 ++++++------ tests/curves/quat-curve.test.ts | 38 +++--- .../curves/serialize-and-deserialize-curve.ts | 4 +- 18 files changed, 339 insertions(+), 341 deletions(-) diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json index 9ae4dbbdbff..f427d51190c 100644 --- a/.vscode/cSpell.json +++ b/.vscode/cSpell.json @@ -20,11 +20,9 @@ "endregion", "eventify", "eventified", - "extrap", "forin", "glsl", "grayscale", - "interp", "IGFX", "lerp", "lerpable", diff --git a/cocos/core/animation/animation.ts b/cocos/core/animation/animation.ts index 59ffc9312c2..951edea938f 100644 --- a/cocos/core/animation/animation.ts +++ b/cocos/core/animation/animation.ts @@ -36,7 +36,7 @@ export * from './cubic-spline-value'; export { Track, TrackPath } from './tracks/track'; export { RealTrack } from './tracks/real-track'; export { VectorTrack } from './tracks/vector-track'; -export { QuaternionTrack } from './tracks/quat-track'; +export { QuatTrack } from './tracks/quat-track'; export { ColorTrack } from './tracks/color-track'; export { SizeTrack } from './tracks/size-track'; export { ObjectTrack } from './tracks/object-track'; diff --git a/cocos/core/animation/compression/compressed-data.ts b/cocos/core/animation/compression/compressed-data.ts index b17a099167f..af8edba467f 100644 --- a/cocos/core/animation/compression/compressed-data.ts +++ b/cocos/core/animation/compression/compressed-data.ts @@ -1,9 +1,9 @@ -import { QuaternionCurve, RealCurve } from '../../curves'; -import { KeySharedQuaternionCurves, KeySharedRealCurves } from '../../curves/keys-shared-curves'; +import { QuatCurve, RealCurve } from '../../curves'; +import { KeySharedQuatCurves, KeySharedRealCurves } from '../../curves/keys-shared-curves'; import { ccclass, serializable } from '../../data/decorators'; import { Quat, Vec2, Vec3, Vec4 } from '../../math'; import { CLASS_NAME_PREFIX_ANIM } from '../define'; -import { QuaternionTrack } from '../tracks/quat-track'; +import { QuatTrack } from '../tracks/quat-track'; import { RealTrack } from '../tracks/real-track'; import { Binder, RuntimeBinding, TrackBinding, trackBindingTag, TrackPath } from '../tracks/track'; import { VectorTrack } from '../tracks/vector-track'; @@ -49,15 +49,15 @@ export class CompressedData { return true; } - public compressQuatTrack (track: QuaternionTrack) { + public compressQuatTrack (track: QuatTrack) { const curve = track.channel.curve; - const mayBeCompressed = KeySharedQuaternionCurves.allowedForCurve(curve); + const mayBeCompressed = KeySharedQuatCurves.allowedForCurve(curve); if (!mayBeCompressed) { return false; } this._quatTracks.push({ binding: track[trackBindingTag], - pointer: this._addQuaternionCurve(curve), + pointer: this._addQuatCurve(curve), }); return true; } @@ -153,7 +153,7 @@ export class CompressedData { private _tracks: CompressedTrack[] = []; @serializable - private _quatCurves: KeySharedQuaternionCurves[] = []; + private _quatCurves: KeySharedQuatCurves[] = []; @serializable private _quatTracks: CompressedQuatTrack[] = []; @@ -174,12 +174,12 @@ export class CompressedData { }; } - public _addQuaternionCurve (curve: QuaternionCurve): CompressedQuatCurvePointer { + public _addQuatCurve (curve: QuatCurve): CompressedQuatCurvePointer { const times = Array.from(curve.times()); let iKeySharedCurves = this._quatCurves.findIndex((shared) => shared.matchCurve(curve)); if (iKeySharedCurves < 0) { iKeySharedCurves = this._quatCurves.length; - const keySharedCurves = new KeySharedQuaternionCurves(times); + const keySharedCurves = new KeySharedQuatCurves(times); this._quatCurves.push(keySharedCurves); } const iCurve = this._quatCurves[iKeySharedCurves].curveCount; @@ -296,7 +296,7 @@ interface CompressedDataEvalStatus { }>; keysSharedQuatCurvesEvalStatues: Array<{ - curves: KeySharedQuaternionCurves; + curves: KeySharedQuatCurves; result: Quat[]; }>; diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts index 6974c06ef8d..4a418cb9b38 100644 --- a/cocos/core/animation/legacy-clip-data.ts +++ b/cocos/core/animation/legacy-clip-data.ts @@ -5,7 +5,7 @@ import { BezierControlPoints } from './bezier'; import { CompactValueTypeArray } from '../data/utils/compact-value-type-array'; import { serializable } from '../data/decorators'; import { AnimCurve, RatioSampler } from './animation-curve'; -import { QuaternionInterpMode, RealCurve, RealInterpMode, RealKeyframeValue, TangentWeightMode } from '../curves'; +import { QuatInterpolationMode, RealCurve, RealInterpolationMode, RealKeyframeValue, TangentWeightMode } from '../curves'; import { assertIsTrue } from '../data/utils/asserts'; import { Track, TrackPath } from './tracks/track'; import { UntypedTrack } from './tracks/untyped-track'; @@ -15,7 +15,7 @@ import { Color, lerp, Quat, Size, Vec2, Vec3, Vec4 } from '../math'; import { CubicSplineNumberValue, CubicSplineQuatValue, CubicSplineVec2Value, CubicSplineVec3Value, CubicSplineVec4Value } from './cubic-spline-value'; import { ColorTrack } from './tracks/color-track'; import { VectorTrack } from './tracks/vector-track'; -import { QuaternionTrack } from './tracks/quat-track'; +import { QuatTrack } from './tracks/quat-track'; import { ObjectTrack } from './tracks/object-track'; import { SizeTrack } from './tracks/size-track'; import { EasingMethod } from '../curves/curve'; @@ -265,9 +265,9 @@ export class AnimationClipLegacyData { newTracks.push(track); realCurve = track.channel.curve; } - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const interpolationMethod = interpolate ? RealInterpolationMode.LINEAR : RealInterpolationMode.CONSTANT; realCurve.assignSorted(times, (legacyValues as number[]).map( - (value) => new RealKeyframeValue({ value, interpMode: interpMethod }), + (value) => new RealKeyframeValue({ value, interpolationMode: interpolationMethod }), )); legacyEasingMethodConverter.convert(realCurve); return; @@ -286,8 +286,8 @@ export class AnimationClipLegacyData { installPathAndSetter(track); track.componentsCount = components; const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.channels(); - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); + const interpolationMode = interpolate ? RealInterpolationMode.LINEAR : RealInterpolationMode.CONSTANT; + const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpolationMode }); switch (components) { case 4: w.assignSorted(times, (legacyValues as Vec4plus).map((value) => valueToFrame(value.w))); @@ -309,12 +309,12 @@ export class AnimationClipLegacyData { } case everyInstanceOf(legacyValues, Quat): { assertIsTrue(legacyEasingMethodConverter.nil); - const track = new QuaternionTrack(); + const track = new QuatTrack(); installPathAndSetter(track); - const interpMode = interpolate ? QuaternionInterpMode.SLERP : QuaternionInterpMode.CONSTANT; + const interpolationMode = interpolate ? QuatInterpolationMode.SLERP : QuatInterpolationMode.CONSTANT; track.channel.curve.assignSorted(times, (legacyValues as Quat[]).map((value) => ({ value: Quat.clone(value), - interpMode, + interpolationMode, }))); newTracks.push(track); return; @@ -323,8 +323,8 @@ export class AnimationClipLegacyData { const track = new ColorTrack(); installPathAndSetter(track); const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.channels(); - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); + const interpolationMode = interpolate ? RealInterpolationMode.LINEAR : RealInterpolationMode.CONSTANT; + const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpolationMode }); r.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.r))); legacyEasingMethodConverter.convert(r); g.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.g))); @@ -340,8 +340,8 @@ export class AnimationClipLegacyData { const track = new SizeTrack(); installPathAndSetter(track); const [{ curve: width }, { curve: height }] = track.channels(); - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; - const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpMode: interpMethod }); + const interpolationMode = interpolate ? RealInterpolationMode.LINEAR : RealInterpolationMode.CONSTANT; + const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpolationMode }); width.assignSorted(times, (legacyValues as Size[]).map((value) => valueToFrame(value.width))); legacyEasingMethodConverter.convert(width); height.assignSorted(times, (legacyValues as Size[]).map((value) => valueToFrame(value.height))); @@ -353,12 +353,12 @@ export class AnimationClipLegacyData { assertIsTrue(legacyEasingMethodConverter.nil); const track = new RealTrack(); installPathAndSetter(track); - const interpMethod = interpolate ? RealInterpMode.CUBIC : RealInterpMode.CONSTANT; + const interpolationMode = interpolate ? RealInterpolationMode.CUBIC : RealInterpolationMode.CONSTANT; track.channel.curve.assignSorted(times, (legacyValues as CubicSplineNumberValue[]).map((value) => new RealKeyframeValue({ value: value.dataPoint, leftTangent: value.inTangent, rightTangent: value.outTangent, - interpMode: interpMethod, + interpolationMode, }))); newTracks.push(track); return; @@ -375,12 +375,12 @@ export class AnimationClipLegacyData { installPathAndSetter(track); track.componentsCount = components; const [x, y, z, w] = track.channels(); - const interpMethod = interpolate ? RealInterpMode.LINEAR : RealInterpMode.CONSTANT; + const interpolationMode = interpolate ? RealInterpolationMode.LINEAR : RealInterpolationMode.CONSTANT; const valueToFrame = (value: number, inTangent: number, outTangent: number): RealKeyframeValue => new RealKeyframeValue({ value, leftTangent: inTangent, rightTangent: outTangent, - interpMode: interpMethod, + interpolationMode, }); switch (components) { case 4: @@ -553,9 +553,9 @@ function applyLegacyEasingMethodName ( const keyframeValue = curve.getKeyframeValue(keyframeIndex); const easingMethod = easingMethodNameMap[easingMethodName]; if (easingMethod === EasingMethod.CONSTANT) { - keyframeValue.interpMode = RealInterpMode.CONSTANT; + keyframeValue.interpolationMode = RealInterpolationMode.CONSTANT; } else { - keyframeValue.interpMode = RealInterpMode.LINEAR; + keyframeValue.interpolationMode = RealInterpolationMode.LINEAR; keyframeValue.easingMethod = easingMethod; } } @@ -641,7 +641,7 @@ export function timeBezierToTangents ( const previousTangentWeight = Math.sqrt(t1x * t1x + t1y * t1y) * ONE_THIRD; const nextTangent = t2y / t2x; const nextTangentWeight = Math.sqrt(t2x * t2x + t2y * t2y) * ONE_THIRD; - previousKeyframe.interpMode = RealInterpMode.CUBIC; + previousKeyframe.interpolationMode = RealInterpolationMode.CUBIC; previousKeyframe.tangentWeightMode = ensureRightTangentWeightMode(previousKeyframe.tangentWeightMode); previousKeyframe.rightTangent = previousTangent; previousKeyframe.rightTangentWeight = previousTangentWeight; diff --git a/cocos/core/animation/tracks/quat-track.ts b/cocos/core/animation/tracks/quat-track.ts index d4758b1119b..ca478982575 100644 --- a/cocos/core/animation/tracks/quat-track.ts +++ b/cocos/core/animation/tracks/quat-track.ts @@ -1,13 +1,13 @@ import { ccclass } from 'cc.decorator'; -import { QuaternionCurve } from '../../curves'; +import { QuatCurve } from '../../curves'; import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; import { SingleChannelTrack } from './track'; import { Quat } from '../../math'; -@ccclass(`${CLASS_NAME_PREFIX_ANIM}QuaternionTrack`) -export class QuaternionTrack extends SingleChannelTrack { +@ccclass(`${CLASS_NAME_PREFIX_ANIM}QuatTrack`) +export class QuatTrack extends SingleChannelTrack { protected createCurve () { - return new QuaternionCurve(); + return new QuatCurve(); } public [createEvalSymbol] () { @@ -16,7 +16,7 @@ export class QuaternionTrack extends SingleChannelTrack { } export class QuatTrackEval { - constructor (private _curve: QuaternionCurve) { + constructor (private _curve: QuatCurve) { } diff --git a/cocos/core/animation/tracks/track.ts b/cocos/core/animation/tracks/track.ts index 7c5cd698fc5..727076ab67d 100644 --- a/cocos/core/animation/tracks/track.ts +++ b/cocos/core/animation/tracks/track.ts @@ -1,6 +1,6 @@ import { ccclass, serializable, uniquelyReferenced } from 'cc.decorator'; import type { Component } from '../../components'; -import type { ObjectCurve, QuaternionCurve, RealCurve } from '../../curves'; +import type { ObjectCurve, QuatCurve, RealCurve } from '../../curves'; import { assertIsTrue } from '../../data/utils/asserts'; import { error, warn } from '../../platform'; import { Node } from '../../scene-graph'; @@ -72,7 +72,7 @@ export interface TrackEval { evaluate(time: number, runtimeBinding: RuntimeBinding): unknown; } -export type Curve = RealCurve | QuaternionCurve | ObjectCurve; +export type Curve = RealCurve | QuatCurve | ObjectCurve; @ccclass(`${CLASS_NAME_PREFIX_ANIM}Channel`) export class Channel { @@ -95,7 +95,7 @@ export class Channel { export type RealChannel = Channel; -export type QuaternionChannel = Channel; +export type QuatChannel = Channel; @ccclass(`${CLASS_NAME_PREFIX_ANIM}SingleChannelTrack`) export abstract class SingleChannelTrack extends Track { diff --git a/cocos/core/curves/curve.ts b/cocos/core/curves/curve.ts index 6a2c70fc320..2af108e30e9 100644 --- a/cocos/core/curves/curve.ts +++ b/cocos/core/curves/curve.ts @@ -2,7 +2,7 @@ import { assertIsTrue } from '../data/utils/asserts'; import { approx, lerp, pingPong, repeat } from '../math'; import { KeyframeCurve } from './keyframe-curve'; import { ccclass, serializable, uniquelyReferenced } from '../data/decorators'; -import { RealInterpMode, ExtrapMode, TangentWeightMode } from './real-curve-param'; +import { RealInterpolationMode, ExtrapolationMode, TangentWeightMode } from './real-curve-param'; import { binarySearchEpsilon } from '../algorithm/binary-search'; import { solveCubic } from './solve-cubic'; import { EditorExtendableMixin } from '../data/editor-extendable'; @@ -10,7 +10,7 @@ import { deserializeTag, SerializationContext, SerializationInput, Serialization import { DeserializationContext } from '../data/custom-serializable'; import * as easing from '../animation/easing'; -export { RealInterpMode, ExtrapMode, TangentWeightMode }; +export { RealInterpolationMode, ExtrapolationMode, TangentWeightMode }; export enum EasingMethod { LINEAR, @@ -63,7 +63,7 @@ export enum EasingMethod { @uniquelyReferenced export class RealKeyframeValue { constructor ({ - interpMode, + interpolationMode, tangentWeightMode, value, rightTangent, @@ -77,7 +77,7 @@ export class RealKeyframeValue { this.rightTangentWeight = rightTangentWeight ?? this.rightTangentWeight; this.leftTangent = leftTangent ?? this.leftTangent; this.leftTangentWeight = leftTangentWeight ?? this.leftTangentWeight; - this.interpMode = interpMode ?? this.interpMode; + this.interpolationMode = interpolationMode ?? this.interpolationMode; this.tangentWeightMode = tangentWeightMode ?? this.tangentWeightMode; this.easingMethod = easingMethod ?? this.easingMethod; } @@ -86,7 +86,7 @@ export class RealKeyframeValue { * Interpolation method used for this keyframe. */ @serializable - public interpMode = RealInterpMode.LINEAR; + public interpolationMode = RealInterpolationMode.LINEAR; /** * Tangent weight mode. @@ -147,27 +147,27 @@ export class RealCurve extends EditorExtendableMixin lastTime) { // Overflow - const { _postExtrap: postExtrap } = this; + const { _postExtrapolation: postExtrapolation } = this; const preFrame = values[nFrames - 1]; - if (postExtrap === ExtrapMode.CLAMP || nFrames < 2) { + if (postExtrapolation === ExtrapolationMode.CLAMP || nFrames < 2) { return preFrame.value; } - switch (postExtrap) { - case ExtrapMode.LINEAR: + switch (postExtrapolation) { + case ExtrapolationMode.LINEAR: return linearTrend(lastTime, preFrame.value, times[nFrames - 2], values[nFrames - 2].value, time); - case ExtrapMode.LOOP: + case ExtrapolationMode.LOOP: time = wrapRepeat(time, firstTime, lastTime); break; - case ExtrapMode.PING_PONG: + case ExtrapolationMode.PING_PONG: time = wrapPingPong(time, firstTime, lastTime); break; default: @@ -296,8 +296,8 @@ export class RealCurve extends EditorExtendableMixin value.interpMode === RealInterpMode.LINEAR); + return curve.postExtrapolation === ExtrapolationMode.CLAMP + && curve.preExtrapolation === ExtrapolationMode.CLAMP + && Array.from(curve.values()).every((value) => value.interpolationMode === RealInterpolationMode.LINEAR); } get curveCount () { @@ -214,19 +214,19 @@ export class KeySharedRealCurves extends KeysSharedCurves { const cacheQuat1 = new Quat(); const cacheQuat2 = new Quat(); -@ccclass('cc.KeySharedQuaternionCurves') -export class KeySharedQuaternionCurves extends KeysSharedCurves { - public static allowedForCurve (curve: QuaternionCurve) { - return curve.postExtrap === ExtrapMode.CLAMP - && curve.preExtrap === ExtrapMode.CLAMP - && Array.from(curve.values()).every((value) => value.interpMode === QuaternionInterpMode.SLERP); +@ccclass('cc.KeySharedQuatCurves') +export class KeySharedQuatCurves extends KeysSharedCurves { + public static allowedForCurve (curve: QuatCurve) { + return curve.postExtrapolation === ExtrapolationMode.CLAMP + && curve.preExtrapolation === ExtrapolationMode.CLAMP + && Array.from(curve.values()).every((value) => value.interpolationMode === QuatInterpolationMode.SLERP); } get curveCount () { return this._curves.length; } - public matchCurve (curve: QuaternionCurve, EPSILON = 1e-5) { + public matchCurve (curve: QuatCurve, EPSILON = 1e-5) { if (curve.keyFramesCount !== this.keyframesCount) { return false; } @@ -234,7 +234,7 @@ export class KeySharedQuaternionCurves extends KeysSharedCurves { return super.matchTimes(times, EPSILON); } - public addCurve (curve: QuaternionCurve) { + public addCurve (curve: QuatCurve) { assertIsTrue(curve.keyFramesCount === this.keyframesCount); const values = new DefaultFloatArray(curve.keyFramesCount * 4); const nKeyframes = curve.keyFramesCount; diff --git a/cocos/core/curves/quat-curve.ts b/cocos/core/curves/quat-curve.ts index 61e32525f60..2054f57231b 100644 --- a/cocos/core/curves/quat-curve.ts +++ b/cocos/core/curves/quat-curve.ts @@ -1,7 +1,7 @@ import { assertIsTrue } from '../data/utils/asserts'; import { IQuatLike, pingPong, Quat, repeat } from '../math'; import { KeyframeCurve } from './keyframe-curve'; -import { ExtrapMode } from './curve'; +import { ExtrapolationMode } from './curve'; import { binarySearchEpsilon } from '../algorithm/binary-search'; import { ccclass, serializable, uniquelyReferenced } from '../data/decorators'; import { deserializeTag, SerializationContext, SerializationInput, SerializationOutput, serializeTag } from '../data'; @@ -9,12 +9,12 @@ import { DeserializationContext } from '../data/custom-serializable'; @ccclass('cc.QuaternionKeyframeValue') @uniquelyReferenced -export class QuaternionKeyframeValue { +export class QuatKeyframeValue { /** * Interpolation method used for this keyframe. */ @serializable - public interpMode: QuaternionInterpMode = QuaternionInterpMode.SLERP; + public interpolationMode: QuatInterpolationMode = QuatInterpolationMode.SLERP; /** * Value of the keyframe. @@ -24,18 +24,18 @@ export class QuaternionKeyframeValue { constructor ({ value, - interpMode, - }: Partial = {}) { + interpolationMode, + }: Partial = {}) { // TODO: shall we normalize it? this.value = value ? Quat.clone(value) : this.value; - this.interpMode = interpMode ?? this.interpMode; + this.interpolationMode = interpolationMode ?? this.interpolationMode; } } /** * The method used for interpolation between values of a keyframe and its next keyframe. */ -export enum QuaternionInterpMode { +export enum QuatInterpolationMode { /** * Perform spherical linear interpolation. */ @@ -59,32 +59,32 @@ export enum QuaternionInterpMode { /** * Quaternion curve. */ -@ccclass('cc.QuaternionCurve') -export class QuaternionCurve extends KeyframeCurve { +@ccclass('cc.QuatCurve') +export class QuatCurve extends KeyframeCurve { /** * Gets or sets the operation should be taken * if input time is less than the time of first keyframe when evaluating this curve. - * Defaults to `ExtrapMode.CLAMP`. + * Defaults to `ExtrapolationMode.CLAMP`. */ - get preExtrap () { - return this._preExtrap; + get preExtrapolation () { + return this._preExtrapolation; } - set preExtrap (value) { - this._preExtrap = value; + set preExtrapolation (value) { + this._preExtrapolation = value; } /** * Gets or sets the operation should be taken * if input time is greater than the time of last keyframe when evaluating this curve. - * Defaults to `ExtrapMode.CLAMP`. + * Defaults to `ExtrapolationMode.CLAMP`. */ - get postExtrap () { - return this._postExtrap; + get postExtrapolation () { + return this._postExtrapolation; } - set postExtrap (value) { - this._postExtrap = value; + set postExtrapolation (value) { + this._postExtrapolation = value; } /** @@ -98,8 +98,8 @@ export class QuaternionCurve extends KeyframeCurve { const { _times: times, _values: values, - _postExtrap: postExtrap, - _preExtrap: preExtrap, + _postExtrapolation: postExtrapolation, + _preExtrapolation: preExtrapolation, } = this; const nFrames = times.length; @@ -112,28 +112,28 @@ export class QuaternionCurve extends KeyframeCurve { if (time < firstTime) { // Underflow const preValue = values[0]; - switch (preExtrap) { - case ExtrapMode.LOOP: + switch (preExtrapolation) { + case ExtrapolationMode.LOOP: time = firstTime + repeat(time - firstTime, lastTime - firstTime); break; - case ExtrapMode.PING_PONG: + case ExtrapolationMode.PING_PONG: time = firstTime + pingPong(time - firstTime, lastTime - firstTime); break; - case ExtrapMode.CLAMP: + case ExtrapolationMode.CLAMP: default: return Quat.copy(quat, preValue.value); } } else if (time > lastTime) { // Overflow const preValue = values[nFrames - 1]; - switch (postExtrap) { - case ExtrapMode.LOOP: + switch (postExtrapolation) { + case ExtrapolationMode.LOOP: time = firstTime + repeat(time - firstTime, lastTime - firstTime); break; - case ExtrapMode.PING_PONG: + case ExtrapolationMode.PING_PONG: time = firstTime + pingPong(time - firstTime, lastTime - firstTime); break; - case ExtrapMode.CLAMP: + case ExtrapolationMode.CLAMP: default: return Quat.copy(quat, preValue.value); } @@ -156,11 +156,11 @@ export class QuaternionCurve extends KeyframeCurve { const dt = nextTime - preTime; const ratio = (time - preTime) / dt; - switch (preValue.interpMode) { + switch (preValue.interpolationMode) { default: - case QuaternionInterpMode.CONSTANT: + case QuatInterpolationMode.CONSTANT: return Quat.copy(quat, preValue.value); - case QuaternionInterpMode.SLERP: + case QuatInterpolationMode.SLERP: return Quat.slerp(quat, preValue.value, nextValue.value, ratio); } } @@ -171,9 +171,9 @@ export class QuaternionCurve extends KeyframeCurve { * @param value Value of the keyframe. * @returns The index to the new keyframe. */ - public addKeyFrame (time: number, value: IQuatLike | QuaternionKeyframeValue): number { - const keyframeValue = value instanceof QuaternionKeyframeValue - ? value : new QuaternionKeyframeValue({ value }); + public addKeyFrame (time: number, value: IQuatLike | QuatKeyframeValue): number { + const keyframeValue = value instanceof QuatKeyframeValue + ? value : new QuatKeyframeValue({ value }); return super.addKeyFrame(time, keyframeValue); } @@ -188,17 +188,17 @@ export class QuaternionCurve extends KeyframeCurve { _values: keyframeValues, } = this; - let interpModeRepeated = true; + let interpolationModeRepeated = true; keyframeValues.forEach((keyframeValue, _index, [firstKeyframeValue]) => { // Values are unlikely to be unified. - if (interpModeRepeated - && keyframeValue.interpMode !== firstKeyframeValue.interpMode) { interpModeRepeated = false; } + if (interpolationModeRepeated + && keyframeValue.interpolationMode !== firstKeyframeValue.interpolationMode) { interpolationModeRepeated = false; } }); const nKeyframes = times.length; const nFrames = nKeyframes; - const interpModesSize = INTERP_MODE_BYTES * (interpModeRepeated ? 1 : nFrames); + const interpolationModesSize = INTERPOLATION_MODE_BYTES * (interpolationModeRepeated ? 1 : nFrames); let dataSize = 0; dataSize += ( @@ -206,7 +206,7 @@ export class QuaternionCurve extends KeyframeCurve { + FRAME_COUNT_BYTES + TIME_BYTES * nFrames + VALUE_BYTES * 4 * nFrames - + interpModesSize + + interpolationModesSize + 0 ); @@ -215,7 +215,7 @@ export class QuaternionCurve extends KeyframeCurve { // Flags let flags = 0; - if (interpModeRepeated) { flags |= KeyframeValueFlagMask.INTERP_MODE; } + if (interpolationModeRepeated) { flags |= KeyframeValueFlagMask.INTERPOLATION_MODE; } dataView.setUint32(P, flags, true); P += FLAGS_BYTES; // Frame count @@ -235,12 +235,12 @@ export class QuaternionCurve extends KeyframeCurve { P += VALUE_BYTES * 4 * nFrames; // Frame values - const INTERP_MODES_START = P; P += interpModesSize; - let pInterpMode = INTERP_MODES_START; - keyframeValues.forEach(({ interpMode }) => { - dataView.setUint8(pInterpMode, interpMode); + const INTERPOLATION_MODES_START = P; P += interpolationModesSize; + let pInterpolationMode = INTERPOLATION_MODES_START; + keyframeValues.forEach(({ interpolationMode }) => { + dataView.setUint8(pInterpolationMode, interpolationMode); - if (!interpModeRepeated) { pInterpMode += INTERP_MODE_BYTES; } + if (!interpolationModeRepeated) { pInterpolationMode += INTERPOLATION_MODE_BYTES; } }); const bytes = new Uint8Array(dataView.buffer); @@ -260,7 +260,7 @@ export class QuaternionCurve extends KeyframeCurve { // Flags const flags = dataView.getUint32(P, true); P += FLAGS_BYTES; - const interpModeRepeated = flags & KeyframeValueFlagMask.INTERP_MODE; + const interpolationModeRepeated = flags & KeyframeValueFlagMask.INTERPOLATION_MODE; // Frame count const nFrames = dataView.getUint32(P, true); P += FRAME_COUNT_BYTES; @@ -272,7 +272,7 @@ export class QuaternionCurve extends KeyframeCurve { // Frame values const P_VALUES = P; P += VALUE_BYTES * 4 * nFrames; - let pInterpModes = P; + let pInterpolationModes = P; const keyframeValues = Array.from({ length: nFrames }, (_, index) => { const pQuat = P_VALUES + VALUE_BYTES * 4 * index; @@ -280,12 +280,12 @@ export class QuaternionCurve extends KeyframeCurve { const y = dataView.getFloat32(pQuat + VALUE_BYTES * 1, true); const z = dataView.getFloat32(pQuat + VALUE_BYTES * 2, true); const w = dataView.getFloat32(pQuat + VALUE_BYTES * 3, true); - const keyframeValue = new QuaternionKeyframeValue({ + const keyframeValue = new QuatKeyframeValue({ value: { x, y, z, w }, - interpMode: dataView.getUint8(pInterpModes), + interpolationMode: dataView.getUint8(pInterpolationModes), }); - if (!interpModeRepeated) { - pInterpModes += INTERP_MODE_BYTES; + if (!interpolationModeRepeated) { + pInterpolationModes += INTERPOLATION_MODE_BYTES; } return keyframeValue; }); @@ -296,18 +296,18 @@ export class QuaternionCurve extends KeyframeCurve { // Always sorted by time @serializable - private _preExtrap: ExtrapMode = ExtrapMode.CLAMP; + private _preExtrapolation: ExtrapolationMode = ExtrapolationMode.CLAMP; @serializable - private _postExtrap: ExtrapMode = ExtrapMode.CLAMP; + private _postExtrapolation: ExtrapolationMode = ExtrapolationMode.CLAMP; } enum KeyframeValueFlagMask { - INTERP_MODE = 1 << 0, + INTERPOLATION_MODE = 1 << 0, } const FLAGS_BYTES = 1; const FRAME_COUNT_BYTES = 4; const TIME_BYTES = 4; const VALUE_BYTES = 4; -const INTERP_MODE_BYTES = 1; +const INTERPOLATION_MODE_BYTES = 1; diff --git a/cocos/core/curves/real-curve-param.ts b/cocos/core/curves/real-curve-param.ts index 9e4080c65f3..45d0259d257 100644 --- a/cocos/core/curves/real-curve-param.ts +++ b/cocos/core/curves/real-curve-param.ts @@ -1,7 +1,7 @@ /** * The method used for interpolation between values of a keyframe and its next keyframe. */ -export enum RealInterpMode { +export enum RealInterpolationMode { /** * Perform linear interpolation. */ @@ -23,7 +23,7 @@ export enum RealInterpMode { * if input time is underflow(less than the the first frame time) or * overflow(greater than the last frame time) when evaluating an animation curve. */ -export enum ExtrapMode { +export enum ExtrapolationMode { /** * Compute the result * according to the first two frame's linear trend in the case of underflow and diff --git a/cocos/core/geometry/curve.ts b/cocos/core/geometry/curve.ts index 27c270cb131..aace4c8a774 100644 --- a/cocos/core/geometry/curve.ts +++ b/cocos/core/geometry/curve.ts @@ -31,7 +31,7 @@ import { CCClass } from '../data/class'; import { clamp, inverseLerp, pingPong, repeat } from '../math/utils'; import { WrapModeMask } from '../animation/types'; -import { ExtrapMode, RealCurve, RealInterpMode, RealKeyframeValue } from '../curves'; +import { ExtrapolationMode, RealCurve, RealInterpolationMode, RealKeyframeValue } from '../curves'; import { ccclass, serializable } from '../data/decorators'; const LOOK_FORWARD = 3; @@ -149,7 +149,7 @@ export class AnimationCurve { this._curve.assignSorted(value.map((legacyCurve) => [ legacyCurve.time, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, value: legacyCurve.value, leftTangent: legacyCurve.inTangent, rightTangent: legacyCurve.outTangent, @@ -164,11 +164,11 @@ export class AnimationCurve { * 当采样时间超出左端时采用的循环模式[[WrapMode]]。 */ get preWrapMode () { - return toLegacyWrapMode(this._curve.preExtrap); + return toLegacyWrapMode(this._curve.preExtrapolation); } set preWrapMode (value) { - this._curve.preExtrap = fromLegacyWrapMode(value); + this._curve.preExtrapolation = fromLegacyWrapMode(value); } /** @@ -178,11 +178,11 @@ export class AnimationCurve { * 当采样时间超出右端时采用的循环模式[[WrapMode]]。 */ get postWrapMode () { - return toLegacyWrapMode(this._curve.postExtrap); + return toLegacyWrapMode(this._curve.postExtrapolation); } set postWrapMode (value) { - this._curve.postExtrap = fromLegacyWrapMode(value); + this._curve.postExtrapolation = fromLegacyWrapMode(value); } private cachedKey: OptimizedKey; @@ -197,16 +197,16 @@ export class AnimationCurve { } else { const curve = new RealCurve(); this._curve = curve; - curve.preExtrap = ExtrapMode.LOOP; - curve.postExtrap = ExtrapMode.CLAMP; + curve.preExtrapolation = ExtrapolationMode.LOOP; + curve.postExtrapolation = ExtrapolationMode.CLAMP; if (!keyFrames) { curve.assignSorted([ - [0.0, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 1.0 })], - [1.0, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 1.0 })], + [0.0, new RealKeyframeValue({ interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 })], + [1.0, new RealKeyframeValue({ interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 })], ]); } else { curve.assignSorted(keyFrames.map((legacyKeyframe) => [legacyKeyframe.time, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, value: legacyKeyframe.value, leftTangent: legacyKeyframe.inTangent, rightTangent: legacyKeyframe.outTangent, @@ -228,7 +228,7 @@ export class AnimationCurve { this._curve.clear(); } else { this._curve.addKeyFrame(keyFrame.time, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, value: keyFrame.value, leftTangent: keyFrame.inTangent, rightTangent: keyFrame.outTangent, @@ -256,17 +256,17 @@ export class AnimationCurve { const nKeyframes = curve.keyFramesCount; const lastKeyframeIndex = nKeyframes - 1; let wrappedTime = time; - const extrapMode = time < 0 ? curve.preExtrap : curve.postExtrap; + const extrapolationMode = time < 0 ? curve.preExtrapolation : curve.postExtrapolation; const startTime = curve.getKeyframeTime(0); const endTime = curve.getKeyframeTime(lastKeyframeIndex); - switch (extrapMode) { - case ExtrapMode.LOOP: + switch (extrapolationMode) { + case ExtrapolationMode.LOOP: wrappedTime = repeat(time - startTime, endTime - startTime) + startTime; break; - case ExtrapMode.PING_PONG: + case ExtrapolationMode.PING_PONG: wrappedTime = pingPong(time - startTime, endTime - startTime) + startTime; break; - case ExtrapMode.CLAMP: + case ExtrapolationMode.CLAMP: default: wrappedTime = clamp(time, startTime, endTime); break; @@ -349,24 +349,24 @@ export class AnimationCurve { } } -function fromLegacyWrapMode (legacyWrapMode: WrapModeMask): ExtrapMode { +function fromLegacyWrapMode (legacyWrapMode: WrapModeMask): ExtrapolationMode { switch (legacyWrapMode) { default: case WrapModeMask.Default: case WrapModeMask.Normal: - case WrapModeMask.Clamp: return ExtrapMode.CLAMP; - case WrapModeMask.PingPong: return ExtrapMode.PING_PONG; - case WrapModeMask.Loop: return ExtrapMode.LOOP; + case WrapModeMask.Clamp: return ExtrapolationMode.CLAMP; + case WrapModeMask.PingPong: return ExtrapolationMode.PING_PONG; + case WrapModeMask.Loop: return ExtrapolationMode.LOOP; } } -function toLegacyWrapMode (extrapMode: ExtrapMode): WrapModeMask { - switch (extrapMode) { +function toLegacyWrapMode (extrapolationMode: ExtrapolationMode): WrapModeMask { + switch (extrapolationMode) { default: - case ExtrapMode.LINEAR: - case ExtrapMode.CLAMP: return WrapModeMask.Clamp; - case ExtrapMode.PING_PONG: return WrapModeMask.PingPong; - case ExtrapMode.LOOP: return WrapModeMask.Loop; + case ExtrapolationMode.LINEAR: + case ExtrapolationMode.CLAMP: return WrapModeMask.Clamp; + case ExtrapolationMode.PING_PONG: return WrapModeMask.PingPong; + case ExtrapolationMode.LOOP: return WrapModeMask.Loop; } } @@ -376,8 +376,8 @@ function toLegacyWrapMode (extrapMode: ExtrapMode): WrapModeMask { export function constructLegacyCurveAndConvert () { const curve = new RealCurve(); curve.assignSorted([ - [0.0, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 1.0 })], - [1.0, new RealKeyframeValue({ interpMode: RealInterpMode.CUBIC, value: 1.0 })], + [0.0, new RealKeyframeValue({ interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 })], + [1.0, new RealKeyframeValue({ interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 })], ]); return curve; } diff --git a/tests/animation/animaion-clip-migration-3.x.test.ts b/tests/animation/animaion-clip-migration-3.x.test.ts index 498086cbd29..c30666b7178 100644 --- a/tests/animation/animaion-clip-migration-3.x.test.ts +++ b/tests/animation/animaion-clip-migration-3.x.test.ts @@ -1,12 +1,12 @@ import { SpriteFrame } from "../../cocos/2d/assets"; -import { math, RealInterpMode } from "../../cocos/core"; +import { math, RealInterpolationMode } from "../../cocos/core"; import { AnimationClip, animation, BezierControlPoints, bezierByTime } from "../../cocos/core/animation"; import { ColorTrack, IValueProxyFactory, RealTrack, Track, TrackPath, VectorTrack } from "../../cocos/core/animation/animation"; import { LegacyClipCurve, LegacyCommonTarget, LegacyEasingMethod, timeBezierToTangents } from "../../cocos/core/animation/legacy-clip-data"; import { ComponentPath, HierarchyPath, ICustomTargetPath, TargetPath } from "../../cocos/core/animation/target-path"; import { RealChannel } from "../../cocos/core/animation/tracks/track"; import { UntypedTrack } from "../../cocos/core/animation/tracks/untyped-track"; -import { EasingMethod, ExtrapMode, RealCurve, RealKeyframeValue, TangentWeightMode } from "../../cocos/core/curves/curve"; +import { EasingMethod, ExtrapolationMode, RealCurve, RealKeyframeValue, TangentWeightMode } from "../../cocos/core/curves/curve"; class ValueProxyFactorFoo implements IValueProxyFactory { forTarget(_target: any): animation.IValueProxy { @@ -209,9 +209,9 @@ describe('Animation Clip Migration 3.x', () => { const INTERPOLATE_FALSE_CURVE_INDEX = 1; expect(clip.tracksCount).toBe(2); test.each([ - [true, [INTERPOLATE_TRUE_CURVE_INDEX, RealInterpMode.LINEAR]], - [false, [INTERPOLATE_FALSE_CURVE_INDEX, RealInterpMode.CONSTANT]], - ])(`with .interpolate: %s`, (_interpolate, [trackIndex, interpMode]) => { + [true, [INTERPOLATE_TRUE_CURVE_INDEX, RealInterpolationMode.LINEAR]], + [false, [INTERPOLATE_FALSE_CURVE_INDEX, RealInterpolationMode.CONSTANT]], + ])(`with .interpolate: %s`, (_interpolate, [trackIndex, interpolationMode]) => { const track = clip.getTrack(trackIndex); expect(track).toBeInstanceOf(expectedTrackType); const channels = Array.from(track.channels()); @@ -229,8 +229,8 @@ describe('Animation Clip Migration 3.x', () => { }; for (let iChannel = 0; iChannel < numberOfValueTypeComponents; ++iChannel) { const { curve } = channels[iChannel] as RealChannel; - expect(curve.preExtrap).toBe(ExtrapMode.CLAMP); - expect(curve.postExtrap).toBe(ExtrapMode.CLAMP); + expect(curve.preExtrapolation).toBe(ExtrapolationMode.CLAMP); + expect(curve.postExtrapolation).toBe(ExtrapolationMode.CLAMP); // Each curve's times are obtained from original times expect(Array.from(curve.times())).toStrictEqual(times); const valuesAtChannel = valueType === Number @@ -238,7 +238,7 @@ describe('Animation Clip Migration 3.x', () => { : values.map((value) => (value as Record)[getComponentNameOfChannel(iChannel)]); expect(Array.from(curve.values())).toStrictEqual(valuesAtChannel.map((value) => new RealKeyframeValue({ value, - interpMode, + interpolationMode, }))); } }); @@ -265,11 +265,11 @@ describe('Animation Clip Migration 3.x', () => { const [{ curve: width }, { curve: height }] = track.channels(); expect(Array.from(width.times())).toStrictEqual([0.0, 0.2, 0.8]); expect(Array.from(width.values())).toStrictEqual( - createRealKeyframesWithoutTangent([10.8, 20, 30], RealInterpMode.LINEAR), + createRealKeyframesWithoutTangent([10.8, 20, 30], RealInterpolationMode.LINEAR), ); expect(Array.from(height.times())).toStrictEqual([0.0, 0.2, 0.8]); expect(Array.from(height.values())).toStrictEqual( - createRealKeyframesWithoutTangent([-1.3, 50, 60], RealInterpMode.LINEAR), + createRealKeyframesWithoutTangent([-1.3, 50, 60], RealInterpolationMode.LINEAR), ); }); @@ -381,7 +381,7 @@ describe('Animation Clip Migration 3.x', () => { true, ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.5]); - expect(Array.from(curve.values())).toStrictEqual(createRealKeyframesWithoutTangent([1, 3, 5], RealInterpMode.LINEAR)); + expect(Array.from(curve.values())).toStrictEqual(createRealKeyframesWithoutTangent([1, 3, 5], RealInterpolationMode.LINEAR)); }); describe(`Specified through ".easingMethod"`, () => { @@ -394,7 +394,7 @@ describe('Animation Clip Migration 3.x', () => { true, ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); - expect(Array.from(curve.values())).toStrictEqual(createRealKeyframesWithoutTangent([1, 3, 5], RealInterpMode.LINEAR)); + expect(Array.from(curve.values())).toStrictEqual(createRealKeyframesWithoutTangent([1, 3, 5], RealInterpolationMode.LINEAR)); }); test(`Time bezier`, () => { @@ -407,13 +407,13 @@ describe('Animation Clip Migration 3.x', () => { ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.RIGHT, value: 1, rightTangent: 14.999999999999998, rightTangentWeight: 0.6013318551349163, }), new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.BOTH, value: 3, leftTangent: 8.333333333333334, @@ -421,7 +421,7 @@ describe('Animation Clip Migration 3.x', () => { rightTangent: 5.999999999999998, rightTangentWeight: 0.6082762530298218, }), new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, + interpolationMode: RealInterpolationMode.LINEAR, value: 5, tangentWeightMode: TangentWeightMode.LEFT, leftTangent: 3.3333333333333335, @@ -439,13 +439,13 @@ describe('Animation Clip Migration 3.x', () => { ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ - interpMode: RealInterpMode.CONSTANT, + interpolationMode: RealInterpolationMode.CONSTANT, value: 1, }), new RealKeyframeValue({ - interpMode: RealInterpMode.CONSTANT, + interpolationMode: RealInterpolationMode.CONSTANT, value: 3, }), new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, // Last frame never converted + interpolationMode: RealInterpolationMode.LINEAR, // Last frame never converted value: 5, })]); }); @@ -460,13 +460,13 @@ describe('Animation Clip Migration 3.x', () => { ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, + interpolationMode: RealInterpolationMode.LINEAR, value: 1, }), new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, + interpolationMode: RealInterpolationMode.LINEAR, value: 3, }), new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, + interpolationMode: RealInterpolationMode.LINEAR, value: 5, })]); }); @@ -481,15 +481,15 @@ describe('Animation Clip Migration 3.x', () => { ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, + interpolationMode: RealInterpolationMode.LINEAR, value: 1, easingMethod: EasingMethod.CUBIC_IN_OUT, }), new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, + interpolationMode: RealInterpolationMode.LINEAR, value: 3, easingMethod: EasingMethod.CUBIC_IN_OUT, }), new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, // Last frame never converted + interpolationMode: RealInterpolationMode.LINEAR, // Last frame never converted value: 5, })]); }); @@ -511,16 +511,16 @@ describe('Animation Clip Migration 3.x', () => { ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.5]); expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, + interpolationMode: RealInterpolationMode.LINEAR, value: 1, }), new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.RIGHT, value: 3, rightTangent: 14.999999999999996, rightTangentWeight: 0.6013318551349163, }), new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, + interpolationMode: RealInterpolationMode.LINEAR, value: 5, tangentWeightMode: TangentWeightMode.LEFT, leftTangent: 8.333333333333332, @@ -603,11 +603,11 @@ function createClipWithLegacyData ({ return clip; } -function createRealKeyframesWithoutTangent (values: number[], interpMode: RealInterpMode): RealKeyframeValue[] { +function createRealKeyframesWithoutTangent (values: number[], interpolationMode: RealInterpolationMode): RealKeyframeValue[] { return values.map((value) => { return new RealKeyframeValue({ value, - interpMode, + interpolationMode, }); }); } \ No newline at end of file diff --git a/tests/core/geometry/geometry-curve.test.ts b/tests/core/geometry/geometry-curve.test.ts index 308a1993c5f..a799ad74cf2 100644 --- a/tests/core/geometry/geometry-curve.test.ts +++ b/tests/core/geometry/geometry-curve.test.ts @@ -1,5 +1,5 @@ import { WrapModeMask } from '../../../cocos/core/animation/types'; -import { ExtrapMode, RealCurve, RealInterpMode, RealKeyframeValue, TangentWeightMode } from '../../../cocos/core/curves'; +import { ExtrapolationMode, RealCurve, RealInterpolationMode, RealKeyframeValue, TangentWeightMode } from '../../../cocos/core/curves'; import { AnimationCurve, Keyframe } from '../../../cocos/core/geometry/curve'; describe('geometry.AnimationCurve', () => { @@ -28,19 +28,19 @@ describe('geometry.AnimationCurve', () => { realCurve.assignSorted([ // Non weighted tangent [0.1, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, value: 0.1, leftTangent: 0.2, rightTangent: 0.3, })], // Non cubic keyframe [0.2, new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, + interpolationMode: RealInterpolationMode.LINEAR, value: 0.1, })], // Weighted tangent [0.3, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, value: 0.1, leftTangent: 0.2, rightTangent: 0.3, @@ -59,14 +59,14 @@ describe('geometry.AnimationCurve', () => { }); test.each([ - { extrapMode: ExtrapMode.LOOP, expected: WrapModeMask.Loop }, - { extrapMode: ExtrapMode.PING_PONG, expected: WrapModeMask.PingPong }, - { extrapMode: ExtrapMode.CLAMP, expected: WrapModeMask.Clamp }, - { extrapMode: ExtrapMode.LINEAR, expected: WrapModeMask.Clamp }, - ])(`new AnimationCurve(realCurve)(INTERNAL): conversion of extrapolation mode $extrapMode`, ({ extrapMode, expected }) => { + { extrapolationMode: ExtrapolationMode.LOOP, expected: WrapModeMask.Loop }, + { extrapolationMode: ExtrapolationMode.PING_PONG, expected: WrapModeMask.PingPong }, + { extrapolationMode: ExtrapolationMode.CLAMP, expected: WrapModeMask.Clamp }, + { extrapolationMode: ExtrapolationMode.LINEAR, expected: WrapModeMask.Clamp }, + ])(`new AnimationCurve(realCurve)(INTERNAL): conversion of extrapolation mode $extrapolationMode`, ({ extrapolationMode, expected }) => { const realCurve = new RealCurve(); - realCurve.preExtrap = extrapMode; - realCurve.postExtrap = extrapMode; + realCurve.preExtrapolation = extrapolationMode; + realCurve.postExtrapolation = extrapolationMode; const geometryCurve = new AnimationCurve(realCurve); expect(geometryCurve.preWrapMode).toStrictEqual(expected); expect(geometryCurve.postWrapMode).toStrictEqual(expected); @@ -74,19 +74,19 @@ describe('geometry.AnimationCurve', () => { }); test.each([ - { wrapMode: WrapModeMask.Clamp, extrapMode: ExtrapMode.CLAMP, }, - { wrapMode: WrapModeMask.Loop, extrapMode: ExtrapMode.LOOP, }, - { wrapMode: WrapModeMask.PingPong, extrapMode: ExtrapMode.PING_PONG, }, - ])(`Wrap mode $wrapMode`, ({ wrapMode, extrapMode }) => { + { wrapMode: WrapModeMask.Clamp, extrapolationMode: ExtrapolationMode.CLAMP, }, + { wrapMode: WrapModeMask.Loop, extrapolationMode: ExtrapolationMode.LOOP, }, + { wrapMode: WrapModeMask.PingPong, extrapolationMode: ExtrapolationMode.PING_PONG, }, + ])(`Wrap mode $wrapMode`, ({ wrapMode, extrapolationMode }) => { const curve = new AnimationCurve(); curve.preWrapMode = wrapMode; expect(curve.preWrapMode).toStrictEqual(wrapMode); - expect(curve._internalCurve.preExtrap).toStrictEqual(extrapMode); + expect(curve._internalCurve.preExtrapolation).toStrictEqual(extrapolationMode); curve.postWrapMode = wrapMode; expect(curve.postWrapMode).toStrictEqual(wrapMode); - expect(curve._internalCurve.postExtrap).toStrictEqual(extrapMode); + expect(curve._internalCurve.postExtrapolation).toStrictEqual(extrapolationMode); }); test(`Add key`, () => { @@ -147,19 +147,19 @@ describe('geometry.AnimationCurve', () => { curve._internalCurve.assignSorted([ // Non weighted tangent [0.1, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, value: 0.1, leftTangent: 0.2, rightTangent: 0.3, })], // Non cubic keyframe [0.2, new RealKeyframeValue({ - interpMode: RealInterpMode.LINEAR, + interpolationMode: RealInterpolationMode.LINEAR, value: 0.1, })], // Weighted tangent [0.3, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, value: 0.1, leftTangent: 0.2, rightTangent: 0.3, diff --git a/tests/curves/curve.test.ts b/tests/curves/curve.test.ts index 4bb46542937..4a20522056b 100644 --- a/tests/curves/curve.test.ts +++ b/tests/curves/curve.test.ts @@ -1,7 +1,7 @@ import { toRadian } from '../../cocos/core'; -import { RealCurve, RealInterpMode } from '../../cocos/core/curves'; +import { RealCurve, RealInterpolationMode } from '../../cocos/core/curves'; import { EasingMethod, RealKeyframeValue } from '../../cocos/core/curves/curve'; -import { ExtrapMode, TangentWeightMode } from '../../cocos/core/curves/real-curve-param'; +import { ExtrapolationMode, TangentWeightMode } from '../../cocos/core/curves/real-curve-param'; import { serializeAndDeserialize } from './serialize-and-deserialize-curve'; describe('Curve', () => { @@ -41,15 +41,15 @@ describe('Curve', () => { test('Normal', () => { const curve = new RealCurve(); curve.assignSorted([0.1, 0.2, 0.3], [ - new RealKeyframeValue({ value: 0.4, rightTangent: 0.0, leftTangent: 0.0, interpMode: RealInterpMode.CONSTANT }), - new RealKeyframeValue({ value: 0.5, rightTangent: 0.0, leftTangent: 0.0, interpMode: RealInterpMode.LINEAR }), + new RealKeyframeValue({ value: 0.4, rightTangent: 0.0, leftTangent: 0.0, interpolationMode: RealInterpolationMode.CONSTANT }), + new RealKeyframeValue({ value: 0.5, rightTangent: 0.0, leftTangent: 0.0, interpolationMode: RealInterpolationMode.LINEAR }), new RealKeyframeValue({ value: 0.6, rightTangent: 0.487, rightTangentWeight: 0.2, leftTangent: 0.4598, leftTangentWeight: 0.32, - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.BOTH, easingMethod: EasingMethod.QUAD_OUT, }), @@ -70,9 +70,9 @@ describe('Curve', () => { test('Optimized for constant curve', () => { const curve = new RealCurve(); curve.assignSorted([0.1, 0.2, 0.3], [ - realKeyframeWithoutTangent(0.4, RealInterpMode.CONSTANT), - realKeyframeWithoutTangent(0.5, RealInterpMode.CONSTANT), - realKeyframeWithoutTangent(0.6, RealInterpMode.CONSTANT), + realKeyframeWithoutTangent(0.4, RealInterpolationMode.CONSTANT), + realKeyframeWithoutTangent(0.5, RealInterpolationMode.CONSTANT), + realKeyframeWithoutTangent(0.6, RealInterpolationMode.CONSTANT), ]); compareCurves(serializeAndDeserialize(curve, RealCurve), curve); }); @@ -81,7 +81,7 @@ describe('Curve', () => { test('Default keyframe value', () => { const keyframeValue = new RealKeyframeValue({}); expect(keyframeValue.value).toBe(0.0); - expect(keyframeValue.interpMode).toBe(RealInterpMode.LINEAR); + expect(keyframeValue.interpolationMode).toBe(RealInterpolationMode.LINEAR); expect(keyframeValue.rightTangent).toBe(0.0); expect(keyframeValue.rightTangentWeight).toBe(0.0); expect(keyframeValue.leftTangent).toBe(0.0); @@ -98,8 +98,8 @@ describe('Curve', () => { test('Interpolation mode: constant', () => { const curve = new RealCurve(); curve.assignSorted([ - [0.2, new RealKeyframeValue({ value: 0.7, interpMode: RealInterpMode.CONSTANT, })], - [0.4, new RealKeyframeValue({ value: 0.8, interpMode: RealInterpMode.LINEAR, })], + [0.2, new RealKeyframeValue({ value: 0.7, interpolationMode: RealInterpolationMode.CONSTANT, })], + [0.4, new RealKeyframeValue({ value: 0.8, interpolationMode: RealInterpolationMode.LINEAR, })], ]); expect(curve.evaluate(0.28)).toBe(0.7); }); @@ -107,8 +107,8 @@ describe('Curve', () => { test('Interpolation mode: linear', () => { const curve = new RealCurve(); curve.assignSorted([ - [0.2, new RealKeyframeValue({ value: 0.7, interpMode: RealInterpMode.LINEAR, })], - [0.4, new RealKeyframeValue({ value: 0.8, interpMode: RealInterpMode.CONSTANT, })], + [0.2, new RealKeyframeValue({ value: 0.7, interpolationMode: RealInterpolationMode.LINEAR, })], + [0.4, new RealKeyframeValue({ value: 0.8, interpolationMode: RealInterpolationMode.CONSTANT, })], ]); expect(curve.evaluate(0.28)).toBeCloseTo(0.74); }); @@ -117,13 +117,13 @@ describe('Curve', () => { const curve = new RealCurve(); curve.assignSorted([ [0.2, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.7, rightTangent: Math.tan(toRadian(30.0)), })], [0.4, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.8, leftTangent: Math.tan(toRadian(30.0)), @@ -136,13 +136,13 @@ describe('Curve', () => { const curve = new RealCurve(); curve.assignSorted([ [0.2, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.RIGHT, value: 0.7, rightTangent: Math.tan(toRadian(30.0)), })], [0.4, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.8, leftTangent: Math.tan(toRadian(30.0)), @@ -155,13 +155,13 @@ describe('Curve', () => { const curve = new RealCurve(); curve.assignSorted([ [0.2, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.7, rightTangent: Math.tan(toRadian(30.0)), })], [0.4, new RealKeyframeValue({ - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.LEFT, value: 0.8, leftTangent: Math.tan(toRadian(30.0)), @@ -170,10 +170,10 @@ describe('Curve', () => { expect(curve.evaluate(0.28)).toBeCloseTo(0.74229, 5); }); - test('Extrap mode: clamp', () => { + test('Extrapolation mode: clamp', () => { const curve = new RealCurve(); - curve.preExtrap = ExtrapMode.CLAMP; - curve.postExtrap = ExtrapMode.CLAMP; + curve.preExtrapolation = ExtrapolationMode.CLAMP; + curve.postExtrapolation = ExtrapolationMode.CLAMP; curve.assignSorted([ [0.2, new RealKeyframeValue({ value: 5.0 })], @@ -183,10 +183,10 @@ describe('Curve', () => { expect(curve.evaluate(0.46)).toBeCloseTo(5.0); }); - test('Extrap mode: linear', () => { + test('Extrapolation mode: linear', () => { const curve = new RealCurve(); - curve.preExtrap = ExtrapMode.LINEAR; - curve.postExtrap = ExtrapMode.LINEAR; + curve.preExtrapolation = ExtrapolationMode.LINEAR; + curve.postExtrapolation = ExtrapolationMode.LINEAR; curve.assignSorted([ [0.2, new RealKeyframeValue({ value: 5.0 })], @@ -204,10 +204,10 @@ describe('Curve', () => { expect(curve.evaluate(0.46)).toBeCloseTo(5.0); }); - test('Extrap mode: loop', () => { + test('Extrapolation mode: loop', () => { const curve = new RealCurve(); - curve.preExtrap = ExtrapMode.LOOP; - curve.postExtrap = ExtrapMode.LOOP; + curve.preExtrapolation = ExtrapolationMode.LOOP; + curve.postExtrapolation = ExtrapolationMode.LOOP; curve.assignSorted([ [0.2, new RealKeyframeValue({ value: 5.0 })], @@ -224,10 +224,10 @@ describe('Curve', () => { expect(curve.evaluate(0.46)).toBeCloseTo(5.0); }); - test('Extrap mode: ping-pong', () => { + test('Extrapolation mode: ping-pong', () => { const curve = new RealCurve(); - curve.preExtrap = ExtrapMode.PING_PONG; - curve.postExtrap = ExtrapMode.PING_PONG; + curve.preExtrapolation = ExtrapolationMode.PING_PONG; + curve.postExtrapolation = ExtrapolationMode.PING_PONG; curve.assignSorted([ [0.2, new RealKeyframeValue({ value: 5.0 })], @@ -247,10 +247,10 @@ describe('Curve', () => { }); }); -function realKeyframeWithoutTangent (value: number, interpMode: RealInterpMode = RealInterpMode.LINEAR): RealKeyframeValue { +function realKeyframeWithoutTangent (value: number, interpolationMode: RealInterpolationMode = RealInterpolationMode.LINEAR): RealKeyframeValue { return new RealKeyframeValue({ value, - interpMode, + interpolationMode, }); } @@ -265,7 +265,7 @@ function compareCurves (left: RealCurve, right: RealCurve, numDigits = 2) { expect(leftKeyframeValue.rightTangentWeight).toBeCloseTo(rightKeyframeValue.rightTangentWeight, numDigits); expect(leftKeyframeValue.leftTangent).toBeCloseTo(rightKeyframeValue.leftTangent, numDigits); expect(leftKeyframeValue.leftTangentWeight).toBeCloseTo(rightKeyframeValue.leftTangentWeight, numDigits); - expect(leftKeyframeValue.interpMode).toStrictEqual(rightKeyframeValue.interpMode); + expect(leftKeyframeValue.interpolationMode).toStrictEqual(rightKeyframeValue.interpolationMode); expect(leftKeyframeValue.easingMethod).toStrictEqual(rightKeyframeValue.easingMethod); } } \ No newline at end of file diff --git a/tests/curves/key-shared-curves.test.ts b/tests/curves/key-shared-curves.test.ts index 63a31c572cf..6adb2d243cc 100644 --- a/tests/curves/key-shared-curves.test.ts +++ b/tests/curves/key-shared-curves.test.ts @@ -1,7 +1,7 @@ -import { ExtrapMode, RealCurve, RealInterpMode, RealKeyframeValue } from '../../cocos/core/curves/curve'; -import { KeySharedQuaternionCurves, KeySharedRealCurves } from '../../cocos/core/curves/keys-shared-curves'; -import { QuaternionCurve, QuaternionInterpMode, QuaternionKeyframeValue } from '../../cocos/core/curves/quat-curve'; +import { ExtrapolationMode, RealCurve, RealInterpolationMode, RealKeyframeValue } from '../../cocos/core/curves/curve'; +import { KeySharedQuatCurves, KeySharedRealCurves } from '../../cocos/core/curves/keys-shared-curves'; +import { QuatCurve, QuatInterpolationMode, QuatKeyframeValue } from '../../cocos/core/curves/quat-curve'; import { Quat } from '../../cocos/core/math'; describe('Keys shared real curves', () => { @@ -19,7 +19,7 @@ describe('Keys shared real curves', () => { curve.assignSorted([[0.1, new RealKeyframeValue({ value: 0.1, })]]); - curve.postExtrap = ExtrapMode.LOOP; + curve.postExtrapolation = ExtrapolationMode.LOOP; expect(KeySharedRealCurves.allowedForCurve(curve)).toBe(false); } @@ -28,7 +28,7 @@ describe('Keys shared real curves', () => { curve.assignSorted([[0.1, new RealKeyframeValue({ value: 0.1, })]]); - curve.preExtrap = ExtrapMode.LOOP; + curve.preExtrapolation = ExtrapolationMode.LOOP; expect(KeySharedRealCurves.allowedForCurve(curve)).toBe(false); } @@ -36,7 +36,7 @@ describe('Keys shared real curves', () => { const curve = new RealCurve(); curve.assignSorted([[0.1, new RealKeyframeValue({ value: 0.1, - interpMode: RealInterpMode.CUBIC, + interpolationMode: RealInterpolationMode.CUBIC, })]]); expect(KeySharedRealCurves.allowedForCurve(curve)).toBe(false); } @@ -120,67 +120,67 @@ describe('Keys shared real curves', () => { describe('Keys shared quaternion curves', () => { test('Enabling', () => { { - const curve = new QuaternionCurve(); - curve.assignSorted([[0.1, new QuaternionKeyframeValue({ - interpMode: QuaternionInterpMode.SLERP, + const curve = new QuatCurve(); + curve.assignSorted([[0.1, new QuatKeyframeValue({ + interpolationMode: QuatInterpolationMode.SLERP, value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, })]]); - expect(KeySharedQuaternionCurves.allowedForCurve(curve)).toBe(true); + expect(KeySharedQuatCurves.allowedForCurve(curve)).toBe(true); } { - const curve = new QuaternionCurve(); - curve.assignSorted([[0.1, new QuaternionKeyframeValue({ + const curve = new QuatCurve(); + curve.assignSorted([[0.1, new QuatKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, })]]); - curve.postExtrap = ExtrapMode.LOOP; - expect(KeySharedQuaternionCurves.allowedForCurve(curve)).toBe(false); + curve.postExtrapolation = ExtrapolationMode.LOOP; + expect(KeySharedQuatCurves.allowedForCurve(curve)).toBe(false); } { - const curve = new QuaternionCurve(); - curve.assignSorted([[0.1, new QuaternionKeyframeValue({ + const curve = new QuatCurve(); + curve.assignSorted([[0.1, new QuatKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, })]]); - curve.preExtrap = ExtrapMode.LOOP; - expect(KeySharedQuaternionCurves.allowedForCurve(curve)).toBe(false); + curve.preExtrapolation = ExtrapolationMode.LOOP; + expect(KeySharedQuatCurves.allowedForCurve(curve)).toBe(false); } { - const curve = new QuaternionCurve(); - curve.assignSorted([[0.1, new QuaternionKeyframeValue({ + const curve = new QuatCurve(); + curve.assignSorted([[0.1, new QuatKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, - interpMode: QuaternionInterpMode.CONSTANT, + interpolationMode: QuatInterpolationMode.CONSTANT, })]]); - expect(KeySharedQuaternionCurves.allowedForCurve(curve)).toBe(false); + expect(KeySharedQuatCurves.allowedForCurve(curve)).toBe(false); } }); test('Composite', () => { - const curves1 = new KeySharedQuaternionCurves([0.1, 0.7, 0.8]); + const curves1 = new KeySharedQuatCurves([0.1, 0.7, 0.8]); - const curveMatched = new QuaternionCurve(); + const curveMatched = new QuatCurve(); curveMatched.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, () => - new QuaternionKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + new QuatKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); expect(curves1.matchCurve(curveMatched)).toBe(true); - const curveNonMatched = new QuaternionCurve(); + const curveNonMatched = new QuatCurve(); curveNonMatched.assignSorted([0.1, 0.3, 0.8], Array.from({ length: 3 }, () => - new QuaternionKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + new QuatKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); expect(curves1.matchCurve(curveNonMatched)).toBe(false); }); test('Composite (may be baked)', () => { - const curves1 = new KeySharedQuaternionCurves([0.1, 0.2, 0.3]); + const curves1 = new KeySharedQuatCurves([0.1, 0.2, 0.3]); - const curveMatched = new QuaternionCurve(); + const curveMatched = new QuatCurve(); curveMatched.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, () => - new QuaternionKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + new QuatKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); expect(curves1.matchCurve(curveMatched)).toBe(true); - const curveNonMatched = new QuaternionCurve(); + const curveNonMatched = new QuatCurve(); curveNonMatched.assignSorted([0.2, 0.3, 0.4], Array.from({ length: 3 }, () => - new QuaternionKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + new QuatKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); expect(curves1.matchCurve(curveNonMatched)).toBe(false); }); @@ -191,11 +191,11 @@ describe('Keys shared quaternion curves', () => { ]; test('Evaluate', () => { - const curve = new QuaternionCurve(); + const curve = new QuatCurve(); curve.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, (_, index) => - new QuaternionKeyframeValue({ value: Quat.clone(quaternions[index]) }))); + new QuatKeyframeValue({ value: Quat.clone(quaternions[index]) }))); - const curves = new KeySharedQuaternionCurves(Array.from(curve.times())); + const curves = new KeySharedQuatCurves(Array.from(curve.times())); curves.addCurve(curve); const values = [new Quat()]; const resetAndEval = (time: number) => { @@ -217,11 +217,11 @@ describe('Keys shared quaternion curves', () => { }); test('Evaluate optimized keys', () => { - const curve = new QuaternionCurve(); + const curve = new QuatCurve(); curve.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, (_, index) => - new QuaternionKeyframeValue({ value: Quat.clone(quaternions[index]) }))); + new QuatKeyframeValue({ value: Quat.clone(quaternions[index]) }))); - const curves = new KeySharedQuaternionCurves(Array.from(curve.times())); + const curves = new KeySharedQuatCurves(Array.from(curve.times())); curves.addCurve(curve); const values = [new Quat()]; const resetAndEval = (time: number) => { diff --git a/tests/curves/quat-curve.test.ts b/tests/curves/quat-curve.test.ts index 2c8ce45a0b9..a99cdab651a 100644 --- a/tests/curves/quat-curve.test.ts +++ b/tests/curves/quat-curve.test.ts @@ -1,51 +1,51 @@ -import { Quat, QuaternionCurve, QuaternionInterpMode, QuaternionKeyframeValue } from '../../cocos/core'; +import { Quat, QuatCurve, QuatInterpolationMode, QuatKeyframeValue } from '../../cocos/core'; import { serializeAndDeserialize } from './serialize-and-deserialize-curve'; describe('Curve', () => { test('Evaluate an empty curve', () => { - const curve = new QuaternionCurve(); + const curve = new QuatCurve(); expect(curve.evaluate(12.34)).toStrictEqual(Quat.IDENTITY); }); describe('serialization', () => { test('Normal', () => { - const curve = new QuaternionCurve(); + const curve = new QuatCurve(); curve.assignSorted([0.1, 0.2, 0.3], [ - new QuaternionKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpMode: QuaternionInterpMode.CONSTANT }), - new QuaternionKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpMode: QuaternionInterpMode.SLERP }), - new QuaternionKeyframeValue({ value: { x: 0.9, y: 0.1, z: 0.11, w: 0.12 }, interpMode: QuaternionInterpMode.CONSTANT }), + new QuatKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpolationMode: QuatInterpolationMode.CONSTANT }), + new QuatKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpolationMode: QuatInterpolationMode.SLERP }), + new QuatKeyframeValue({ value: { x: 0.9, y: 0.1, z: 0.11, w: 0.12 }, interpolationMode: QuatInterpolationMode.CONSTANT }), ]); - compareCurves(serializeAndDeserialize(curve, QuaternionCurve), curve); + compareCurves(serializeAndDeserialize(curve, QuatCurve), curve); }); test('Optimized for linear curve', () => { - const curve = new QuaternionCurve(); + const curve = new QuatCurve(); curve.assignSorted([0.1, 0.2], [ - new QuaternionKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpMode: QuaternionInterpMode.SLERP }), - new QuaternionKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpMode: QuaternionInterpMode.SLERP }), + new QuatKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpolationMode: QuatInterpolationMode.SLERP }), + new QuatKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpolationMode: QuatInterpolationMode.SLERP }), ]); - compareCurves(serializeAndDeserialize(curve, QuaternionCurve), curve); + compareCurves(serializeAndDeserialize(curve, QuatCurve), curve); }); test('Optimized for constant curve', () => { - const curve = new QuaternionCurve(); + const curve = new QuatCurve(); curve.assignSorted([0.1, 0.2], [ - new QuaternionKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpMode: QuaternionInterpMode.CONSTANT }), - new QuaternionKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpMode: QuaternionInterpMode.CONSTANT }), + new QuatKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpolationMode: QuatInterpolationMode.CONSTANT }), + new QuatKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpolationMode: QuatInterpolationMode.CONSTANT }), ]); - compareCurves(serializeAndDeserialize(curve, QuaternionCurve), curve); + compareCurves(serializeAndDeserialize(curve, QuatCurve), curve); }); }); test('Default keyframe value', () => { - const keyframeValue = new QuaternionKeyframeValue({}); + const keyframeValue = new QuatKeyframeValue({}); expect(Quat.equals(keyframeValue.value, Quat.IDENTITY)).toBe(true); - expect(keyframeValue.interpMode).toBe(QuaternionInterpMode.SLERP); + expect(keyframeValue.interpolationMode).toBe(QuatInterpolationMode.SLERP); }); }); -function compareCurves (left: QuaternionCurve, right: QuaternionCurve, numDigits = 2) { +function compareCurves (left: QuatCurve, right: QuatCurve, numDigits = 2) { expect(left.keyFramesCount).toBe(right.keyFramesCount); for (let iKeyframe = 0; iKeyframe < left.keyFramesCount; ++iKeyframe) { expect(left.getKeyframeTime(iKeyframe)).toBeCloseTo(right.getKeyframeTime(iKeyframe), numDigits); @@ -55,6 +55,6 @@ function compareCurves (left: QuaternionCurve, right: QuaternionCurve, numDigits expect(leftKeyframeValue.value.y).toBeCloseTo(rightKeyframeValue.value.y, numDigits); expect(leftKeyframeValue.value.z).toBeCloseTo(rightKeyframeValue.value.z, numDigits); expect(leftKeyframeValue.value.w).toBeCloseTo(rightKeyframeValue.value.w, numDigits); - expect(leftKeyframeValue.interpMode).toStrictEqual(rightKeyframeValue.interpMode); + expect(leftKeyframeValue.interpolationMode).toStrictEqual(rightKeyframeValue.interpolationMode); } } \ No newline at end of file diff --git a/tests/curves/serialize-and-deserialize-curve.ts b/tests/curves/serialize-and-deserialize-curve.ts index 9684a01b85a..760d80f64ec 100644 --- a/tests/curves/serialize-and-deserialize-curve.ts +++ b/tests/curves/serialize-and-deserialize-curve.ts @@ -1,9 +1,9 @@ import { deserializeTag, SerializationInput, SerializationOutput, serializeTag } from '../../cocos/core'; import type { RealCurve } from '../../cocos/core/curves/curve'; -import type { QuaternionCurve } from '../../cocos/core/curves/quat-curve'; +import type { QuatCurve } from '../../cocos/core/curves/quat-curve'; -export function serializeAndDeserialize (curve: T, CurveConstructor: new () => T) { +export function serializeAndDeserialize (curve: T, CurveConstructor: new () => T) { class CurveOutput implements SerializationOutput, SerializationInput { public readProperty(name: string): unknown { return this._properties[name]; From 273e5011a3f5a519dd806d6dbed2d79b6a2d9fc8 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Wed, 21 Jul 2021 11:32:32 +0800 Subject: [PATCH 26/35] Update pre/post extrap --- cocos/core/curves/curve.ts | 37 +++++++++------------------------ cocos/core/curves/quat-curve.ts | 29 ++++++-------------------- 2 files changed, 16 insertions(+), 50 deletions(-) diff --git a/cocos/core/curves/curve.ts b/cocos/core/curves/curve.ts index 2af108e30e9..a7b06b5673b 100644 --- a/cocos/core/curves/curve.ts +++ b/cocos/core/curves/curve.ts @@ -149,26 +149,16 @@ export class RealCurve extends EditorExtendableMixin lastTime) { // Overflow - const { _postExtrapolation: postExtrapolation } = this; + const { postExtrapolation } = this; const preFrame = values[nFrames - 1]; if (postExtrapolation === ExtrapolationMode.CLAMP || nFrames < 2) { return preFrame.value; @@ -296,8 +286,8 @@ export class RealCurve extends EditorExtendableMixin { * if input time is less than the time of first keyframe when evaluating this curve. * Defaults to `ExtrapolationMode.CLAMP`. */ - get preExtrapolation () { - return this._preExtrapolation; - } - - set preExtrapolation (value) { - this._preExtrapolation = value; - } + @serializable + public preExtrapolation: ExtrapolationMode = ExtrapolationMode.CLAMP; /** * Gets or sets the operation should be taken * if input time is greater than the time of last keyframe when evaluating this curve. * Defaults to `ExtrapolationMode.CLAMP`. */ - get postExtrapolation () { - return this._postExtrapolation; - } - - set postExtrapolation (value) { - this._postExtrapolation = value; - } + @serializable + public postExtrapolation: ExtrapolationMode = ExtrapolationMode.CLAMP; /** * Evaluates this curve at specified time. @@ -98,8 +88,8 @@ export class QuatCurve extends KeyframeCurve { const { _times: times, _values: values, - _postExtrapolation: postExtrapolation, - _preExtrapolation: preExtrapolation, + postExtrapolation, + preExtrapolation, } = this; const nFrames = times.length; @@ -293,13 +283,6 @@ export class QuatCurve extends KeyframeCurve { this._times = times; this._values = keyframeValues; } - - // Always sorted by time - @serializable - private _preExtrapolation: ExtrapolationMode = ExtrapolationMode.CLAMP; - - @serializable - private _postExtrapolation: ExtrapolationMode = ExtrapolationMode.CLAMP; } enum KeyframeValueFlagMask { From 5c7551657c4d9aeb2762bd9022a1df6967c35ff6 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Wed, 21 Jul 2021 15:32:35 +0800 Subject: [PATCH 27/35] Optimize-1 --- cocos/core/animation/animation-clip.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/cocos/core/animation/animation-clip.ts b/cocos/core/animation/animation-clip.ts index ce121e4374b..1ca730712fa 100644 --- a/cocos/core/animation/animation-clip.ts +++ b/cocos/core/animation/animation-clip.ts @@ -804,9 +804,11 @@ class AnimationClipEvaluation { _exoticAnimationEvaluator: exoticAnimationEvaluator, } = this; - for (const trackEvalStatus of trackEvalStatuses) { - const value = trackEvalStatus.trackEval.evaluate(time, trackEvalStatus.binding); - trackEvalStatus.binding.setValue(value); + const nTrackEvalStatuses = trackEvalStatuses.length; + for (let iTrackEvalStatus = 0; iTrackEvalStatus < nTrackEvalStatuses; ++iTrackEvalStatus) { + const { trackEval, binding } = trackEvalStatuses[iTrackEvalStatus]; + const value = trackEval.evaluate(time, binding); + binding.setValue(value); } if (exoticAnimationEvaluator) { @@ -942,9 +944,11 @@ class RootMotionEvaluation { _trackEvalStatuses: trackEvalStatuses, } = this; - for (const trackEvalStatus of trackEvalStatuses) { - const value = trackEvalStatus.trackEval.evaluate(time, trackEvalStatus.binding); - trackEvalStatus.binding.setValue(value); + const nTrackEvalStatuses = trackEvalStatuses.length; + for (let iTrackEvalStatus = 0; iTrackEvalStatus < nTrackEvalStatuses; ++iTrackEvalStatus) { + const { trackEval, binding } = trackEvalStatuses[iTrackEvalStatus]; + const value = trackEval.evaluate(time, binding); + binding.setValue(value); } this._boneTransform.getTransform(outTransform); From dc2f9ce0bcddfa5386a3b1ef5302987f370abfc3 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Wed, 21 Jul 2021 16:00:27 +0800 Subject: [PATCH 28/35] Optimize-2 --- .../skeletal-animation-state.ts | 3 ++- cocos/core/animation/animation-state.ts | 18 ++++++++++-------- cocos/core/animation/tracks/track.ts | 13 ++++++++++--- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/cocos/3d/skeletal-animation/skeletal-animation-state.ts b/cocos/3d/skeletal-animation/skeletal-animation-state.ts index 32d6414fbfa..fccf1eb3b4a 100644 --- a/cocos/3d/skeletal-animation/skeletal-animation-state.ts +++ b/cocos/3d/skeletal-animation/skeletal-animation-state.ts @@ -175,7 +175,8 @@ export class SkeletalAnimationState extends AnimationState { } } - private _sampleCurvesBaked (ratio: number) { + private _sampleCurvesBaked (time: number) { + const ratio = time / this.duration; const info = this._animInfo!; const curFrame = (ratio * this._frames + 0.5) | 0; if (curFrame === info.data[0]) { return; } diff --git a/cocos/core/animation/animation-state.ts b/cocos/core/animation/animation-state.ts index 050a6257b6f..69216c6658a 100644 --- a/cocos/core/animation/animation-state.ts +++ b/cocos/core/animation/animation-state.ts @@ -479,7 +479,7 @@ export class AnimationState extends Playable { public sample () { const info = this.getWrappedInfo(this.time, this._wrappedInfo); - this._sampleCurves(info.ratio); + this._sampleCurves(info.time); if (!EDITOR || legacyCC.GAME_VIEW) { this._sampleEvents(info); } @@ -510,12 +510,13 @@ export class AnimationState extends Playable { this.emit(EventType.PAUSE, this); } - protected _sampleCurves (ratio: number) { - if (this._poseOutput) { - this._poseOutput.weight = this.weight; + protected _sampleCurves (time: number) { + const { _poseOutput: poseOutput, _clipEval: clipEval } = this; + if (poseOutput) { + poseOutput.weight = this.weight; } - if (this._clipEval) { - this._clipEval.evaluate(this.current); + if (clipEval) { + clipEval.evaluate(time); } } @@ -558,8 +559,9 @@ export class AnimationState extends Playable { let time = this.time % playbackDuration; if (time < 0.0) { time += playbackDuration; } - const ratio = (playbackStart + time) * this._invDuration; - this._sampleCurves(ratio); + const realTime = playbackStart + time; + const ratio = realTime * this._invDuration; + this._sampleCurves(playbackStart + time); if (!EDITOR || legacyCC.GAME_VIEW) { this._sampleEvents(this.getWrappedInfo(this.time, this._wrappedInfo)); diff --git a/cocos/core/animation/tracks/track.ts b/cocos/core/animation/tracks/track.ts index 727076ab67d..eb845e42fe8 100644 --- a/cocos/core/animation/tracks/track.ts +++ b/cocos/core/animation/tracks/track.ts @@ -118,15 +118,22 @@ export abstract class SingleChannelTrack extends Track { public [createEvalSymbol] (_runtimeBinding: RuntimeBinding): TrackEval { const { curve } = this._channel; - return { - evaluate: (time) => curve.evaluate(time), - }; + return new SingleChannelTrackEval(curve); } @serializable private _channel: Channel; } +class SingleChannelTrackEval implements TrackEval { + constructor (private _curve: TCurve) { + } + + public evaluate (time: number) { + return this._curve.evaluate(time); + } +} + export type RuntimeBinding = { setValue(value: unknown): void; From 7e7b18fa78822bd5980123e1923492778f6bf638 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Wed, 21 Jul 2021 19:38:10 +0800 Subject: [PATCH 29/35] Eliminate for-of --- cocos/core/animation/animation-clip.ts | 63 ++++++++++++++------------ 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/cocos/core/animation/animation-clip.ts b/cocos/core/animation/animation-clip.ts index 1ca730712fa..c1fac346ab6 100644 --- a/cocos/core/animation/animation-clip.ts +++ b/cocos/core/animation/animation-clip.ts @@ -231,11 +231,11 @@ export class AnimationClip extends Asset { */ public range () { const range: Range = { min: Infinity, max: -Infinity }; - for (const track of this._tracks) { + this._tracks.forEach((track) => { const trackRange = track.range(); range.min = Math.min(range.min, trackRange.min); range.max = Math.max(range.max, trackRange.max); - } + }); return range; } @@ -325,15 +325,16 @@ export class AnimationClip extends Asset { const step = 1.0 / samples; const animatedJoints = this._collectAnimatedJoints(); + const nAnimatedJoints = animatedJoints.length; const jointsBakeInfo: Record = {}; - for (const joint of animatedJoints) { + animatedJoints.forEach((joint) => { jointsBakeInfo[joint] = { transforms: Array.from({ length: frames }, () => new Mat4()), }; - } + }); const skeletonFrames = animatedJoints.reduce((result, joint) => { result[joint] = new BoneGlobalTransform(); @@ -372,13 +373,15 @@ export class AnimationClip extends Asset { for (let iFrame = 0; iFrame < frames; ++iFrame) { const time = start + step * iFrame; evaluator.evaluate(time); - for (const joint of animatedJoints) { + for (let iAnimatedJoint = 0; iAnimatedJoint < nAnimatedJoints; ++iAnimatedJoint) { + const joint = animatedJoints[iAnimatedJoint]; Mat4.copy( jointsBakeInfo[joint].transforms[iFrame], skeletonFrames[joint].globalTransform, ); } - for (const joint of animatedJoints) { + for (let iAnimatedJoint = 0; iAnimatedJoint < nAnimatedJoints; ++iAnimatedJoint) { + const joint = animatedJoints[iAnimatedJoint]; skeletonFrames[joint].invalidate(); } } @@ -400,19 +403,19 @@ export class AnimationClip extends Asset { public upgradeUntypedTracks (refine: UntypedTrackRefine) { const newTracks: Track[] = []; const removals: Track[] = []; - for (const track of this._tracks) { + this._tracks.forEach((track) => { if (!(track instanceof UntypedTrack)) { - continue; + return; } const newTrack = track.upgrade(refine); if (newTrack) { newTracks.push(newTrack); removals.push(track); } - } - for (const removal of removals) { + }); + removals.forEach((removal) => { array.remove(this._tracks, removal); - } + }); this._tracks.push(...newTracks); } @@ -577,20 +580,20 @@ export class AnimationClip extends Asset { const trackEvalStatues: TrackEvalStatus[] = []; let exoticAnimationEvaluator: ExoticAnimationEvaluator | undefined; - for (const track of this._tracks) { + this._tracks.forEach((track) => { if (rootMotionTrackExcludes.includes(track)) { - continue; + return; } const trackTarget = binder(track[trackBindingTag]); if (!trackTarget) { - continue; + return; } const trackEval = track[createEvalSymbol](trackTarget); trackEvalStatues.push({ binding: trackTarget, trackEval, }); - } + }); if (this._exoticAnimation) { exoticAnimationEvaluator = this._exoticAnimation.createEvaluator(binder); @@ -631,28 +634,28 @@ export class AnimationClip extends Asset { const boneTransform = new BoneTransform(); const rootMotionsTrackEvaluations: TrackEvalStatus[] = []; - for (const track of this._tracks) { + this._tracks.forEach((track) => { const { [trackBindingTag]: trackBinding } = track; const trsPath = trackBinding.parseTrsPath(); if (!trsPath) { - continue; + return; } const bonePath = trsPath.node; if (bonePath !== rootBonePath) { - continue; + return; } rootMotionTrackExcludes.push(track); const property = trsPath.property; const trackTarget = createBoneTransformBinding(boneTransform, property); if (!trackTarget) { - continue; + return; } const trackEval = track[createEvalSymbol](trackTarget); rootMotionsTrackEvaluations.push({ binding: trackTarget, trackEval, }); - } + }); const rootMotionEvaluation = new RootMotionEvaluation( rootBone, this._duration, @@ -725,25 +728,25 @@ export class AnimationClip extends Asset { private _fromLegacy (legacyData: legacy.AnimationClipLegacyData) { const newTracks = legacyData.toTracks(); - for (const track of newTracks) { + newTracks.forEach((track) => { this.addTrack(track); - } + }); } private _collectAnimatedJoints () { const joints = new Set(); - for (const track of this._tracks) { + this._tracks.forEach((track) => { const trsPath = track[trackBindingTag].parseTrsPath(); if (trsPath) { joints.add(trsPath.node); } - } + }); if (this._exoticAnimation) { - for (const joint of this._exoticAnimation.collectAnimatedJoints()) { + this._exoticAnimation.collectAnimatedJoints().forEach((joint) => { joints.add(joint); - } + }); } return Array.from(joints); @@ -1145,15 +1148,15 @@ class EventEvaluator { const eventGroup = eventGroups[eventIndex]; const components = this._targetNode.components; - for (const event of eventGroup.events) { + eventGroup.events.forEach((event) => { const { functionName } = event; - for (const component of components) { + components.forEach((component) => { const fx = component[functionName]; if (typeof fx === 'function') { fx.apply(component, event.parameters); } - } - } + }); + }); } } From d0f8145d63fd98039b8cf5c658c47c1407c46d2d Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Thu, 22 Jul 2021 11:28:57 +0800 Subject: [PATCH 30/35] Mark deprecateds --- cocos/core/animation/animation-clip.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cocos/core/animation/animation-clip.ts b/cocos/core/animation/animation-clip.ts index c1fac346ab6..026e69d5d22 100644 --- a/cocos/core/animation/animation-clip.ts +++ b/cocos/core/animation/animation-clip.ts @@ -427,14 +427,14 @@ export class AnimationClip extends Asset { } // #region Legacy area - // The following are significantly refactored and deprecated since 3.1. + // The following are significantly refactored and deprecated since 3.3. // We deprecates the direct exposure of keys, values, events. // Instead, we use track to organize them together. /** * @zh 曲线可引用的所有时间轴。 * @en Frame keys referenced by curves. - * @deprecated Since V3.1. + * @deprecated Since V3.3. Please reference to the track/channel/curve mechanism introduced in V3.3. */ get keys () { return this._getLegacyData().keys; @@ -448,7 +448,7 @@ export class AnimationClip extends Asset { /** * @zh 此动画包含的所有曲线。 * @en Curves this animation contains. - * @deprecated Since V3.1. + * @deprecated Since V3.3. Please reference to the track/channel/curve mechanism introduced in V3.3. */ get curves () { this._legacyDataDirty = true; @@ -460,7 +460,7 @@ export class AnimationClip extends Asset { } /** - * @deprecated Since V3.1. + * @deprecated Since V3.3. Please reference to the track/channel/curve mechanism introduced in V3.3. */ get commonTargets () { return this._getLegacyData().commonTargets; @@ -473,7 +473,7 @@ export class AnimationClip extends Asset { /** * 此动画的数据。 - * @deprecated Since V3.1. + * @deprecated Since V3.3. Please reference to the track/channel/curve mechanism introduced in V3.3. */ get data () { return this._getLegacyData().data; @@ -481,7 +481,7 @@ export class AnimationClip extends Asset { /** * @internal - * @deprecated Since V3.1. + * @deprecated Since V3.3. Please reference to the track/channel/curve mechanism introduced in V3.3. */ public getPropertyCurves () { return this._getLegacyData().getPropertyCurves(); @@ -489,7 +489,7 @@ export class AnimationClip extends Asset { /** * @protected - * @deprecated Since V3.1. + * @deprecated Since V3.3. Please reference to the track/channel/curve mechanism introduced in V3.3. */ get eventGroups (): readonly IAnimationEventGroup[] { return this._runtimeEvents.eventGroups; @@ -502,7 +502,7 @@ export class AnimationClip extends Asset { * Commit event data update. * You should call this function after you changed the `events` data to take effect. * @internal - * @deprecated Since V3.1. + * @deprecated Since V3.3. Please reference to the track/channel/curve mechanism introduced in V3.3. */ public updateEventDatas () { // EMPTY @@ -521,7 +521,7 @@ export class AnimationClip extends Asset { * Migrates legacy data into tracks. * @internal This method tend to be used as internal purpose or patch. * DO NOT use it in your code since it might be removed for the future at any time. - * @deprecated Since V3.1. + * @deprecated Since V3.3. Please reference to the track/channel/curve mechanism introduced in V3.3. */ public syncLegacyData () { if (this._legacyData) { From d7c22a065587ad20957390cd6360a755308e3e2d Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Thu, 22 Jul 2021 11:45:03 +0800 Subject: [PATCH 31/35] Hide RealKeyframeValue/QuatKeyframeValue Constructor --- cocos/core/animation/legacy-clip-data.ts | 12 ++-- cocos/core/curves/curve.ts | 64 +++++++++++++++++-- cocos/core/curves/index.ts | 10 ++- cocos/core/curves/keyframe-curve.ts | 22 +++++-- cocos/core/curves/quat-curve.ts | 61 ++++++++++++++++-- cocos/core/geometry/curve.ts | 20 +++--- .../animaion-clip-migration-3.x.test.ts | 60 ++++++++--------- tests/animation/animation-clip-3.x.test.ts | 6 +- tests/animation/animation-clip.test.ts | 2 +- tests/core/geometry/geometry-curve.test.ts | 12 ++-- tests/curves/curve-test-utils.ts | 26 ++++++++ tests/curves/curve.test.ts | 64 +++++++++++-------- tests/curves/key-shared-curves.test.ts | 52 +++++++-------- tests/curves/quat-curve.test.ts | 19 +++--- 14 files changed, 289 insertions(+), 141 deletions(-) create mode 100644 tests/curves/curve-test-utils.ts diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts index 4a418cb9b38..de42895bf82 100644 --- a/cocos/core/animation/legacy-clip-data.ts +++ b/cocos/core/animation/legacy-clip-data.ts @@ -267,7 +267,7 @@ export class AnimationClipLegacyData { } const interpolationMethod = interpolate ? RealInterpolationMode.LINEAR : RealInterpolationMode.CONSTANT; realCurve.assignSorted(times, (legacyValues as number[]).map( - (value) => new RealKeyframeValue({ value, interpolationMode: interpolationMethod }), + (value) => ({ value, interpolationMode: interpolationMethod }), )); legacyEasingMethodConverter.convert(realCurve); return; @@ -287,7 +287,7 @@ export class AnimationClipLegacyData { track.componentsCount = components; const [{ curve: x }, { curve: y }, { curve: z }, { curve: w }] = track.channels(); const interpolationMode = interpolate ? RealInterpolationMode.LINEAR : RealInterpolationMode.CONSTANT; - const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpolationMode }); + const valueToFrame = (value: number): Partial => ({ value, interpolationMode }); switch (components) { case 4: w.assignSorted(times, (legacyValues as Vec4plus).map((value) => valueToFrame(value.w))); @@ -324,7 +324,7 @@ export class AnimationClipLegacyData { installPathAndSetter(track); const [{ curve: r }, { curve: g }, { curve: b }, { curve: a }] = track.channels(); const interpolationMode = interpolate ? RealInterpolationMode.LINEAR : RealInterpolationMode.CONSTANT; - const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpolationMode }); + const valueToFrame = (value: number): Partial => ({ value, interpolationMode }); r.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.r))); legacyEasingMethodConverter.convert(r); g.assignSorted(times, (legacyValues as Color[]).map((value) => valueToFrame(value.g))); @@ -341,7 +341,7 @@ export class AnimationClipLegacyData { installPathAndSetter(track); const [{ curve: width }, { curve: height }] = track.channels(); const interpolationMode = interpolate ? RealInterpolationMode.LINEAR : RealInterpolationMode.CONSTANT; - const valueToFrame = (value: number): RealKeyframeValue => new RealKeyframeValue({ value, interpolationMode }); + const valueToFrame = (value: number): Partial => ({ value, interpolationMode }); width.assignSorted(times, (legacyValues as Size[]).map((value) => valueToFrame(value.width))); legacyEasingMethodConverter.convert(width); height.assignSorted(times, (legacyValues as Size[]).map((value) => valueToFrame(value.height))); @@ -354,7 +354,7 @@ export class AnimationClipLegacyData { const track = new RealTrack(); installPathAndSetter(track); const interpolationMode = interpolate ? RealInterpolationMode.CUBIC : RealInterpolationMode.CONSTANT; - track.channel.curve.assignSorted(times, (legacyValues as CubicSplineNumberValue[]).map((value) => new RealKeyframeValue({ + track.channel.curve.assignSorted(times, (legacyValues as CubicSplineNumberValue[]).map((value) => ({ value: value.dataPoint, leftTangent: value.inTangent, rightTangent: value.outTangent, @@ -376,7 +376,7 @@ export class AnimationClipLegacyData { track.componentsCount = components; const [x, y, z, w] = track.channels(); const interpolationMode = interpolate ? RealInterpolationMode.LINEAR : RealInterpolationMode.CONSTANT; - const valueToFrame = (value: number, inTangent: number, outTangent: number): RealKeyframeValue => new RealKeyframeValue({ + const valueToFrame = (value: number, inTangent: number, outTangent: number): Partial => ({ value, leftTangent: inTangent, rightTangent: outTangent, diff --git a/cocos/core/curves/curve.ts b/cocos/core/curves/curve.ts index a7b06b5673b..1d012f153ee 100644 --- a/cocos/core/curves/curve.ts +++ b/cocos/core/curves/curve.ts @@ -59,9 +59,13 @@ export enum EasingMethod { FADE, } +/** + * View to a real frame value. + * Note, the view may be invalidated due to keyframe change/add/remove. + */ @ccclass('cc.RealKeyframeValue') @uniquelyReferenced -export class RealKeyframeValue { +class RealKeyframeValue { constructor ({ interpolationMode, tangentWeightMode, @@ -139,6 +143,23 @@ export class RealKeyframeValue { public easingMethod = EasingMethod.LINEAR; } +export type { RealKeyframeValue }; + +/** + * The parameter describing a real keyframe value. + * In the case of partial keyframe value, + * each component of the keyframe value is taken from the parameter. + * For unspecified components, default values are taken: + * - Interpolation mode: linear + * - Tangent weight mode: none + * - Value/Tangents/Tangent weights: 0.0 + */ +type RealKeyframeValueParameters = number | Partial; + +function createRealKeyframeValue (params: RealKeyframeValueParameters) { + return new RealKeyframeValue(typeof params === 'number' ? { value: params } : params); +} + /** * Curve. */ @@ -245,9 +266,40 @@ export class RealCurve extends EditorExtendableMixin): void; + + /** + * Assigns all keyframes. + * @param times Times array. Should be sorted. + * @param values Values array. Corresponding to each time in `times`. + */ + public assignSorted (times: readonly number[], values: RealKeyframeValueParameters[]): void; + + public assignSorted ( + times: Iterable<[number, RealKeyframeValueParameters]> | readonly number[], + values?: readonly RealKeyframeValueParameters[], + ) { + if (values !== undefined) { + assertIsTrue(Array.isArray(times)); + this.setKeyframes( + times.slice(), + values.map((value) => createRealKeyframeValue(value)), + ); + } else { + const keyframes = Array.from(times as Iterable<[number, Partial]>); + this.setKeyframes( + keyframes.map(([time]) => time), + keyframes.map(([, value]) => createRealKeyframeValue(value)), + ); + } } /** @@ -331,7 +383,7 @@ export class RealCurve extends EditorExtendableMixin(nKeyframes); for (let iKeyFrame = 0; iKeyFrame < nKeyframes; ++iKeyFrame) { - const keyframeValue = new RealKeyframeValue({}); + const keyframeValue = createRealKeyframeValue({}); currentOffset = loadRealKeyFrameValue(dataView, keyframeValue, currentOffset); keyframeValues[iKeyFrame] = keyframeValue; } @@ -375,7 +427,7 @@ const { leftTangentWeight: DEFAULT_LEFT_TANGENT_WEIGHT, rightTangent: DEFAULT_RIGHT_TANGENT, rightTangentWeight: DEFAULT_RIGHT_TANGENT_WEIGHT, -} = new RealKeyframeValue({}); +} = createRealKeyframeValue({}); const REAL_KEY_FRAME_VALUE_MAX_SIZE = KEY_FRAME_VALUE_FLAGS_BYTES + VALUE_BYTES diff --git a/cocos/core/curves/index.ts b/cocos/core/curves/index.ts index b265797cd46..70fdec19461 100644 --- a/cocos/core/curves/index.ts +++ b/cocos/core/curves/index.ts @@ -1,17 +1,23 @@ export { RealCurve, - RealKeyframeValue, RealInterpolationMode, ExtrapolationMode, TangentWeightMode, } from './curve'; +export type { + RealKeyframeValue, +} from './curve'; + export { QuatCurve, - QuatKeyframeValue, QuatInterpolationMode, } from './quat-curve'; +export type { + QuatKeyframeValue, +} from './quat-curve'; + export { ObjectCurve, } from './object-curve'; diff --git a/cocos/core/curves/keyframe-curve.ts b/cocos/core/curves/keyframe-curve.ts index 30b5c6a7953..38be1504acf 100644 --- a/cocos/core/curves/keyframe-curve.ts +++ b/cocos/core/curves/keyframe-curve.ts @@ -144,16 +144,17 @@ export class KeyframeCurve implements CurveBase, Iterable | readonly number[], values?: readonly TKeyframeValue[]) { if (values !== undefined) { assertIsTrue(Array.isArray(times)); - assertIsTrue(times.length === values.length); - this._times = times.slice(); - this._values = values.map((value) => value); + this.setKeyframes( + times.slice(), + values.slice(), + ); } else { const keyframes = Array.from(times as Iterable<[number, TKeyframeValue]>); - this._times = keyframes.map(([time]) => time); - this._values = keyframes.map(([, value]) => value); + this.setKeyframes( + keyframes.map(([time]) => time), + keyframes.map(([, value]) => value), + ); } - - assertIsTrue(isSorted(this._times)); } /** @@ -168,6 +169,13 @@ export class KeyframeCurve implements CurveBase, Iterable; + +function createQuatKeyframeValue (params: QuatKeyframeValueParameters) { + return new QuatKeyframeValue(params); +} + /** * The method used for interpolation between values of a keyframe and its next keyframe. */ @@ -161,12 +181,43 @@ export class QuatCurve extends KeyframeCurve { * @param value Value of the keyframe. * @returns The index to the new keyframe. */ - public addKeyFrame (time: number, value: IQuatLike | QuatKeyframeValue): number { - const keyframeValue = value instanceof QuatKeyframeValue - ? value : new QuatKeyframeValue({ value }); + public addKeyFrame (time: number, value: QuatKeyframeValueParameters): number { + const keyframeValue = new QuatKeyframeValue(value); return super.addKeyFrame(time, keyframeValue); } + /** + * Assigns all keyframes. + * @param keyframes An iterable to keyframes. The keyframes should be sorted by their time. + */ + public assignSorted (keyframes: Iterable<[number, QuatKeyframeValueParameters]>): void; + + /** + * Assigns all keyframes. + * @param times Times array. Should be sorted. + * @param values Values array. Corresponding to each time in `times`. + */ + public assignSorted (times: readonly number[], values: QuatKeyframeValueParameters[]): void; + + public assignSorted ( + times: Iterable<[number, QuatKeyframeValueParameters]> | readonly number[], + values?: readonly QuatKeyframeValueParameters[], + ) { + if (values !== undefined) { + assertIsTrue(Array.isArray(times)); + this.setKeyframes( + times.slice(), + values.map((value) => createQuatKeyframeValue(value)), + ); + } else { + const keyframes = Array.from(times as Iterable<[number, QuatKeyframeValueParameters]>); + this.setKeyframes( + keyframes.map(([time]) => time), + keyframes.map(([, value]) => createQuatKeyframeValue(value)), + ); + } + } + public [serializeTag] (output: SerializationOutput, context: SerializationContext) { if (!context.toCCON) { output.writeThis(); @@ -270,7 +321,7 @@ export class QuatCurve extends KeyframeCurve { const y = dataView.getFloat32(pQuat + VALUE_BYTES * 1, true); const z = dataView.getFloat32(pQuat + VALUE_BYTES * 2, true); const w = dataView.getFloat32(pQuat + VALUE_BYTES * 3, true); - const keyframeValue = new QuatKeyframeValue({ + const keyframeValue = createQuatKeyframeValue({ value: { x, y, z, w }, interpolationMode: dataView.getUint8(pInterpolationModes), }); diff --git a/cocos/core/geometry/curve.ts b/cocos/core/geometry/curve.ts index aace4c8a774..34fc8ef99d7 100644 --- a/cocos/core/geometry/curve.ts +++ b/cocos/core/geometry/curve.ts @@ -148,12 +148,12 @@ export class AnimationCurve { set keyFrames (value) { this._curve.assignSorted(value.map((legacyCurve) => [ legacyCurve.time, - new RealKeyframeValue({ + { interpolationMode: RealInterpolationMode.CUBIC, value: legacyCurve.value, leftTangent: legacyCurve.inTangent, rightTangent: legacyCurve.outTangent, - }), + }, ])); } @@ -201,16 +201,16 @@ export class AnimationCurve { curve.postExtrapolation = ExtrapolationMode.CLAMP; if (!keyFrames) { curve.assignSorted([ - [0.0, new RealKeyframeValue({ interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 })], - [1.0, new RealKeyframeValue({ interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 })], + [0.0, { interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 }], + [1.0, { interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 }], ]); } else { - curve.assignSorted(keyFrames.map((legacyKeyframe) => [legacyKeyframe.time, new RealKeyframeValue({ + curve.assignSorted(keyFrames.map((legacyKeyframe) => [legacyKeyframe.time, { interpolationMode: RealInterpolationMode.CUBIC, value: legacyKeyframe.value, leftTangent: legacyKeyframe.inTangent, rightTangent: legacyKeyframe.outTangent, - })])); + }])); } } this.cachedKey = new OptimizedKey(); @@ -227,12 +227,12 @@ export class AnimationCurve { if (!keyFrame) { this._curve.clear(); } else { - this._curve.addKeyFrame(keyFrame.time, new RealKeyframeValue({ + this._curve.addKeyFrame(keyFrame.time, { interpolationMode: RealInterpolationMode.CUBIC, value: keyFrame.value, leftTangent: keyFrame.inTangent, rightTangent: keyFrame.outTangent, - })); + }); } } @@ -376,8 +376,8 @@ function toLegacyWrapMode (extrapolationMode: ExtrapolationMode): WrapModeMask { export function constructLegacyCurveAndConvert () { const curve = new RealCurve(); curve.assignSorted([ - [0.0, new RealKeyframeValue({ interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 })], - [1.0, new RealKeyframeValue({ interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 })], + [0.0, { interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 }], + [1.0, { interpolationMode: RealInterpolationMode.CUBIC, value: 1.0 }], ]); return curve; } diff --git a/tests/animation/animaion-clip-migration-3.x.test.ts b/tests/animation/animaion-clip-migration-3.x.test.ts index c30666b7178..d9387d1f2f4 100644 --- a/tests/animation/animaion-clip-migration-3.x.test.ts +++ b/tests/animation/animaion-clip-migration-3.x.test.ts @@ -7,6 +7,7 @@ import { ComponentPath, HierarchyPath, ICustomTargetPath, TargetPath } from "../ import { RealChannel } from "../../cocos/core/animation/tracks/track"; import { UntypedTrack } from "../../cocos/core/animation/tracks/untyped-track"; import { EasingMethod, ExtrapolationMode, RealCurve, RealKeyframeValue, TangentWeightMode } from "../../cocos/core/curves/curve"; +import { createMultipleRealKeyframesWithoutTangent, createRealKeyframeValueLike } from "../curves/curve-test-utils"; class ValueProxyFactorFoo implements IValueProxyFactory { forTarget(_target: any): animation.IValueProxy { @@ -236,7 +237,7 @@ describe('Animation Clip Migration 3.x', () => { const valuesAtChannel = valueType === Number ? values as number[] : values.map((value) => (value as Record)[getComponentNameOfChannel(iChannel)]); - expect(Array.from(curve.values())).toStrictEqual(valuesAtChannel.map((value) => new RealKeyframeValue({ + expect(Array.from(curve.values())).toStrictEqual(valuesAtChannel.map((value) => createRealKeyframeValueLike({ value, interpolationMode, }))); @@ -265,11 +266,11 @@ describe('Animation Clip Migration 3.x', () => { const [{ curve: width }, { curve: height }] = track.channels(); expect(Array.from(width.times())).toStrictEqual([0.0, 0.2, 0.8]); expect(Array.from(width.values())).toStrictEqual( - createRealKeyframesWithoutTangent([10.8, 20, 30], RealInterpolationMode.LINEAR), + createMultipleRealKeyframesWithoutTangent([10.8, 20, 30], RealInterpolationMode.LINEAR), ); expect(Array.from(height.times())).toStrictEqual([0.0, 0.2, 0.8]); expect(Array.from(height.values())).toStrictEqual( - createRealKeyframesWithoutTangent([-1.3, 50, 60], RealInterpolationMode.LINEAR), + createMultipleRealKeyframesWithoutTangent([-1.3, 50, 60], RealInterpolationMode.LINEAR), ); }); @@ -333,8 +334,8 @@ describe('Animation Clip Migration 3.x', () => { expect(track1.path.isPropertyAt(0)).toBe(true); expect(track1.path.parsePropertyAt(0)).toBe('p1'); expect(track1.channels()).toHaveLength(2); - expect(Array.from(track1.channels()[0].curve.keyframes())).toStrictEqual([[1.2, new RealKeyframeValue({ value: 0.1 })]]); - expect(Array.from(track1.channels()[1].curve.keyframes())).toStrictEqual([[1.2, new RealKeyframeValue({ value: 0.2 })]]); + expect(Array.from(track1.channels()[0].curve.keyframes())).toStrictEqual([[1.2, createRealKeyframeValueLike({ value: 0.1 })]]); + expect(Array.from(track1.channels()[1].curve.keyframes())).toStrictEqual([[1.2, createRealKeyframeValueLike({ value: 0.2 })]]); expect(track2).toBeInstanceOf(UntypedTrack); expect(track2.path.length).toBe(1); @@ -342,7 +343,7 @@ describe('Animation Clip Migration 3.x', () => { expect(track2.path.parsePropertyAt(0)).toBe('p2'); expect(track2.proxy).toBe(valueProxy); expect(track2.channels()).toHaveLength(1); - expect(Array.from(track2.channels()[0].curve.keyframes())).toStrictEqual([[1.2, new RealKeyframeValue({ value: 0.3 })]]); + expect(Array.from(track2.channels()[0].curve.keyframes())).toStrictEqual([[1.2, createRealKeyframeValueLike({ value: 0.3 })]]); }); describe(`Easing methods`, () => { @@ -381,7 +382,7 @@ describe('Animation Clip Migration 3.x', () => { true, ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.5]); - expect(Array.from(curve.values())).toStrictEqual(createRealKeyframesWithoutTangent([1, 3, 5], RealInterpolationMode.LINEAR)); + expect(Array.from(curve.values())).toStrictEqual(createMultipleRealKeyframesWithoutTangent([1, 3, 5], RealInterpolationMode.LINEAR)); }); describe(`Specified through ".easingMethod"`, () => { @@ -394,7 +395,7 @@ describe('Animation Clip Migration 3.x', () => { true, ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); - expect(Array.from(curve.values())).toStrictEqual(createRealKeyframesWithoutTangent([1, 3, 5], RealInterpolationMode.LINEAR)); + expect(Array.from(curve.values())).toStrictEqual(createMultipleRealKeyframesWithoutTangent([1, 3, 5], RealInterpolationMode.LINEAR)); }); test(`Time bezier`, () => { @@ -406,13 +407,13 @@ describe('Animation Clip Migration 3.x', () => { true, ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); - expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ + expect(Array.from(curve.values())).toStrictEqual([createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.RIGHT, value: 1, rightTangent: 14.999999999999998, rightTangentWeight: 0.6013318551349163, - }), new RealKeyframeValue({ + }), createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.BOTH, value: 3, @@ -420,7 +421,7 @@ describe('Animation Clip Migration 3.x', () => { leftTangentWeight: 1.0071742649611337, rightTangent: 5.999999999999998, rightTangentWeight: 0.6082762530298218, - }), new RealKeyframeValue({ + }), createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.LINEAR, value: 5, tangentWeightMode: TangentWeightMode.LEFT, @@ -438,13 +439,13 @@ describe('Animation Clip Migration 3.x', () => { true, ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); - expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ + expect(Array.from(curve.values())).toStrictEqual([createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.CONSTANT, value: 1, - }), new RealKeyframeValue({ + }), createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.CONSTANT, value: 3, - }), new RealKeyframeValue({ + }), createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.LINEAR, // Last frame never converted value: 5, })]); @@ -459,13 +460,13 @@ describe('Animation Clip Migration 3.x', () => { true, ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); - expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ + expect(Array.from(curve.values())).toStrictEqual([createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.LINEAR, value: 1, - }), new RealKeyframeValue({ + }), createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.LINEAR, value: 3, - }), new RealKeyframeValue({ + }), createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.LINEAR, value: 5, })]); @@ -480,15 +481,15 @@ describe('Animation Clip Migration 3.x', () => { true, ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.8]); - expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ + expect(Array.from(curve.values())).toStrictEqual([createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.LINEAR, value: 1, easingMethod: EasingMethod.CUBIC_IN_OUT, - }), new RealKeyframeValue({ + }), createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.LINEAR, value: 3, easingMethod: EasingMethod.CUBIC_IN_OUT, - }), new RealKeyframeValue({ + }), createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.LINEAR, // Last frame never converted value: 5, })]); @@ -510,16 +511,16 @@ describe('Animation Clip Migration 3.x', () => { true, ); expect(Array.from(curve.times())).toStrictEqual([0.1, 0.3, 0.5]); - expect(Array.from(curve.values())).toStrictEqual([new RealKeyframeValue({ + expect(Array.from(curve.values())).toStrictEqual([createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.LINEAR, value: 1, - }), new RealKeyframeValue({ + }), createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.RIGHT, value: 3, rightTangent: 14.999999999999996, rightTangentWeight: 0.6013318551349163, - }), new RealKeyframeValue({ + }), createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.LINEAR, value: 5, tangentWeightMode: TangentWeightMode.LEFT, @@ -554,10 +555,10 @@ describe('Animation Clip Migration 3.x', () => { function testTimeBezierCurveConversion (testCase: TimeBezierTestCase) { const curve = new RealCurve(); curve.assignSorted([ - [testCase.t0, new RealKeyframeValue({ + [testCase.t0, ({ value: testCase.v0, })], - [testCase.t1, new RealKeyframeValue({ + [testCase.t1, ({ value: testCase.v1, })], ]); @@ -602,12 +603,3 @@ function createClipWithLegacyData ({ clip.syncLegacyData(); return clip; } - -function createRealKeyframesWithoutTangent (values: number[], interpolationMode: RealInterpolationMode): RealKeyframeValue[] { - return values.map((value) => { - return new RealKeyframeValue({ - value, - interpolationMode, - }); - }); -} \ No newline at end of file diff --git a/tests/animation/animation-clip-3.x.test.ts b/tests/animation/animation-clip-3.x.test.ts index cbc1ffe6a44..13abaf4cf80 100644 --- a/tests/animation/animation-clip-3.x.test.ts +++ b/tests/animation/animation-clip-3.x.test.ts @@ -73,9 +73,9 @@ describe('Animation Clip', () => { rootBoneTranslationTrack.path = new TrackPath().hierarchy(rootJointName).property('position'); const [x, _y, _z] = rootBoneTranslationTrack.channels(); x.curve.assignSorted([ - [0.4, new RealKeyframeValue({ value: 0.4 })], - [0.6, new RealKeyframeValue({ value: 0.6 })], - [0.8, new RealKeyframeValue({ value: 0.8 })], + [0.4, ({ value: 0.4 })], + [0.6, ({ value: 0.6 })], + [0.8, ({ value: 0.8 })], ]); } diff --git a/tests/animation/animation-clip.test.ts b/tests/animation/animation-clip.test.ts index 12df416d452..ee7d7c0255b 100644 --- a/tests/animation/animation-clip.test.ts +++ b/tests/animation/animation-clip.test.ts @@ -147,7 +147,7 @@ describe('Custom track setter', () => { const track = new VectorTrack(); track.proxy = valueProxyWithGetSet; track.channels().forEach(({ curve }) => { - curve.assignSorted([[0.0, new RealKeyframeValue({ value: 0.0 })]]); + curve.assignSorted([[0.0, ({ value: 0.0 })]]); }); const clip = new AnimationClip(); diff --git a/tests/core/geometry/geometry-curve.test.ts b/tests/core/geometry/geometry-curve.test.ts index a799ad74cf2..f10aea3c979 100644 --- a/tests/core/geometry/geometry-curve.test.ts +++ b/tests/core/geometry/geometry-curve.test.ts @@ -27,19 +27,19 @@ describe('geometry.AnimationCurve', () => { const realCurve = new RealCurve(); realCurve.assignSorted([ // Non weighted tangent - [0.1, new RealKeyframeValue({ + [0.1, ({ interpolationMode: RealInterpolationMode.CUBIC, value: 0.1, leftTangent: 0.2, rightTangent: 0.3, })], // Non cubic keyframe - [0.2, new RealKeyframeValue({ + [0.2, ({ interpolationMode: RealInterpolationMode.LINEAR, value: 0.1, })], // Weighted tangent - [0.3, new RealKeyframeValue({ + [0.3, ({ interpolationMode: RealInterpolationMode.CUBIC, value: 0.1, leftTangent: 0.2, @@ -146,19 +146,19 @@ describe('geometry.AnimationCurve', () => { curve._internalCurve.assignSorted([ // Non weighted tangent - [0.1, new RealKeyframeValue({ + [0.1, ({ interpolationMode: RealInterpolationMode.CUBIC, value: 0.1, leftTangent: 0.2, rightTangent: 0.3, })], // Non cubic keyframe - [0.2, new RealKeyframeValue({ + [0.2, ({ interpolationMode: RealInterpolationMode.LINEAR, value: 0.1, })], // Weighted tangent - [0.3, new RealKeyframeValue({ + [0.3, ({ interpolationMode: RealInterpolationMode.CUBIC, value: 0.1, leftTangent: 0.2, diff --git a/tests/curves/curve-test-utils.ts b/tests/curves/curve-test-utils.ts new file mode 100644 index 00000000000..13161754c02 --- /dev/null +++ b/tests/curves/curve-test-utils.ts @@ -0,0 +1,26 @@ +import { EasingMethod, RealCurve, RealInterpolationMode, RealKeyframeValue, TangentWeightMode } from "../../cocos/core/curves/curve"; + +export function createRealKeyframeValueLike (value: Partial): RealKeyframeValue { + const curve = new RealCurve(); + // return { + // interpolationMode: RealInterpolationMode.LINEAR, + // tangentWeightMode: TangentWeightMode.NONE, + // value: 0.0, + // leftTangent: 0.0, + // leftTangentWeight: 0.0, + // rightTangent: 0.0, + // rightTangentWeight: 0.0, + // easingMethod: EasingMethod.LINEAR, + // ...value, + // }; + return curve.getKeyframeValue(curve.addKeyFrame(0.0, value)); +} + +export function createMultipleRealKeyframesWithoutTangent (values: number[], interpolationMode: RealInterpolationMode): RealKeyframeValue[] { + return values.map((value) => { + return createRealKeyframeValueLike({ + value, + interpolationMode, + }); + }); +} diff --git a/tests/curves/curve.test.ts b/tests/curves/curve.test.ts index 4a20522056b..de225203551 100644 --- a/tests/curves/curve.test.ts +++ b/tests/curves/curve.test.ts @@ -2,6 +2,7 @@ import { toRadian } from '../../cocos/core'; import { RealCurve, RealInterpolationMode } from '../../cocos/core/curves'; import { EasingMethod, RealKeyframeValue } from '../../cocos/core/curves/curve'; import { ExtrapolationMode, TangentWeightMode } from '../../cocos/core/curves/real-curve-param'; +import { createRealKeyframeValueLike } from './curve-test-utils'; import { serializeAndDeserialize } from './serialize-and-deserialize-curve'; describe('Curve', () => { @@ -21,6 +22,15 @@ describe('Curve', () => { curve.assignSorted([]); expect(curve.keyFramesCount).toBe(0); + // Assign(keys, values), values can be number of partial RealKeyframeValue + curve.assignSorted([0.1, 0.2, 0.3], [-3.6, { value: 2.7 }, { value: 3.8, interpolationMode: RealInterpolationMode.CUBIC }]); + expect(Array.from(curve.times())).toStrictEqual([0.1, 0.2, 0.3]); + expect(Array.from(curve.values())).toStrictEqual([ + createRealKeyframeValueLike({ value: -3.6 }), + createRealKeyframeValueLike({ value: 2.7 }), + createRealKeyframeValueLike({ value: 3.8, interpolationMode: RealInterpolationMode.CUBIC }), + ]); + // The count of keys and values should be same, if not, the behavior is undefined. // In test mode, assertion error would be thrown. expect(() => curve.assignSorted([0.1, 0.2], [])).toThrow(); @@ -41,9 +51,9 @@ describe('Curve', () => { test('Normal', () => { const curve = new RealCurve(); curve.assignSorted([0.1, 0.2, 0.3], [ - new RealKeyframeValue({ value: 0.4, rightTangent: 0.0, leftTangent: 0.0, interpolationMode: RealInterpolationMode.CONSTANT }), - new RealKeyframeValue({ value: 0.5, rightTangent: 0.0, leftTangent: 0.0, interpolationMode: RealInterpolationMode.LINEAR }), - new RealKeyframeValue({ + ({ value: 0.4, rightTangent: 0.0, leftTangent: 0.0, interpolationMode: RealInterpolationMode.CONSTANT }), + ({ value: 0.5, rightTangent: 0.0, leftTangent: 0.0, interpolationMode: RealInterpolationMode.LINEAR }), + ({ value: 0.6, rightTangent: 0.487, rightTangentWeight: 0.2, @@ -79,7 +89,9 @@ describe('Curve', () => { }); test('Default keyframe value', () => { - const keyframeValue = new RealKeyframeValue({}); + const curve = new RealCurve(); + curve.addKeyFrame(0, {}); + const keyframeValue = curve.getKeyframeValue(0); expect(keyframeValue.value).toBe(0.0); expect(keyframeValue.interpolationMode).toBe(RealInterpolationMode.LINEAR); expect(keyframeValue.rightTangent).toBe(0.0); @@ -98,8 +110,8 @@ describe('Curve', () => { test('Interpolation mode: constant', () => { const curve = new RealCurve(); curve.assignSorted([ - [0.2, new RealKeyframeValue({ value: 0.7, interpolationMode: RealInterpolationMode.CONSTANT, })], - [0.4, new RealKeyframeValue({ value: 0.8, interpolationMode: RealInterpolationMode.LINEAR, })], + [0.2, createRealKeyframeValueLike({ value: 0.7, interpolationMode: RealInterpolationMode.CONSTANT, })], + [0.4, createRealKeyframeValueLike({ value: 0.8, interpolationMode: RealInterpolationMode.LINEAR, })], ]); expect(curve.evaluate(0.28)).toBe(0.7); }); @@ -107,8 +119,8 @@ describe('Curve', () => { test('Interpolation mode: linear', () => { const curve = new RealCurve(); curve.assignSorted([ - [0.2, new RealKeyframeValue({ value: 0.7, interpolationMode: RealInterpolationMode.LINEAR, })], - [0.4, new RealKeyframeValue({ value: 0.8, interpolationMode: RealInterpolationMode.CONSTANT, })], + [0.2, createRealKeyframeValueLike({ value: 0.7, interpolationMode: RealInterpolationMode.LINEAR, })], + [0.4, createRealKeyframeValueLike({ value: 0.8, interpolationMode: RealInterpolationMode.CONSTANT, })], ]); expect(curve.evaluate(0.28)).toBeCloseTo(0.74); }); @@ -116,13 +128,13 @@ describe('Curve', () => { test('Interpolation mode: cubic; Both weights are unused', () => { const curve = new RealCurve(); curve.assignSorted([ - [0.2, new RealKeyframeValue({ + [0.2, createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.7, rightTangent: Math.tan(toRadian(30.0)), })], - [0.4, new RealKeyframeValue({ + [0.4, createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.8, @@ -135,13 +147,13 @@ describe('Curve', () => { test('Interpolation mode: cubic; Start weight is used', () => { const curve = new RealCurve(); curve.assignSorted([ - [0.2, new RealKeyframeValue({ + [0.2, createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.RIGHT, value: 0.7, rightTangent: Math.tan(toRadian(30.0)), })], - [0.4, new RealKeyframeValue({ + [0.4, createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.8, @@ -154,13 +166,13 @@ describe('Curve', () => { test('Interpolation mode: cubic; End weight is used', () => { const curve = new RealCurve(); curve.assignSorted([ - [0.2, new RealKeyframeValue({ + [0.2, createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.NONE, value: 0.7, rightTangent: Math.tan(toRadian(30.0)), })], - [0.4, new RealKeyframeValue({ + [0.4, createRealKeyframeValueLike({ interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.LEFT, value: 0.8, @@ -176,7 +188,7 @@ describe('Curve', () => { curve.postExtrapolation = ExtrapolationMode.CLAMP; curve.assignSorted([ - [0.2, new RealKeyframeValue({ value: 5.0 })], + [0.2, ({ value: 5.0 })], ]); // Fall back to clamp expect(curve.evaluate(0.05)).toBeCloseTo(5.0); @@ -189,15 +201,15 @@ describe('Curve', () => { curve.postExtrapolation = ExtrapolationMode.LINEAR; curve.assignSorted([ - [0.2, new RealKeyframeValue({ value: 5.0 })], - [0.3, new RealKeyframeValue({ value: 3.14 })], + [0.2, ({ value: 5.0 })], + [0.3, ({ value: 3.14 })], ]); expect(curve.evaluate(0.05)).toBeCloseTo(7.79); expect(curve.evaluate(-102.4)).toBeCloseTo(1913.36); expect(curve.evaluate(0.46)).toBeCloseTo(0.164); curve.assignSorted([ - [0.2, new RealKeyframeValue({ value: 5.0 })], + [0.2, ({ value: 5.0 })], ]); // Fall back to clamp expect(curve.evaluate(0.05)).toBeCloseTo(5.0); @@ -210,14 +222,14 @@ describe('Curve', () => { curve.postExtrapolation = ExtrapolationMode.LOOP; curve.assignSorted([ - [0.2, new RealKeyframeValue({ value: 5.0 })], - [0.36, new RealKeyframeValue({ value: 3.14 })], + [0.2, ({ value: 5.0 })], + [0.36, ({ value: 3.14 })], ]); expect(curve.evaluate(-2.7)).toBeCloseTo(curve.evaluate(0.34)); expect(curve.evaluate(4.6)).toBeCloseTo(curve.evaluate(0.28)); curve.assignSorted([ - [0.2, new RealKeyframeValue({ value: 5.0 })], + [0.2, ({ value: 5.0 })], ]); // Fall back to clamp expect(curve.evaluate(0.05)).toBeCloseTo(5.0); @@ -230,15 +242,15 @@ describe('Curve', () => { curve.postExtrapolation = ExtrapolationMode.PING_PONG; curve.assignSorted([ - [0.2, new RealKeyframeValue({ value: 5.0 })], - [0.36, new RealKeyframeValue({ value: 3.14 })], + [0.2, ({ value: 5.0 })], + [0.36, ({ value: 3.14 })], ]); expect(curve.evaluate(-2.7)).toBeCloseTo(curve.evaluate(0.22)); expect(curve.evaluate(4.6)).toBeCloseTo(curve.evaluate(0.28)); expect(curve.evaluate(4.77)).toBeCloseTo(curve.evaluate(0.29)); curve.assignSorted([ - [0.2, new RealKeyframeValue({ value: 5.0 })], + [0.2, ({ value: 5.0 })], ]); // Fall back to clamp expect(curve.evaluate(0.05)).toBeCloseTo(5.0); @@ -247,8 +259,8 @@ describe('Curve', () => { }); }); -function realKeyframeWithoutTangent (value: number, interpolationMode: RealInterpolationMode = RealInterpolationMode.LINEAR): RealKeyframeValue { - return new RealKeyframeValue({ +function realKeyframeWithoutTangent (value: number, interpolationMode: RealInterpolationMode = RealInterpolationMode.LINEAR): Partial { + return createRealKeyframeValueLike({ value, interpolationMode, }); diff --git a/tests/curves/key-shared-curves.test.ts b/tests/curves/key-shared-curves.test.ts index 6adb2d243cc..5e4692a781f 100644 --- a/tests/curves/key-shared-curves.test.ts +++ b/tests/curves/key-shared-curves.test.ts @@ -1,14 +1,14 @@ -import { ExtrapolationMode, RealCurve, RealInterpolationMode, RealKeyframeValue } from '../../cocos/core/curves/curve'; +import { ExtrapolationMode, RealCurve, RealInterpolationMode } from '../../cocos/core/curves/curve'; import { KeySharedQuatCurves, KeySharedRealCurves } from '../../cocos/core/curves/keys-shared-curves'; -import { QuatCurve, QuatInterpolationMode, QuatKeyframeValue } from '../../cocos/core/curves/quat-curve'; +import { QuatCurve, QuatInterpolationMode } from '../../cocos/core/curves/quat-curve'; import { Quat } from '../../cocos/core/math'; describe('Keys shared real curves', () => { test('Enabling', () => { { const curve = new RealCurve(); - curve.assignSorted([[0.1, new RealKeyframeValue({ + curve.assignSorted([[0.1, ({ value: 0.1, })]]); expect(KeySharedRealCurves.allowedForCurve(curve)).toBe(true); @@ -16,7 +16,7 @@ describe('Keys shared real curves', () => { { const curve = new RealCurve(); - curve.assignSorted([[0.1, new RealKeyframeValue({ + curve.assignSorted([[0.1, ({ value: 0.1, })]]); curve.postExtrapolation = ExtrapolationMode.LOOP; @@ -25,7 +25,7 @@ describe('Keys shared real curves', () => { { const curve = new RealCurve(); - curve.assignSorted([[0.1, new RealKeyframeValue({ + curve.assignSorted([[0.1, ({ value: 0.1, })]]); curve.preExtrapolation = ExtrapolationMode.LOOP; @@ -34,7 +34,7 @@ describe('Keys shared real curves', () => { { const curve = new RealCurve(); - curve.assignSorted([[0.1, new RealKeyframeValue({ + curve.assignSorted([[0.1, ({ value: 0.1, interpolationMode: RealInterpolationMode.CUBIC, })]]); @@ -46,11 +46,11 @@ describe('Keys shared real curves', () => { const curves1 = new KeySharedRealCurves([0.1, 0.7, 0.8]); const curveMatched = new RealCurve(); - curveMatched.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, () => new RealKeyframeValue({ value: 0.1 }))); + curveMatched.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, () => ({ value: 0.1 }))); expect(curves1.matchCurve(curveMatched)).toBe(true); const curveNonMatched = new RealCurve(); - curveNonMatched.assignSorted([0.1, 0.3, 0.8], Array.from({ length: 3 }, () => new RealKeyframeValue({ value: 0.1 }))); + curveNonMatched.assignSorted([0.1, 0.3, 0.8], Array.from({ length: 3 }, () => ({ value: 0.1 }))); expect(curves1.matchCurve(curveNonMatched)).toBe(false); }); @@ -58,17 +58,17 @@ describe('Keys shared real curves', () => { const curves1 = new KeySharedRealCurves([0.1, 0.2, 0.3]); const curveMatched = new RealCurve(); - curveMatched.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, () => new RealKeyframeValue({ value: 0.1 }))); + curveMatched.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, () => ({ value: 0.1 }))); expect(curves1.matchCurve(curveMatched)).toBe(true); const curveNonMatched = new RealCurve(); - curveNonMatched.assignSorted([0.2, 0.3, 0.4], Array.from({ length: 3 }, () => new RealKeyframeValue({ value: 0.1 }))); + curveNonMatched.assignSorted([0.2, 0.3, 0.4], Array.from({ length: 3 }, () => ({ value: 0.1 }))); expect(curves1.matchCurve(curveNonMatched)).toBe(false); }); test('Evaluate', () => { const curve = new RealCurve(); - curve.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, (_, index) => new RealKeyframeValue({ value: index + 1 }))); + curve.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, (_, index) => ({ value: index + 1 }))); const curves = new KeySharedRealCurves(Array.from(curve.times())); curves.addCurve(curve); @@ -93,7 +93,7 @@ describe('Keys shared real curves', () => { test('Evaluate optimized keys', () => { const curve = new RealCurve(); - curve.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, (_, index) => new RealKeyframeValue({ value: index + 1 }))); + curve.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, (_, index) => ({ value: index + 1 }))); const curves = new KeySharedRealCurves(Array.from(curve.times())); curves.addCurve(curve); @@ -121,37 +121,37 @@ describe('Keys shared quaternion curves', () => { test('Enabling', () => { { const curve = new QuatCurve(); - curve.assignSorted([[0.1, new QuatKeyframeValue({ + curve.assignSorted([[0.1, { interpolationMode: QuatInterpolationMode.SLERP, value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, - })]]); + }]]); expect(KeySharedQuatCurves.allowedForCurve(curve)).toBe(true); } { const curve = new QuatCurve(); - curve.assignSorted([[0.1, new QuatKeyframeValue({ + curve.assignSorted([[0.1, { value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, - })]]); + }]]); curve.postExtrapolation = ExtrapolationMode.LOOP; expect(KeySharedQuatCurves.allowedForCurve(curve)).toBe(false); } { const curve = new QuatCurve(); - curve.assignSorted([[0.1, new QuatKeyframeValue({ + curve.assignSorted([[0.1, { value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, - })]]); + }]]); curve.preExtrapolation = ExtrapolationMode.LOOP; expect(KeySharedQuatCurves.allowedForCurve(curve)).toBe(false); } { const curve = new QuatCurve(); - curve.assignSorted([[0.1, new QuatKeyframeValue({ + curve.assignSorted([[0.1, { value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 }, interpolationMode: QuatInterpolationMode.CONSTANT, - })]]); + }]]); expect(KeySharedQuatCurves.allowedForCurve(curve)).toBe(false); } }); @@ -161,12 +161,12 @@ describe('Keys shared quaternion curves', () => { const curveMatched = new QuatCurve(); curveMatched.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, () => - new QuatKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + ({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); expect(curves1.matchCurve(curveMatched)).toBe(true); const curveNonMatched = new QuatCurve(); curveNonMatched.assignSorted([0.1, 0.3, 0.8], Array.from({ length: 3 }, () => - new QuatKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + ({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); expect(curves1.matchCurve(curveNonMatched)).toBe(false); }); @@ -175,12 +175,12 @@ describe('Keys shared quaternion curves', () => { const curveMatched = new QuatCurve(); curveMatched.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, () => - new QuatKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + ({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); expect(curves1.matchCurve(curveMatched)).toBe(true); const curveNonMatched = new QuatCurve(); curveNonMatched.assignSorted([0.2, 0.3, 0.4], Array.from({ length: 3 }, () => - new QuatKeyframeValue({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); + ({ value: { x: -0.542, y: -0.688, z: 0.199, w: -0.439 } }))); expect(curves1.matchCurve(curveNonMatched)).toBe(false); }); @@ -193,7 +193,7 @@ describe('Keys shared quaternion curves', () => { test('Evaluate', () => { const curve = new QuatCurve(); curve.assignSorted([0.1, 0.7, 0.8], Array.from({ length: 3 }, (_, index) => - new QuatKeyframeValue({ value: Quat.clone(quaternions[index]) }))); + ({ value: Quat.clone(quaternions[index]) }))); const curves = new KeySharedQuatCurves(Array.from(curve.times())); curves.addCurve(curve); @@ -219,7 +219,7 @@ describe('Keys shared quaternion curves', () => { test('Evaluate optimized keys', () => { const curve = new QuatCurve(); curve.assignSorted([0.1, 0.2, 0.3], Array.from({ length: 3 }, (_, index) => - new QuatKeyframeValue({ value: Quat.clone(quaternions[index]) }))); + ({ value: Quat.clone(quaternions[index]) }))); const curves = new KeySharedQuatCurves(Array.from(curve.times())); curves.addCurve(curve); diff --git a/tests/curves/quat-curve.test.ts b/tests/curves/quat-curve.test.ts index a99cdab651a..bd12fa224c1 100644 --- a/tests/curves/quat-curve.test.ts +++ b/tests/curves/quat-curve.test.ts @@ -1,5 +1,5 @@ -import { Quat, QuatCurve, QuatInterpolationMode, QuatKeyframeValue } from '../../cocos/core'; +import { Quat, QuatCurve, QuatInterpolationMode } from '../../cocos/core'; import { serializeAndDeserialize } from './serialize-and-deserialize-curve'; describe('Curve', () => { @@ -12,9 +12,9 @@ describe('Curve', () => { test('Normal', () => { const curve = new QuatCurve(); curve.assignSorted([0.1, 0.2, 0.3], [ - new QuatKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpolationMode: QuatInterpolationMode.CONSTANT }), - new QuatKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpolationMode: QuatInterpolationMode.SLERP }), - new QuatKeyframeValue({ value: { x: 0.9, y: 0.1, z: 0.11, w: 0.12 }, interpolationMode: QuatInterpolationMode.CONSTANT }), + { value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpolationMode: QuatInterpolationMode.CONSTANT }, + { value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpolationMode: QuatInterpolationMode.SLERP }, + { value: { x: 0.9, y: 0.1, z: 0.11, w: 0.12 }, interpolationMode: QuatInterpolationMode.CONSTANT }, ]); compareCurves(serializeAndDeserialize(curve, QuatCurve), curve); }); @@ -22,8 +22,8 @@ describe('Curve', () => { test('Optimized for linear curve', () => { const curve = new QuatCurve(); curve.assignSorted([0.1, 0.2], [ - new QuatKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpolationMode: QuatInterpolationMode.SLERP }), - new QuatKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpolationMode: QuatInterpolationMode.SLERP }), + { value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpolationMode: QuatInterpolationMode.SLERP }, + { value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpolationMode: QuatInterpolationMode.SLERP }, ]); compareCurves(serializeAndDeserialize(curve, QuatCurve), curve); }); @@ -31,15 +31,16 @@ describe('Curve', () => { test('Optimized for constant curve', () => { const curve = new QuatCurve(); curve.assignSorted([0.1, 0.2], [ - new QuatKeyframeValue({ value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpolationMode: QuatInterpolationMode.CONSTANT }), - new QuatKeyframeValue({ value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpolationMode: QuatInterpolationMode.CONSTANT }), + { value: { x: 0.1, y: 0.2, z: 0.3, w: 0.4 }, interpolationMode: QuatInterpolationMode.CONSTANT }, + { value: { x: 0.5, y: -0.6, z: 0.7, w: 0.8 }, interpolationMode: QuatInterpolationMode.CONSTANT }, ]); compareCurves(serializeAndDeserialize(curve, QuatCurve), curve); }); }); test('Default keyframe value', () => { - const keyframeValue = new QuatKeyframeValue({}); + const curve = new QuatCurve(); + const keyframeValue = curve.getKeyframeValue(curve.addKeyFrame(0.0, {})); expect(Quat.equals(keyframeValue.value, Quat.IDENTITY)).toBe(true); expect(keyframeValue.interpolationMode).toBe(QuatInterpolationMode.SLERP); }); From 0dece5dd7540ef67c9835830210eb33b0625be95 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Thu, 22 Jul 2021 12:34:34 +0800 Subject: [PATCH 32/35] Fix quat keyframe value name --- cocos/core/curves/quat-curve.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocos/core/curves/quat-curve.ts b/cocos/core/curves/quat-curve.ts index d89eb78fda5..16efb2824ea 100644 --- a/cocos/core/curves/quat-curve.ts +++ b/cocos/core/curves/quat-curve.ts @@ -11,7 +11,7 @@ import { DeserializationContext } from '../data/custom-serializable'; * View to a quaternion frame value. * Note, the view may be invalidated due to keyframe change/add/remove. */ -@ccclass('cc.QuaternionKeyframeValue') +@ccclass('cc.QuatKeyframeValue') @uniquelyReferenced class QuatKeyframeValue { /** From 83dd1f90d4d4c47e6ab7c0b2950d49e7cfd114b7 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Thu, 22 Jul 2021 14:13:00 +0800 Subject: [PATCH 33/35] Error ID --- EngineErrorMap.md | 64 ++++++++++++++++++++ cocos/core/animation/animation-clip.ts | 10 +-- cocos/core/animation/legacy-clip-data.ts | 10 +-- cocos/core/animation/target-path.ts | 10 +-- cocos/core/animation/tracks/size-track.ts | 2 +- cocos/core/animation/tracks/track.ts | 9 +-- cocos/core/animation/tracks/untyped-track.ts | 12 +++- 7 files changed, 92 insertions(+), 25 deletions(-) diff --git a/EngineErrorMap.md b/EngineErrorMap.md index a5a506c6919..955e58cbfa1 100644 --- a/EngineErrorMap.md +++ b/EngineErrorMap.md @@ -1747,6 +1747,70 @@ animation not added or already removed already-playing +### 3920 + +Current context does not allow root motion. + +### 3921 + +You provided a ill-formed track path. The last component of track path should be property key, or the setter should not be empty. + +### 3922 + +Seems like we have animation for %s but are missing its parent joint %s in animation? + +### 3923 + +Root motion is ignored since root bone could not be located in animation. + +### 3924 + +Root motion is ignored since the root bone could not be located in scene. + +### 3925 + +Target of hierarchy path should be of type Node. + +### 3926 + +Node "%s" has no path "%s". + +### 3927 + +Target of component path should be of type Node. + +### 3928 + +Node "%s" has no component "%s". + +### 3929 + +Target object has no property "%s". + +### 3930 + +Can not decide type for untyped track: runtime binding does not provide a getter. + +### 3931 + +Can not decide type for untyped track: got a unsupported value from runtime binding. + +### 3932 + +Common targets should only target Vectors/`Size`/`Color`. + +### 3933 + +Each curve that has common target should be numeric curve and targets string property. + +### 3934 + +Misconfigured legacy curve: the first keyframe value is number but others aren't. + +### 3935 + +We don't currently support conversion of \`CubicSplineQuatValue\`. + ### 4000 diff --git a/cocos/core/animation/animation-clip.ts b/cocos/core/animation/animation-clip.ts index 026e69d5d22..5798d97544e 100644 --- a/cocos/core/animation/animation-clip.ts +++ b/cocos/core/animation/animation-clip.ts @@ -31,7 +31,7 @@ import { ccclass, serializable } from 'cc.decorator'; import { Asset } from '../assets/asset'; import { SpriteFrame } from '../../2d/assets/sprite-frame'; -import { error, errorID, warn } from '../platform/debug'; +import { error, errorID, getError, warn, warnID } from '../platform/debug'; import { DataPoolManager } from '../../3d/skeletal-animation/data-pool-manager'; import { binarySearchEpsilon } from '../algorithm/binary-search'; import { murmurhash2_32_gc } from '../utils/murmurhash2_gc'; @@ -347,7 +347,7 @@ export class AnimationClip extends Asset { const parentJointName = joint.substring(0, parentJoint); const parentJointFrame = skeletonFrames[parentJointName]; if (!parentJointFrame) { - warn(`Seems like we have animation for ${joint} but are missing its parent joint ${parentJointName} in animation?`); + warnID(3922, joint, parentJointName); } else { skeletonFrame.parent = parentJointFrame; } @@ -614,19 +614,19 @@ export class AnimationClip extends Asset { rootMotionTrackExcludes: Track[], ) { if (!(target instanceof Node)) { - error(`Current context does not allow root motion.`); + errorID(3920); return undefined; } const rootBonePath = this._searchForRootBonePath(); if (!rootBonePath) { - warn(`Root motion is ignored since root bone could not be located in animation.`); + warnID(3923); return undefined; } const rootBone = target.getChildByPath(rootBonePath); if (!rootBone) { - warn(`Root motion is ignored since the root bone could not be located in scene.`); + warnID(3924); return undefined; } diff --git a/cocos/core/animation/legacy-clip-data.ts b/cocos/core/animation/legacy-clip-data.ts index de42895bf82..d215e468965 100644 --- a/cocos/core/animation/legacy-clip-data.ts +++ b/cocos/core/animation/legacy-clip-data.ts @@ -9,7 +9,7 @@ import { QuatInterpolationMode, RealCurve, RealInterpolationMode, RealKeyframeVa import { assertIsTrue } from '../data/utils/asserts'; import { Track, TrackPath } from './tracks/track'; import { UntypedTrack } from './tracks/untyped-track'; -import { warn } from '../platform'; +import { warn, warnID } from '../platform'; import { RealTrack } from './tracks/real-track'; import { Color, lerp, Quat, Size, Vec2, Vec3, Vec4 } from '../math'; import { CubicSplineNumberValue, CubicSplineQuatValue, CubicSplineVec2Value, CubicSplineVec3Value, CubicSplineVec4Value } from './cubic-spline-value'; @@ -236,12 +236,12 @@ export class AnimationClipLegacyData { if (typeof legacyCurve.commonTarget === 'number') { // Rule: common targets should only target Vectors/`Size`/`Color`. if (!legacyValues.every((value) => typeof value === 'number')) { - warn(`Incorrect curve.`); + warnID(3932); continue; } // Rule: Each curve that has common target should be numeric curve and targets string property. if (legacyCurve.valueAdapter || legacyCurve.modifiers.length !== 1 || typeof legacyCurve.modifiers[0] !== 'string') { - warn(`Incorrect curve.`); + warnID(3933); continue; } const propertyName = legacyCurve.modifiers[0]; @@ -253,7 +253,7 @@ export class AnimationClipLegacyData { const convertCurve = () => { if (typeof firstValue === 'number') { if (!legacyValues.every((value) => typeof value === 'number')) { - warn(`Misconfigured curve.`); + warnID(3934); return; } let realCurve: RealCurve; @@ -406,7 +406,7 @@ export class AnimationClipLegacyData { return; } case legacyValues.every((value) => value instanceof CubicSplineQuatValue): { - warn(`We don't currently support conversion of \`CubicSplineQuatValue\`.`); + warnID(3935); break; } } // End switch diff --git a/cocos/core/animation/target-path.ts b/cocos/core/animation/target-path.ts index 3e0cadd3588..323d5a658b6 100644 --- a/cocos/core/animation/target-path.ts +++ b/cocos/core/animation/target-path.ts @@ -30,7 +30,7 @@ import { ccclass, serializable } from 'cc.decorator'; import { Node } from '../scene-graph/node'; -import { warn } from '../platform/debug'; +import { warn, warnID } from '../platform/debug'; export type PropertyPath = string | number; @@ -63,12 +63,12 @@ export class HierarchyPath implements ICustomTargetPath { public get (target: Node) { if (!(target instanceof Node)) { - warn(`Target of hierarchy path should be of type Node.`); + warnID(3925); return null; } const result = target.getChildByPath(this.path); if (!result) { - warn(`Node "${target.name}" has no path "${this.path}"`); + warnID(3926, target.name, this.path); return null; } return result; @@ -86,12 +86,12 @@ export class ComponentPath implements ICustomTargetPath { public get (target: Node) { if (!(target instanceof Node)) { - warn(`Target of component path should be of type Node.`); + warnID(3927); return null; } const result = target.getComponent(this.component); if (!result) { - warn(`Node "${target.name}" has no component "${this.component}"`); + warnID(3928, target.name, this.component); return null; } return result; diff --git a/cocos/core/animation/tracks/size-track.ts b/cocos/core/animation/tracks/size-track.ts index dd748f203fe..d9871b3459e 100644 --- a/cocos/core/animation/tracks/size-track.ts +++ b/cocos/core/animation/tracks/size-track.ts @@ -34,7 +34,7 @@ export class SizeTrack extends Track { private _channels: [RealChannel, RealChannel]; } -class SizeTrackEval { +export class SizeTrackEval { constructor ( private _width: RealCurve | undefined, private _height: RealCurve | undefined, diff --git a/cocos/core/animation/tracks/track.ts b/cocos/core/animation/tracks/track.ts index eb845e42fe8..b49b8863024 100644 --- a/cocos/core/animation/tracks/track.ts +++ b/cocos/core/animation/tracks/track.ts @@ -2,7 +2,7 @@ import { ccclass, serializable, uniquelyReferenced } from 'cc.decorator'; import type { Component } from '../../components'; import type { ObjectCurve, QuatCurve, RealCurve } from '../../curves'; import { assertIsTrue } from '../../data/utils/asserts'; -import { error, warn } from '../../platform'; +import { error, errorID, warn, warnID } from '../../platform'; import { Node } from '../../scene-graph'; import { js } from '../../utils/js'; import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; @@ -278,7 +278,7 @@ class TrackPath { const path = paths[iPath]; if (isPropertyPath(path)) { if (!(path in (result as any))) { - warn(`Target object has no property "${path}"`); + warnID(3929, path); return null; } else { if (poseOutput && iPath === endIndex - 1 && result instanceof Node && isTrsPropertyName(path)) { @@ -346,10 +346,7 @@ export class TrackBinding { }, }; } else if (!proxy) { - error( - `You provided a ill-formed track path.` - + `The last component of track path should be property key, or the setter should not be empty.`, - ); + errorID(3921); return null; } else { const resultTarget = path[normalizedFollowTag](target, 0, nPaths, poseOutput, isConstant); diff --git a/cocos/core/animation/tracks/untyped-track.ts b/cocos/core/animation/tracks/untyped-track.ts index ee5c7a93c5d..5c6482c97b9 100644 --- a/cocos/core/animation/tracks/untyped-track.ts +++ b/cocos/core/animation/tracks/untyped-track.ts @@ -1,9 +1,11 @@ import { ccclass, serializable } from 'cc.decorator'; import { RealCurve } from '../../curves'; import { Color, Size, Vec2, Vec3, Vec4 } from '../../math'; +import { getError } from '../../platform'; import { CLASS_NAME_PREFIX_ANIM, createEvalSymbol } from '../define'; import { IValueProxyFactory } from '../value-proxy'; import { ColorTrack, ColorTrackEval } from './color-track'; +import { SizeTrackEval } from './size-track'; import { Channel, RealChannel, RuntimeBinding, Track, TrackPath } from './track'; import { Vec2TrackEval, Vec3TrackEval, Vec4TrackEval, VectorTrack } from './vector-track'; @@ -28,14 +30,13 @@ export class UntypedTrack extends Track { public [createEvalSymbol] (runtimeBinding: RuntimeBinding) { if (!runtimeBinding.getValue) { - throw new Error(`Can not decide type for untyped track: runtime binding does not provide a getter.`); + throw new Error(getError(3930)); } const trySearchCurve = (property: string) => this._channels.find((channel) => channel.property === property)?.curve; const value = runtimeBinding.getValue(); switch (true) { - case value instanceof Size: default: - throw new Error(`Can not decide type for untyped track: got a unsupported value from runtime binding.`); + throw new Error(getError(3931)); case value instanceof Vec2: return new Vec2TrackEval( trySearchCurve('x'), @@ -62,6 +63,11 @@ export class UntypedTrack extends Track { trySearchCurve('b'), trySearchCurve('a'), ); + case value instanceof Size: + return new SizeTrackEval( + trySearchCurve('width'), + trySearchCurve('height'), + ); } } From b95b68b0acd589279f635cee1bbd200e41e6c23d Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Thu, 22 Jul 2021 14:42:44 +0800 Subject: [PATCH 34/35] Fix RealKeyframeValue editor extras --- cocos/core/curves/curve.ts | 26 ++++++++++++++++++++++---- tests/curves/curve.test.ts | 4 +++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/cocos/core/curves/curve.ts b/cocos/core/curves/curve.ts index 1d012f153ee..af5f45a16c9 100644 --- a/cocos/core/curves/curve.ts +++ b/cocos/core/curves/curve.ts @@ -5,8 +5,8 @@ import { ccclass, serializable, uniquelyReferenced } from '../data/decorators'; import { RealInterpolationMode, ExtrapolationMode, TangentWeightMode } from './real-curve-param'; import { binarySearchEpsilon } from '../algorithm/binary-search'; import { solveCubic } from './solve-cubic'; -import { EditorExtendableMixin } from '../data/editor-extendable'; -import { deserializeTag, SerializationContext, SerializationInput, SerializationOutput, serializeTag } from '../data'; +import { EditorExtendable, EditorExtendableMixin } from '../data/editor-extendable'; +import { deserializeTag, editorExtrasTag, SerializationContext, SerializationInput, SerializationOutput, serializeTag } from '../data'; import { DeserializationContext } from '../data/custom-serializable'; import * as easing from '../animation/easing'; @@ -65,7 +65,7 @@ export enum EasingMethod { */ @ccclass('cc.RealKeyframeValue') @uniquelyReferenced -class RealKeyframeValue { +class RealKeyframeValue extends EditorExtendable { constructor ({ interpolationMode, tangentWeightMode, @@ -75,7 +75,9 @@ class RealKeyframeValue { leftTangent, leftTangentWeight, easingMethod, + [editorExtrasTag]: editorExtras, }: Partial = { }) { + super(); this.value = value ?? this.value; this.rightTangent = rightTangent ?? this.rightTangent; this.rightTangentWeight = rightTangentWeight ?? this.rightTangentWeight; @@ -84,6 +86,9 @@ class RealKeyframeValue { this.interpolationMode = interpolationMode ?? this.interpolationMode; this.tangentWeightMode = tangentWeightMode ?? this.tangentWeightMode; this.easingMethod = easingMethod ?? this.easingMethod; + if (editorExtras) { + this[editorExtrasTag] = editorExtras; + } } /** @@ -164,7 +169,7 @@ function createRealKeyframeValue (params: RealKeyframeValueParameters) { * Curve. */ @ccclass('cc.RealCurve') -export class RealCurve extends EditorExtendableMixin>(KeyframeCurve) { +export class RealCurve extends KeyframeCurve { /** * Gets or sets the operation should be taken * if input time is less than the time of first keyframe when evaluating this curve. @@ -355,6 +360,11 @@ export class RealCurve extends EditorExtendableMixin keyframeValue[editorExtrasTag]); + if (keyframeValueEditorExtras.some((extras) => extras !== undefined)) { + output.writeProperty(`keyframeValueEditorExtras`, keyframeValueEditorExtras); + } } public [deserializeTag] (input: SerializationInput, context: DeserializationContext) { @@ -390,6 +400,14 @@ export class RealCurve extends EditorExtendableMixin keyframeValues[index][editorExtrasTag] = extras, + ); + } + this._times = times; this._values = keyframeValues; } diff --git a/tests/curves/curve.test.ts b/tests/curves/curve.test.ts index de225203551..2dd13575b2b 100644 --- a/tests/curves/curve.test.ts +++ b/tests/curves/curve.test.ts @@ -1,4 +1,4 @@ -import { toRadian } from '../../cocos/core'; +import { editorExtrasTag, toRadian } from '../../cocos/core'; import { RealCurve, RealInterpolationMode } from '../../cocos/core/curves'; import { EasingMethod, RealKeyframeValue } from '../../cocos/core/curves/curve'; import { ExtrapolationMode, TangentWeightMode } from '../../cocos/core/curves/real-curve-param'; @@ -62,6 +62,7 @@ describe('Curve', () => { interpolationMode: RealInterpolationMode.CUBIC, tangentWeightMode: TangentWeightMode.BOTH, easingMethod: EasingMethod.QUAD_OUT, + [editorExtrasTag]: { 'foo': 'bar' }, }), ]); compareCurves(serializeAndDeserialize(curve, RealCurve), curve); @@ -279,5 +280,6 @@ function compareCurves (left: RealCurve, right: RealCurve, numDigits = 2) { expect(leftKeyframeValue.leftTangentWeight).toBeCloseTo(rightKeyframeValue.leftTangentWeight, numDigits); expect(leftKeyframeValue.interpolationMode).toStrictEqual(rightKeyframeValue.interpolationMode); expect(leftKeyframeValue.easingMethod).toStrictEqual(rightKeyframeValue.easingMethod); + expect(leftKeyframeValue[editorExtrasTag]).toStrictEqual(rightKeyframeValue[editorExtrasTag]); } } \ No newline at end of file From 744aa4fe704f5ffe9806b6475fd04b0dbd091358 Mon Sep 17 00:00:00 2001 From: shrinktofit Date: Fri, 23 Jul 2021 10:56:47 +0800 Subject: [PATCH 35/35] Elimanate for-of forEach --- cocos/core/animation/animation-clip.ts | 94 ++++++++++++++++---------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/cocos/core/animation/animation-clip.ts b/cocos/core/animation/animation-clip.ts index 5798d97544e..dc13393d0b0 100644 --- a/cocos/core/animation/animation-clip.ts +++ b/cocos/core/animation/animation-clip.ts @@ -191,7 +191,9 @@ export class AnimationClip extends Asset { const ratios: number[] = []; const eventGroups: IAnimationEventGroup[] = []; const events = this.events.sort((a, b) => a.frame - b.frame); - for (const eventData of events) { + const nEvents = events.length; + for (let iEvent = 0; iEvent < nEvents; ++iEvent) { + const eventData = events[iEvent]; const ratio = eventData.frame / this._duration; let i = ratios.findIndex((r) => r === ratio); if (i < 0) { @@ -231,11 +233,14 @@ export class AnimationClip extends Asset { */ public range () { const range: Range = { min: Infinity, max: -Infinity }; - this._tracks.forEach((track) => { + const { _tracks: tracks } = this; + const nTracks = tracks.length; + for (let iTrack = 0; iTrack < nTracks; ++iTrack) { + const track = tracks[iTrack]; const trackRange = track.range(); range.min = Math.min(range.min, trackRange.min); range.max = Math.max(range.max, trackRange.max); - }); + } return range; } @@ -330,17 +335,18 @@ export class AnimationClip extends Asset { const jointsBakeInfo: Record = {}; - animatedJoints.forEach((joint) => { + for (let iAnimatedJoint = 0; iAnimatedJoint < nAnimatedJoints; ++iAnimatedJoint) { + const joint = animatedJoints[iAnimatedJoint]; jointsBakeInfo[joint] = { transforms: Array.from({ length: frames }, () => new Mat4()), }; - }); + } const skeletonFrames = animatedJoints.reduce((result, joint) => { result[joint] = new BoneGlobalTransform(); return result; }, {} as Record); - for (const joint of Object.keys(skeletonFrames)) { + for (const joint in skeletonFrames) { const skeletonFrame = skeletonFrames[joint]; const parentJoint = joint.lastIndexOf('/'); if (parentJoint >= 0) { @@ -403,20 +409,24 @@ export class AnimationClip extends Asset { public upgradeUntypedTracks (refine: UntypedTrackRefine) { const newTracks: Track[] = []; const removals: Track[] = []; - this._tracks.forEach((track) => { + const { _tracks: tracks } = this; + const nTracks = tracks.length; + for (let iTrack = 0; iTrack < nTracks; ++iTrack) { + const track = tracks[iTrack]; if (!(track instanceof UntypedTrack)) { - return; + continue; } const newTrack = track.upgrade(refine); if (newTrack) { newTracks.push(newTrack); removals.push(track); } - }); - removals.forEach((removal) => { - array.remove(this._tracks, removal); - }); - this._tracks.push(...newTracks); + } + const nRemovalTracks = removals.length; + for (let iRemovalTrack = 0; iRemovalTrack < nRemovalTracks; ++iRemovalTrack) { + array.remove(tracks, removals[iRemovalTrack]); + } + tracks.push(...newTracks); } /** @@ -580,20 +590,23 @@ export class AnimationClip extends Asset { const trackEvalStatues: TrackEvalStatus[] = []; let exoticAnimationEvaluator: ExoticAnimationEvaluator | undefined; - this._tracks.forEach((track) => { + const { _tracks: tracks } = this; + const nTracks = tracks.length; + for (let iTrack = 0; iTrack < nTracks; ++iTrack) { + const track = tracks[iTrack]; if (rootMotionTrackExcludes.includes(track)) { - return; + continue; } const trackTarget = binder(track[trackBindingTag]); if (!trackTarget) { - return; + continue; } const trackEval = track[createEvalSymbol](trackTarget); trackEvalStatues.push({ binding: trackTarget, trackEval, }); - }); + } if (this._exoticAnimation) { exoticAnimationEvaluator = this._exoticAnimation.createEvaluator(binder); @@ -634,28 +647,31 @@ export class AnimationClip extends Asset { const boneTransform = new BoneTransform(); const rootMotionsTrackEvaluations: TrackEvalStatus[] = []; - this._tracks.forEach((track) => { + const { _tracks: tracks } = this; + const nTracks = tracks.length; + for (let iTrack = 0; iTrack < nTracks; ++iTrack) { + const track = tracks[iTrack]; const { [trackBindingTag]: trackBinding } = track; const trsPath = trackBinding.parseTrsPath(); if (!trsPath) { - return; + continue; } const bonePath = trsPath.node; if (bonePath !== rootBonePath) { - return; + continue; } rootMotionTrackExcludes.push(track); const property = trsPath.property; const trackTarget = createBoneTransformBinding(boneTransform, property); if (!trackTarget) { - return; + continue; } const trackEval = track[createEvalSymbol](trackTarget); rootMotionsTrackEvaluations.push({ binding: trackTarget, trackEval, }); - }); + } const rootMotionEvaluation = new RootMotionEvaluation( rootBone, this._duration, @@ -728,25 +744,31 @@ export class AnimationClip extends Asset { private _fromLegacy (legacyData: legacy.AnimationClipLegacyData) { const newTracks = legacyData.toTracks(); - newTracks.forEach((track) => { - this.addTrack(track); - }); + const nNewTracks = newTracks.length; + for (let iNewTrack = 0; iNewTrack < nNewTracks; ++iNewTrack) { + this.addTrack(newTracks[iNewTrack]); + } } private _collectAnimatedJoints () { const joints = new Set(); - this._tracks.forEach((track) => { + const { _tracks: tracks } = this; + const nTracks = tracks.length; + for (let iTrack = 0; iTrack < nTracks; ++iTrack) { + const track = tracks[iTrack]; const trsPath = track[trackBindingTag].parseTrsPath(); if (trsPath) { joints.add(trsPath.node); } - }); + } if (this._exoticAnimation) { - this._exoticAnimation.collectAnimatedJoints().forEach((joint) => { - joints.add(joint); - }); + const animatedJoints = this._exoticAnimation.collectAnimatedJoints(); + const nAnimatedJoints = animatedJoints.length; + for (let iAnimatedJoint = 0; iAnimatedJoint < nAnimatedJoints; ++iAnimatedJoint) { + joints.add(animatedJoints[iAnimatedJoint]); + } } return Array.from(joints); @@ -1148,15 +1170,19 @@ class EventEvaluator { const eventGroup = eventGroups[eventIndex]; const components = this._targetNode.components; - eventGroup.events.forEach((event) => { + const nEvents = eventGroup.events.length; + for (let iEvent = 0; iEvent < nEvents; ++iEvent) { + const event = eventGroup.events[iEvent]; const { functionName } = event; - components.forEach((component) => { + const nComponents = components.length; + for (let iComponent = 0; iComponent < nComponents; ++iComponent) { + const component = components[iComponent]; const fx = component[functionName]; if (typeof fx === 'function') { fx.apply(component, event.parameters); } - }); - }); + } + } } }