diff --git a/packages/effects-core/NOTICE b/packages/effects-core/NOTICE index 48a701a20..59e17dc4b 100644 --- a/packages/effects-core/NOTICE +++ b/packages/effects-core/NOTICE @@ -48,6 +48,32 @@ Repository: https://github.com/ampas/aces-dev WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, THE ACADEMY SPECIFICALLY DISCLAIMS ANY REPRESENTATIONS OR WARRANTIES WHATSOEVER RELATED TO PATENT OR OTHER INTELLECTUAL PROPERTY RIGHTS IN THE ACADEMY COLOR ENCODING SYSTEM, OR APPLICATIONS THEREOF, HELD BY PARTIES OTHER THAN A.M.P.A.S.,WHETHER DISCLOSED OR UNDISCLOSED. +3. pixijs + +License: [MIT License](https://github.com/pixijs/pixijs/blob/dev/LICENSE) + +Repository: https://github.com/pixijs/pixijs + + Copyright (c) 2013-2023 Mathew Groves, Chad Engler + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 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. + Please refer to the corresponding repository for more information on each component's license and terms of use. Galacean Effects Core Library is distributed under the MIT License. Please see the LICENSE file for the full text of the license. diff --git a/packages/effects-core/package.json b/packages/effects-core/package.json index 5c15d0de3..fca106e15 100644 --- a/packages/effects-core/package.json +++ b/packages/effects-core/package.json @@ -51,9 +51,10 @@ "registry": "https://registry.npmjs.org" }, "dependencies": { - "@galacean/effects-specification": "2.0.2", + "@galacean/effects-specification": "2.1.0", "@galacean/effects-math": "1.1.0", "flatbuffers": "24.3.25", - "uuid": "9.0.1" + "uuid": "9.0.1", + "libtess": "1.2.2" } } diff --git a/packages/effects-core/src/animation/color-playable.ts b/packages/effects-core/src/animation/color-playable.ts new file mode 100644 index 000000000..44b290a79 --- /dev/null +++ b/packages/effects-core/src/animation/color-playable.ts @@ -0,0 +1,85 @@ +import * as spec from '@galacean/effects-specification'; +import { createValueGetter, vecFill, vecMulCombine, type ValueGetter } from '../math'; +import type { FrameContext } from '../plugins/cal/playable-graph'; +import { Playable } from '../plugins/cal/playable-graph'; +import { VFXItem } from '../vfx-item'; +import type { Material } from '../material'; +import type { ColorStop } from '../utils'; +import { colorStopsFromGradient, getColorFromGradientStops } from '../utils'; +import { BaseRenderComponent } from '../components'; + +export interface ColorPlayableAssetData extends spec.EffectsObjectData { + colorOverLifetime?: spec.ColorOverLifetime, +} + +const tempColor: spec.RGBAColorValue = [1, 1, 1, 1]; + +export class ColorPlayable extends Playable { + clipData: { colorOverLifetime?: spec.ColorOverLifetime, startColor?: spec.RGBAColorValue }; + colorOverLifetime: ColorStop[]; + opacityOverLifetime: ValueGetter; + startColor: spec.RGBAColorValue; + renderColor: spec.vec4 = [1, 1, 1, 1]; + activeComponent?: BaseRenderComponent; + activeMaterial?: Material; + + override processFrame (context: FrameContext): void { + const boundObject = context.output.getUserData(); + + if (!(boundObject instanceof VFXItem)) { + return; + } + if (!this.activeComponent) { + this.activeComponent = this.getActiveComponent(boundObject); + } + if (!this.activeMaterial) { + this.activeMaterial = this.activeComponent?.material; + const startColor = this.activeMaterial?.getVector4('_Color'); + + if (startColor) { + this.startColor = startColor.toArray(); + } + } + + this.activeComponent?.setAnimationTime(this.time); + let colorInc = vecFill(tempColor, 1); + let colorChanged; + const life = this.time / boundObject.duration; + + const opacityOverLifetime = this.opacityOverLifetime; + const colorOverLifetime = this.colorOverLifetime; + + if (colorOverLifetime) { + colorInc = getColorFromGradientStops(colorOverLifetime, life, true) as spec.vec4; + colorChanged = true; + } + if (opacityOverLifetime) { + colorInc[3] *= opacityOverLifetime.getValue(life); + colorChanged = true; + } + + if (colorChanged) { + vecMulCombine(this.renderColor, colorInc, this.startColor); + this.activeMaterial?.getVector4('_Color')?.setFromArray(this.renderColor); + } + } + + create (clipData: ColorPlayableAssetData) { + this.clipData = clipData; + const colorOverLifetime = clipData.colorOverLifetime; + + if (colorOverLifetime) { + this.opacityOverLifetime = createValueGetter(colorOverLifetime.opacity ?? 1); + if (colorOverLifetime.color && colorOverLifetime.color[0] === spec.ValueType.GRADIENT_COLOR) { + this.colorOverLifetime = colorStopsFromGradient(colorOverLifetime.color[1]); + } + } + + return this; + } + + getActiveComponent (boundObject: VFXItem): BaseRenderComponent { + return boundObject.getComponent(BaseRenderComponent); + } + +} diff --git a/packages/effects-core/src/animation/index.ts b/packages/effects-core/src/animation/index.ts new file mode 100644 index 000000000..d53d95c7b --- /dev/null +++ b/packages/effects-core/src/animation/index.ts @@ -0,0 +1 @@ +export * from './color-playable'; diff --git a/packages/effects-core/src/asset-loader.ts b/packages/effects-core/src/asset-loader.ts index 5a81100b8..164d4661a 100644 --- a/packages/effects-core/src/asset-loader.ts +++ b/packages/effects-core/src/asset-loader.ts @@ -60,7 +60,7 @@ export class AssetLoader { effectsObject.setInstanceId(effectsObjectData.id); this.engine.addInstance(effectsObject); - SerializationHelper.deserializeTaggedProperties(effectsObjectData, effectsObject); + SerializationHelper.deserialize(effectsObjectData, effectsObject); return effectsObject as T; } @@ -124,7 +124,7 @@ export class AssetLoader { effectsObject.setInstanceId(effectsObjectData.id); this.engine.addInstance(effectsObject); - await SerializationHelper.deserializeTaggedPropertiesAsync(effectsObjectData, effectsObject); + await SerializationHelper.deserializeAsync(effectsObjectData, effectsObject); return effectsObject as T; } diff --git a/packages/effects-core/src/asset-manager.ts b/packages/effects-core/src/asset-manager.ts index 7bb8c37ef..695a93fc9 100644 --- a/packages/effects-core/src/asset-manager.ts +++ b/packages/effects-core/src/asset-manager.ts @@ -6,8 +6,8 @@ import type { PrecompileOptions } from './plugin-system'; import { PluginSystem } from './plugin-system'; import type { JSONValue } from './downloader'; import { Downloader, loadWebPOptional, loadImage, loadVideo, loadMedia, loadAVIFOptional } from './downloader'; -import type { ImageSource, Scene, SceneLoadOptions, SceneRenderLevel, SceneType } from './scene'; -import { isSceneJSON } from './scene'; +import type { ImageLike, SceneLoadOptions } from './scene'; +import { Scene } from './scene'; import type { Disposable } from './utils'; import { isObject, isString, logger, isValidFontFamily, isCanvas, base64ToFile } from './utils'; import type { TextureSourceOptions } from './texture'; @@ -15,7 +15,7 @@ import { deserializeMipmapTexture, TextureSourceType, getKTXTextureOptions, Text import type { Renderer } from './render'; import { COMPRESSED_TEXTURE } from './render'; import { combineImageTemplate, getBackgroundImage } from './template-image'; -import { ImageAsset } from './image-asset'; +import { Asset } from './asset'; import type { Engine } from './engine'; let seed = 1; @@ -26,18 +26,21 @@ let seed = 1; */ export class AssetManager implements Disposable { /** - * 相对url的基本路径 + * 相对 url 的基本路径 */ private baseUrl: string; /** - * 图像资源,用于创建和释放GPU纹理资源 + * 图像资源,用于创建和释放 GPU 纹理资源 */ - private assets: Record = {}; - + private assets: Record = {}; + /** + * TextureSource 来源 + */ + private sourceFrom: Record = {}; /** * 自定义文本缓存,随页面销毁而销毁 */ - static fonts: Set = new Set(); + private static fontCache: Set = new Set(); private id = seed++; /** @@ -56,7 +59,7 @@ export class AssetManager implements Disposable { * @param downloader - 资源下载对象 */ constructor ( - private options: SceneLoadOptions = {}, + public options: Omit = {}, private readonly downloader = new Downloader(), ) { this.updateOptions(options); @@ -80,16 +83,15 @@ export class AssetManager implements Disposable { * @returns */ async loadScene ( - url: SceneType, + url: Scene.LoadType, renderer?: Renderer, options?: { env: string }, ): Promise { - let rawJSON: SceneType | JSONValue; + let rawJSON: Scene.LoadType; const assetUrl = isString(url) ? url : this.id; const startTime = performance.now(); const timeInfoMessages: string[] = []; const gpuInstance = renderer?.engine.gpuCapability; - const asyncShaderCompile = gpuInstance?.detail?.asyncShaderCompile ?? false; const compressedTexture = gpuInstance?.detail.compressedTexture ?? COMPRESSED_TEXTURE.NONE; const timeInfos: Record = {}; let loadTimer: number; @@ -127,85 +129,64 @@ export class AssetManager implements Disposable { const loadResourcePromise = async () => { let scene: Scene; - // url 为 JSONValue 或 Scene 对象 - if (isObject(url)) { - // TODO: 原 JSONLoader contructor 判断是否兼容 + if (isString(url)) { + // 兼容相对路径 + const link = new URL(url, location.href).href; + + this.baseUrl = link; + rawJSON = await hookTimeInfo('loadJSON', () => this.loadJSON(link) as unknown as Promise); + } else { + // url 为 spec.JSONScene 或 Scene 对象 rawJSON = url; this.baseUrl = location.href; - } else { - // 兼容相对路径 - url = new URL(url as string, location.href).href; - this.baseUrl = url; - rawJSON = await hookTimeInfo('loadJSON', () => this.loadJSON(url as string)); } - if (isSceneJSON(rawJSON)) { + if (Scene.isJSONObject(rawJSON)) { // 已经加载过的 可能需要更新数据模板 scene = { ...rawJSON, }; - if ( - this.options && - this.options.variables && - Object.keys(this.options.variables).length !== 0 - ) { - const { images: rawImages } = rawJSON.jsonScene; - const images = scene.images; - - for (let i = 0; i < rawImages.length; i++) { - // 仅重新加载数据模板对应的图片 - if (images[i] instanceof HTMLCanvasElement) { - images[i] = rawImages[i]; - } - } - scene.images = await hookTimeInfo('processImages', () => this.processImages(images, compressedTexture)); - // 更新 TextureOptions 中的 image 指向 - for (let i = 0; i < scene.images.length; i++) { - scene.textureOptions[i].image = scene.images[i]; - } - } + const { jsonScene, pluginSystem, images: loadedImages } = scene; + const { compositions, images } = jsonScene; + + this.assignImagesToAssets(images, loadedImages); + await Promise.all([ + hookTimeInfo('plugin:processAssets', () => this.processPluginAssets(jsonScene, pluginSystem, options)), + hookTimeInfo('plugin:precompile', () => this.precompile(compositions, pluginSystem, renderer, options)), + ]); } else { // TODO: JSONScene 中 bins 的类型可能为 ArrayBuffer[] - const { usedImages, jsonScene, pluginSystem } = await hookTimeInfo('processJSON', () => this.processJSON(rawJSON as JSONValue)); + const { jsonScene, pluginSystem } = await hookTimeInfo('processJSON', () => this.processJSON(rawJSON as JSONValue)); const { bins = [], images, compositions, fonts } = jsonScene; const [loadedBins, loadedImages] = await Promise.all([ hookTimeInfo('processBins', () => this.processBins(bins)), hookTimeInfo('processImages', () => this.processImages(images, compressedTexture)), - hookTimeInfo(`${asyncShaderCompile ? 'async' : 'sync'}Compile`, () => this.precompile(compositions, pluginSystem, renderer, options)), + hookTimeInfo('plugin:processAssets', () => this.processPluginAssets(jsonScene, pluginSystem, options)), + hookTimeInfo('plugin:precompile', () => this.precompile(compositions, pluginSystem, renderer, options)), + hookTimeInfo('processFontURL', () => this.processFontURL(fonts as spec.FontDefine[])), ]); - - if (renderer) { - for (let i = 0; i < images.length; i++) { - const imageAsset = new ImageAsset(renderer.engine); - - imageAsset.data = loadedImages[i]; - imageAsset.setInstanceId(images[i].id); - renderer.engine.addInstance(imageAsset); - } - } - - await hookTimeInfo('processFontURL', () => this.processFontURL(fonts as spec.FontDefine[])); - const loadedTextures = await hookTimeInfo('processTextures', () => this.processTextures(loadedImages, loadedBins, jsonScene, renderer!.engine)); + const loadedTextures = await hookTimeInfo('processTextures', () => this.processTextures(loadedImages, loadedBins, jsonScene)); scene = { timeInfos, - url: url, + url, renderLevel: this.options.renderLevel, storage: {}, pluginSystem, jsonScene, - usedImages, + bins: loadedBins, images: loadedImages, textureOptions: loadedTextures, - bins: loadedBins, }; // 触发插件系统 pluginSystem 的回调 prepareResource - await hookTimeInfo('processPlugins', () => pluginSystem.loadResources(scene, this.options)); + await hookTimeInfo('plugin:prepareResource', () => pluginSystem.loadResources(scene, this.options)); } + await hookTimeInfo('prepareAssets', () => this.prepareAssets(renderer?.engine)); + const totalTime = performance.now() - startTime; logger.info(`Load asset: totalTime: ${totalTime.toFixed(4)}ms ${timeInfoMessages.join(' ')}, url: ${assetUrl}.`); @@ -224,45 +205,24 @@ export class AssetManager implements Disposable { private async precompile ( compositions: spec.CompositionData[], - pluginSystem?: PluginSystem, + pluginSystem: PluginSystem, renderer?: Renderer, options?: PrecompileOptions, ) { if (!renderer || !renderer.getShaderLibrary()) { return; } - const shaderLibrary = renderer?.getShaderLibrary(); - - await pluginSystem?.precompile(compositions, renderer, options); - - await new Promise(resolve => { - shaderLibrary?.compileAllShaders(() => { - resolve(null); - }); - }); + await pluginSystem.precompile(compositions, renderer, options); } private async processJSON (json: JSONValue) { const jsonScene = getStandardJSON(json); - const { plugins = [], compositions: sceneCompositions, imgUsage, images } = jsonScene; + const { plugins = [] } = jsonScene; const pluginSystem = new PluginSystem(plugins); await pluginSystem.processRawJSON(jsonScene, this.options); - const { renderLevel } = this.options; - const usedImages: Record = {}; - - if (imgUsage) { - // TODO: 考虑放到独立的 fix 文件 - fixOldImageUsage(usedImages, sceneCompositions, imgUsage, images, renderLevel); - } else { - images?.forEach((_, i) => { - usedImages[i] = true; - }); - } - return { - usedImages, jsonScene, pluginSystem, }; @@ -293,7 +253,7 @@ export class AssetManager implements Disposable { const jobs = fonts.map(async font => { // 数据模版兼容判断 - if (font.fontURL && !AssetManager.fonts.has(font.fontFamily)) { + if (font.fontURL && !AssetManager.fontCache.has(font.fontFamily)) { if (!isValidFontFamily(font.fontFamily)) { // 在所有设备上提醒开发者 console.warn(`Risky font family: ${font.fontFamily}.`); @@ -303,11 +263,9 @@ export class AssetManager implements Disposable { const fontFace = new FontFace(font.fontFamily ?? '', 'url(' + url + ')'); await fontFace.load(); - //@ts-expect-error document.fonts.add(fontFace); - AssetManager.fonts.add(font.fontFamily); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { + AssetManager.fontCache.add(font.fontFamily); + } catch (_) { logger.warn(`Invalid font family or font source: ${JSON.stringify(font.fontURL)}.`); } } @@ -317,12 +275,12 @@ export class AssetManager implements Disposable { } private async processImages ( - images: any, + images: spec.ImageSource[], compressedTexture: COMPRESSED_TEXTURE = 0, - ): Promise { + ): Promise { const { useCompressedTexture, variables } = this.options; const baseUrl = this.baseUrl; - const jobs = images.map(async (img: spec.Image, idx: number) => { + const jobs = images.map(async (img, idx: number) => { const { url: png, webp, avif } = img; // eslint-disable-next-line compat/compat const imageURL = new URL(png, baseUrl).href; @@ -333,7 +291,7 @@ export class AssetManager implements Disposable { if ('template' in img) { // 1. 数据模板 - const template = img.template as spec.TemplateContent; + const template = img.template; // 获取数据模板 background 参数 const background = template.background; @@ -348,6 +306,8 @@ export class AssetManager implements Disposable { const resultImage = await loadMedia(url as string | string[], loadFn); if (resultImage instanceof HTMLVideoElement) { + this.sourceFrom[idx] = { url: resultImage.src, type: TextureSourceType.video }; + return resultImage; } else { // 如果是加载图片且是数组,设置变量,视频情况下不需要 @@ -355,20 +315,21 @@ export class AssetManager implements Disposable { variables[background.name] = resultImage.src; } + this.sourceFrom[idx] = { url: resultImage.src, type: TextureSourceType.image }; + return await combineImageTemplate( resultImage, template, variables, ); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - throw new Error(`Failed to load. Check the template or if the URL is ${isVideo ? 'video' : 'image'} type, URL: ${url}, Error: ${(e as any).message || e}.`); + throw new Error(`Failed to load. Check the template or if the URL is ${isVideo ? 'video' : 'image'} type, URL: ${url}, Error: ${(e as Error).message || e}.`); } } } else if ('compressed' in img && useCompressedTexture && compressedTexture) { // 2. 压缩纹理 - const { compressed } = img as spec.CompressedImage; + const { compressed } = img; let src; if (compressedTexture === COMPRESSED_TEXTURE.ASTC) { @@ -379,13 +340,10 @@ export class AssetManager implements Disposable { if (src) { const bufferURL = new URL(src, baseUrl).href; - this.assets[idx] = { url: bufferURL, type: TextureSourceType.compressed }; + this.sourceFrom[idx] = { url: bufferURL, type: TextureSourceType.compressed }; return this.loadBins(bufferURL); } - } else if ('sourceType' in img) { - // TODO: 确定是否有用 - return img; } else if ( img instanceof HTMLImageElement || img instanceof HTMLCanvasElement || @@ -399,47 +357,79 @@ export class AssetManager implements Disposable { ? await loadAVIFOptional(imageURL, avifURL) : await loadWebPOptional(imageURL, webpURL); - this.assets[idx] = { url, type: TextureSourceType.image }; + this.sourceFrom[idx] = { url, type: TextureSourceType.image }; return image; }); + const loadedImages = await Promise.all(jobs); - return Promise.all(jobs); + this.assignImagesToAssets(images, loadedImages); + + return loadedImages; + } + + private async processPluginAssets ( + jsonScene: spec.JSONScene, + pluginSystem: PluginSystem, + options?: SceneLoadOptions, + ) { + const pluginResult = await pluginSystem.processAssets(jsonScene, options); + const { assets, loadedAssets } = pluginResult.reduce((acc, cur) => { + acc.assets = acc.assets.concat(cur.assets); + acc.loadedAssets = acc.loadedAssets.concat(cur.loadedAssets); + + return acc; + }, { assets: [], loadedAssets: [] }); + + for (let i = 0; i < assets.length; i++) { + this.assets[assets[i].id] = loadedAssets[i] as ImageLike; + } + } + + private async prepareAssets (engine?: Engine) { + if (!engine) { + return; + } + + for (const assetId of Object.keys(this.assets)) { + const asset = this.assets[assetId]; + const engineAsset = new Asset(engine); + + engineAsset.data = asset; + engineAsset.setInstanceId(assetId); + engine.addInstance(engineAsset); + } } private async processTextures ( - images: any, + images: ImageLike[], bins: ArrayBuffer[], jsonScene: spec.JSONScene, - engine: Engine ) { - const textures = jsonScene.textures ?? images.map((img: never, source: number) => ({ source })) as spec.SerializedTextureSource[]; + const textures = jsonScene.textures ?? images.map((img, source: number) => ({ source })) as spec.SerializedTextureSource[]; const jobs = textures.map(async (textureOptions, idx) => { if (textureOptions instanceof Texture) { return textureOptions; } if ('mipmaps' in textureOptions) { try { - return await deserializeMipmapTexture(textureOptions, bins, engine, jsonScene.bins); + return await deserializeMipmapTexture(textureOptions, bins, this.assets, jsonScene.bins); } catch (e) { throw new Error(`Load texture ${idx} fails, error message: ${e}.`); } } - const { source } = textureOptions; - let image: any; + const { source, id } = textureOptions; + let image: ImageLike | undefined; if (isObject(source)) { // source 为 images 数组 id - image = engine.assetLoader.loadGUID((source as Record).id).data; + image = this.assets[source.id as string]; } else if (typeof source === 'string') { // source 为 base64 数据 image = await loadImage(base64ToFile(source)); } if (image) { - const texture = createTextureOptionsBySource(image, this.assets[idx]); - - texture.id = textureOptions.id; - texture.dataType = spec.DataType.Texture; + const texture = createTextureOptionsBySource(image, this.sourceFrom[idx], id); return texture.sourceType === TextureSourceType.compressed ? texture : { ...texture, ...textureOptions }; } @@ -471,6 +461,12 @@ export class AssetManager implements Disposable { }); } + private assignImagesToAssets (images: spec.ImageSource[], loadedImages: ImageLike[]) { + for (let i = 0; i < images.length; i++) { + this.assets[images[i].id] = loadedImages[i]; + } + } + private removeTimer (id: number) { const index = this.timers.indexOf(id); @@ -485,46 +481,30 @@ export class AssetManager implements Disposable { if (this.timers.length) { this.timers.map(id => window.clearTimeout(id)); } - for (const key in this.assets) { - const asset = this.assets[key]; - - asset?.dispose?.(); - } this.assets = {}; + this.sourceFrom = {}; this.timers = []; } } -function fixOldImageUsage ( - usedImages: Record, - compositions: spec.CompositionData[], - imgUsage: Record, - images: any, - renderLevel?: SceneRenderLevel, +function createTextureOptionsBySource ( + image: TextureSourceOptions | ImageLike, + sourceFrom: { url: string, type: TextureSourceType }, + id?: string, ) { - for (let i = 0; i < compositions.length; i++) { - const id = compositions[i].id; - const ids = imgUsage[id]; + const options = { + id, + dataType: spec.DataType.Texture, + }; - if (ids) { - for (let j = 0; j < ids.length; j++) { - const id = ids[j]; - const tag = images[id].renderLevel; - - if (passRenderLevel(tag, renderLevel)) { - usedImages[id] = true; - } - } - } - } -} - -function createTextureOptionsBySource (image: any, sourceFrom: TextureSourceOptions): Record { if (image instanceof Texture) { - return image.source; + return { + ...image.source, + ...options, + }; } else if ( image instanceof HTMLImageElement || - isCanvas(image) + isCanvas(image as HTMLCanvasElement) ) { return { image, @@ -533,6 +513,7 @@ function createTextureOptionsBySource (image: any, sourceFrom: TextureSourceOpti keepImageSource: true, minFilter: glContext.LINEAR, magFilter: glContext.LINEAR, + ...options, }; } else if (image instanceof HTMLVideoElement) { // 视频 @@ -541,12 +522,14 @@ function createTextureOptionsBySource (image: any, sourceFrom: TextureSourceOpti video: image, minFilter: glContext.LINEAR, magFilter: glContext.LINEAR, + ...options, }; } else if (image instanceof ArrayBuffer) { // 压缩纹理 return { ...getKTXTextureOptions(image), sourceFrom, + ...options, }; } else if ( 'width' in image && @@ -560,6 +543,7 @@ function createTextureOptionsBySource (image: any, sourceFrom: TextureSourceOpti wrapT: glContext.CLAMP_TO_EDGE, minFilter: glContext.NEAREST, magFilter: glContext.NEAREST, + ...options, }; } diff --git a/packages/effects-core/src/asset.ts b/packages/effects-core/src/asset.ts new file mode 100644 index 000000000..7fccea59a --- /dev/null +++ b/packages/effects-core/src/asset.ts @@ -0,0 +1,5 @@ +import { EffectsObject } from './effects-object'; + +export class Asset extends EffectsObject { + data: T; +} diff --git a/packages/effects-core/src/binary-asset.ts b/packages/effects-core/src/binary-asset.ts index 230aa491d..935ac4013 100644 --- a/packages/effects-core/src/binary-asset.ts +++ b/packages/effects-core/src/binary-asset.ts @@ -1,13 +1,13 @@ -import type { EffectsObjectData } from '@galacean/effects-specification'; +import * as spec from '@galacean/effects-specification'; import { EffectsObject } from './effects-object'; import { effectsClass, serialize } from './decorators'; -@effectsClass('BinaryAsset') +@effectsClass(spec.DataType.BinaryAsset) export class BinaryAsset extends EffectsObject { @serialize() buffer: ArrayBuffer; - override fromData (data: EffectsObjectData): void { + override fromData (data: spec.EffectsObjectData): void { } -} \ No newline at end of file +} diff --git a/packages/effects-core/src/comp-vfx-item.ts b/packages/effects-core/src/comp-vfx-item.ts index 3ff5f3de9..1f87bc6bd 100644 --- a/packages/effects-core/src/comp-vfx-item.ts +++ b/packages/effects-core/src/comp-vfx-item.ts @@ -4,15 +4,14 @@ import { Vector3 } from '@galacean/effects-math/es/core/vector3'; import * as spec from '@galacean/effects-specification'; import { Behaviour } from './components'; import type { CompositionHitTestOptions } from './composition'; -import type { ContentOptions } from './composition-source-manager'; import type { Region, TrackAsset } from './plugins'; -import { HitTestType, ObjectBindingTrack } from './plugins'; +import { HitTestType } from './plugins'; import type { Playable } from './plugins/cal/playable-graph'; import { PlayableGraph } from './plugins/cal/playable-graph'; -import { TimelineAsset } from './plugins/cal/timeline-asset'; -import { Transform } from './transform'; +import { TimelineAsset } from './plugins/timeline'; import { generateGUID, noop } from './utils'; -import { Item, VFXItem } from './vfx-item'; +import { VFXItem } from './vfx-item'; +import { SerializationHelper } from './serialization-helper'; export interface SceneBinding { key: TrackAsset, @@ -30,9 +29,7 @@ export interface SceneBindingData { export class CompositionComponent extends Behaviour { time = 0; startTime = 0; - refId: string; items: VFXItem[] = []; // 场景的所有元素 - data: ContentOptions; private reusable = false; private sceneBindings: SceneBinding[] = []; @@ -40,10 +37,10 @@ export class CompositionComponent extends Behaviour { private timelinePlayable: Playable; private graph: PlayableGraph = new PlayableGraph(); - override start (): void { - const { startTime = 0 } = this.item.props; - - this.startTime = startTime; + override onStart (): void { + if (!this.timelineAsset) { + this.timelineAsset = new TimelineAsset(this.engine); + } this.resolveBindings(); this.timelinePlayable = this.timelineAsset.createPlayable(this.graph); @@ -55,13 +52,10 @@ export class CompositionComponent extends Behaviour { setReusable (value: boolean) { for (const track of this.timelineAsset.tracks) { - const binding = track.binding; + const boundObject = track.boundObject; - if (binding instanceof VFXItem) { - if (track instanceof ObjectBindingTrack) { - binding.reusable = value; - } - const subCompositionComponent = binding.getComponent(CompositionComponent); + if (boundObject instanceof VFXItem) { + const subCompositionComponent = boundObject.getComponent(CompositionComponent); if (subCompositionComponent) { subCompositionComponent.setReusable(value); @@ -74,7 +68,7 @@ export class CompositionComponent extends Behaviour { return this.reusable; } - override update (dt: number): void { + override onUpdate (dt: number): void { const time = this.time; this.timelinePlayable.setTime(time); @@ -82,50 +76,23 @@ export class CompositionComponent extends Behaviour { } createContent () { - const sceneBindings = []; - - for (const sceneBindingData of this.data.sceneBindings) { - sceneBindings.push({ - key: this.engine.assetLoader.loadGUID(sceneBindingData.key.id), - value: this.engine.assetLoader.loadGUID(sceneBindingData.value.id), - }); - } - this.sceneBindings = sceneBindings; - const timelineAsset = this.data.timelineAsset ? this.engine.assetLoader.loadGUID(this.data.timelineAsset.id) : new TimelineAsset(this.engine); - - this.timelineAsset = timelineAsset; - const items = this.items; - - this.items.length = 0; if (this.item.composition) { - const assetLoader = this.item.engine.assetLoader; - const itemProps = this.data.items ? this.data.items : []; - - for (let i = 0; i < itemProps.length; i++) { - let item: VFXItem; - const itemData = itemProps[i]; + for (const item of this.items) { + item.composition = this.item.composition; // 设置预合成作为元素时的时长、结束行为和渲染延时 - if (Item.isComposition(itemData)) { - const refId = itemData.content.options.refId; + if (VFXItem.isComposition(item)) { + this.item.composition.refContent.push(item); + const compositionContent = item.props.content as unknown as spec.CompositionContent; + const refId = compositionContent.options.refId; const props = this.item.composition.refCompositionProps.get(refId); if (!props) { throw new Error(`Referenced precomposition with Id: ${refId} does not exist.`); } - // endBehavior 类型需优化 - props.content = itemData.content; - item = assetLoader.loadGUID(itemData.id); - item.composition = this.item.composition; const compositionComponent = item.addComponent(CompositionComponent); - compositionComponent.data = props as unknown as ContentOptions; - compositionComponent.refId = refId; - item.transform.parentTransform = this.transform; - this.item.composition.refContent.push(item); - if (item.endBehavior === spec.EndBehavior.restart) { - this.item.composition.autoRefTex = false; - } + SerializationHelper.deserialize(props as unknown as spec.EffectsObjectData, compositionComponent); compositionComponent.createContent(); for (const vfxItem of compositionComponent.items) { vfxItem.setInstanceId(generateGUID()); @@ -133,30 +100,20 @@ export class CompositionComponent extends Behaviour { component.setInstanceId(generateGUID()); } } - } else { - item = assetLoader.loadGUID(itemData.id); - item.composition = this.item.composition; } - item.parent = this.item; - // 相机不跟随合成移动 - item.transform.parentTransform = itemData.type === spec.ItemType.camera ? new Transform() : this.transform; - if (VFXItem.isExtraCamera(item)) { - this.item.composition.extraCamera = item; - } - items.push(item); } } } - showItems () { + override onEnable () { for (const item of this.items) { - item.setVisible(true); + item.setActive(true); } } - hideItems () { + override onDisable () { for (const item of this.items) { - item.setVisible(false); + item.setActive(false); } } @@ -186,9 +143,8 @@ export class CompositionComponent extends Behaviour { const item = this.items[i]; if ( - item.getVisible() + item.isActive && item.transform.getValid() - && !item.ended && !VFXItem.isComposition(item) && !skip(item) ) { @@ -264,23 +220,33 @@ export class CompositionComponent extends Behaviour { return regions; } - override fromData (data: unknown): void { + override fromData (data: any): void { + super.fromData(data); + + this.items = data.items; + this.startTime = data.startTime ?? 0; + this.sceneBindings = data.sceneBindings; + this.timelineAsset = data.timelineAsset; } private resolveBindings () { for (const sceneBinding of this.sceneBindings) { - sceneBinding.key.binding = sceneBinding.value; + sceneBinding.key.boundObject = sceneBinding.value; } + + // 为了通过帧对比,需要保证和原有的 update 时机一致。 + // 因此这边更新一次对象绑定,后续 timeline playable 中 sort tracks 的排序才能和原先的版本对上。 + // 如果不需要严格保证和之前的 updata 时机一致,这边的更新和 timeline asset 中的 sortTracks 都能去掉。 for (const masterTrack of this.timelineAsset.tracks) { - this.resolveTrackBindingsWithRoot(masterTrack); + this.updateTrackAnimatedObject(masterTrack); } } - private resolveTrackBindingsWithRoot (track: TrackAsset) { + private updateTrackAnimatedObject (track: TrackAsset) { for (const subTrack of track.getChildTracks()) { - subTrack.binding = subTrack.resolveBinding(track.binding); + subTrack.updateAnimatedObject(); - this.resolveTrackBindingsWithRoot(subTrack); + this.updateTrackAnimatedObject(subTrack); } } } diff --git a/packages/effects-core/src/components/base-render-component.ts b/packages/effects-core/src/components/base-render-component.ts new file mode 100644 index 000000000..caabadda0 --- /dev/null +++ b/packages/effects-core/src/components/base-render-component.ts @@ -0,0 +1,382 @@ +import * as spec from '@galacean/effects-specification'; +import { Matrix4 } from '@galacean/effects-math/es/core/matrix4'; +import { Vector3 } from '@galacean/effects-math/es/core/vector3'; +import { Vector4 } from '@galacean/effects-math/es/core/vector4'; +import { RendererComponent } from './renderer-component'; +import type { Texture } from '../texture'; +import type { GeometryDrawMode, Renderer } from '../render'; +import { Geometry } from '../render'; +import type { Engine } from '../engine'; +import { glContext } from '../gl'; +import { addItem } from '../utils'; +import type { BoundingBoxTriangle, HitTestTriangleParams } from '../plugins'; +import { HitTestType, spriteMeshShaderFromRenderInfo } from '../plugins'; +import type { MaterialProps } from '../material'; +import { getPreMultiAlpha, Material, setBlendMode, setMaskMode, setSideMode } from '../material'; +import { trianglesFromRect } from '../math'; +import type { GeometryFromShape } from '../shape'; + +/** + * 图层元素渲染属性, 经过处理后的 spec.SpriteContent.renderer + */ +export interface ItemRenderer extends Required> { + order: number, + mask: number, + texture: Texture, + shape?: GeometryFromShape, + anchor?: spec.vec2, + particleOrigin?: spec.ParticleOrigin, +} + +/** + * 图层的渲染属性,用于 Mesh 的合并判断 + */ +export interface ItemRenderInfo { + side: number, + occlusion: boolean, + blending: number, + cachePrefix: string, + mask: number, + maskMode: number, + cacheId: string, + wireframe?: boolean, +} + +/** + * @since 2.1.0 + */ +export class BaseRenderComponent extends RendererComponent { + interaction?: { behavior: spec.InteractBehavior }; + cachePrefix = '-'; + geoData: { atlasOffset: number[] | spec.TypedArray, index: number[] | spec.TypedArray }; + anchor?: spec.vec2; + renderer: ItemRenderer; + + emptyTexture: Texture; + color: spec.vec4 = [1, 1, 1, 1]; + worldMatrix: Matrix4; + geometry: Geometry; + + protected renderInfo: ItemRenderInfo; + // readonly mesh: Mesh; + protected readonly wireframe?: boolean; + protected preMultiAlpha: number; + protected visible = true; + protected isManualTimeSet = false; + protected frameAnimationTime = 0; + + /** + * + * @param engine + */ + constructor (engine: Engine) { + super(engine); + + this.renderer = { + renderMode: spec.RenderMode.BILLBOARD, + blending: spec.BlendingMode.ALPHA, + texture: this.engine.emptyTexture, + occlusion: false, + transparentOcclusion: false, + side: spec.SideMode.DOUBLE, + mask: 0, + maskMode: spec.MaskMode.NONE, + order: 0, + }; + this.emptyTexture = this.engine.emptyTexture; + this.renderInfo = getImageItemRenderInfo(this); + + const material = this.createMaterial(this.renderInfo, 2); + + this.worldMatrix = Matrix4.fromIdentity(); + this.material = material; + this.material.setVector4('_Color', new Vector4().setFromArray([1, 1, 1, 1])); + this.material.setVector4('_TexOffset', new Vector4().setFromArray([0, 0, 1, 1])); + } + + /** + * 设置当前 Mesh 的可见性。 + * @param visible - true:可见,false:不可见 + */ + setVisible (visible: boolean) { + this.visible = visible; + } + /** + * 获取当前 Mesh 的可见性。 + */ + getVisible (): boolean { + return this.visible; + } + + /** + * 设置当前图层的颜色 + * > Tips: 透明度也属于颜色的一部分,当有透明度/颜色 K 帧变化时,该 API 会失效 + * @since 2.0.0 + * @param color - 颜色值 + */ + setColor (color: spec.vec4) { + this.color = color; + this.material.setVector4('_Color', new Vector4().setFromArray(color)); + } + + /** + * 设置当前 Mesh 的纹理 + * @since 2.0.0 + * @param texture - 纹理对象 + */ + setTexture (texture: Texture) { + this.renderer.texture = texture; + this.material.setTexture('_MainTex', texture); + } + + /** + * @internal + */ + setAnimationTime (time: number) { + this.frameAnimationTime = time; + this.isManualTimeSet = true; + } + + override render (renderer: Renderer) { + if (!this.getVisible()) { + return; + } + const material = this.material; + const geo = this.geometry; + + if (renderer.renderingData.currentFrame.globalUniforms) { + renderer.setGlobalMatrix('effects_ObjectToWorld', this.transform.getWorldMatrix()); + } + this.material.setVector2('_Size', this.transform.size); + renderer.drawGeometry(geo, material); + } + + override onStart (): void { + this.item.getHitTestParams = this.getHitTestParams; + } + + override onDestroy (): void { + if (this.item && this.item.composition) { + this.item.composition.destroyTextures(this.getTextures()); + } + } + + protected getItemInitData () { + this.geoData = this.getItemGeometryData(); + + const { index, atlasOffset } = this.geoData; + const idxCount = index.length; + // @ts-expect-error + const indexData: number[] = this.wireframe ? new Uint8Array([0, 1, 1, 3, 2, 3, 2, 0]) : new index.constructor(idxCount); + + if (!this.wireframe) { + for (let i = 0; i < idxCount; i++) { + indexData[i] = 0 + index[i]; + } + } + + return { + atlasOffset, + index: indexData, + }; + } + + protected setItem () { + const textures: Texture[] = []; + let texture = this.renderer.texture; + + if (texture) { + addItem(textures, texture); + } + texture = this.renderer.texture; + const data = this.getItemInitData(); + + const renderer = this.renderer; + const texParams = this.material.getVector4('_TexParams'); + + if (texParams) { + texParams.x = renderer.occlusion ? +(renderer.transparentOcclusion) : 1; + texParams.y = +this.preMultiAlpha; + texParams.z = renderer.renderMode; + } + + const attributes = { + atlasOffset: new Float32Array(data.atlasOffset.length), + index: new Uint16Array(data.index.length), + }; + + attributes.atlasOffset.set(data.atlasOffset); + attributes.index.set(data.index); + const { material, geometry } = this; + const indexData = attributes.index; + + geometry.setIndexData(indexData); + geometry.setAttributeData('atlasOffset', attributes.atlasOffset); + geometry.setDrawCount(data.index.length); + + material.setTexture('_MainTex', texture); + } + + protected getItemGeometryData () { + const renderer = this.renderer; + + if (renderer.shape) { + const { index = [], aPoint = [] } = renderer.shape; + const point = new Float32Array(aPoint); + const position = []; + + const atlasOffset = []; + + for (let i = 0; i < point.length; i += 6) { + atlasOffset.push(aPoint[i + 2], aPoint[i + 3]); + position.push(point[i], point[i + 1], 0.0); + } + this.geometry.setAttributeData('aPos', new Float32Array(position)); + + return { + index: index as number[], + atlasOffset, + }; + } else { + this.geometry.setAttributeData('aPos', new Float32Array([-0.5, 0.5, 0, -0.5, -0.5, 0, 0.5, 0.5, 0, 0.5, -0.5, 0])); + + return { index: [0, 1, 2, 2, 1, 3], atlasOffset: [0, 1, 0, 0, 1, 1, 1, 0] }; + } + } + + protected createGeometry (mode: GeometryDrawMode) { + return Geometry.create(this.engine, { + attributes: { + aPos: { + type: glContext.FLOAT, + size: 3, + data: new Float32Array([ + -0.5, 0.5, 0, //左上 + -0.5, -0.5, 0, //左下 + 0.5, 0.5, 0, //右上 + 0.5, -0.5, 0, //右下 + ]), + }, + atlasOffset: { + size: 2, + offset: 0, + releasable: true, + type: glContext.FLOAT, + data: new Float32Array(0), + }, + }, + indices: { data: new Uint16Array(0), releasable: true }, + mode, + maxVertex: 4, + }); + } + + protected createMaterial (renderInfo: ItemRenderInfo, count: number): Material { + const { side, occlusion, blending, maskMode, mask } = renderInfo; + const materialProps: MaterialProps = { + shader: spriteMeshShaderFromRenderInfo(renderInfo, count, 1), + }; + + this.preMultiAlpha = getPreMultiAlpha(blending); + + const material = Material.create(this.engine, materialProps); + const states = { + side, + blending: true, + blendMode: blending, + mask, + maskMode, + depthTest: true, + depthMask: occlusion, + }; + + material.blending = states.blending; + material.stencilRef = states.mask !== undefined ? [states.mask, states.mask] : undefined; + material.depthTest = states.depthTest; + material.depthMask = states.depthMask; + states.blending && setBlendMode(material, states.blendMode); + setMaskMode(material, states.maskMode); + setSideMode(material, states.side); + + material.shader.shaderData.properties = '_MainTex("_MainTex",2D) = "white" {}'; + if (!material.hasUniform('_Color')) { + material.setVector4('_Color', new Vector4(0, 0, 0, 1)); + } + if (!material.hasUniform('_TexOffset')) { + material.setVector4('_TexOffset', new Vector4()); + } + if (!material.hasUniform('_TexParams')) { + material.setVector4('_TexParams', new Vector4()); + } + + return material; + } + + getTextures (): Texture[] { + const ret = []; + const tex = this.renderer.texture; + + if (tex) { + ret.push(tex); + } + + return ret; + } + + /** + * 获取图层包围盒的类型和世界坐标 + * @returns + */ + getBoundingBox (): BoundingBoxTriangle | void { + if (!this.item) { + return; + } + const worldMatrix = this.transform.getWorldMatrix(); + const triangles = trianglesFromRect(Vector3.ZERO, 0.5 * this.transform.size.x, 0.5 * this.transform.size.y); + + triangles.forEach(triangle => { + worldMatrix.transformPoint(triangle.p0 as Vector3); + worldMatrix.transformPoint(triangle.p1 as Vector3); + worldMatrix.transformPoint(triangle.p2 as Vector3); + }); + + return { + type: HitTestType.triangle, + area: triangles, + }; + } + + getHitTestParams = (force?: boolean): HitTestTriangleParams | undefined => { + const ui = this.interaction; + + if ((force || ui)) { + const area = this.getBoundingBox(); + + if (area) { + return { + behavior: this.interaction?.behavior || 0, + type: area.type, + triangles: area.area, + backfaceCulling: this.renderer.side === spec.SideMode.FRONT, + }; + } + } + }; +} + +export function getImageItemRenderInfo (item: BaseRenderComponent): ItemRenderInfo { + const { renderer } = item; + const { blending, side, occlusion, mask, maskMode, order } = renderer; + const blendingCache = +blending; + const cachePrefix = item.cachePrefix || '-'; + + return { + side, + occlusion, + blending, + mask, + maskMode, + cachePrefix, + cacheId: `${cachePrefix}.${+side}+${+occlusion}+${blendingCache}+${order}+${maskMode}.${mask}`, + }; +} diff --git a/packages/effects-core/src/components/component.ts b/packages/effects-core/src/components/component.ts index e4222a6eb..8b50b64d8 100644 --- a/packages/effects-core/src/components/component.ts +++ b/packages/effects-core/src/components/component.ts @@ -12,6 +12,13 @@ export abstract class Component extends EffectsObject { * 附加到的 VFXItem 对象 */ item: VFXItem; + isAwakeCalled = false; + isStartCalled = false; + isEnableCalled = false; + + @serialize() + private _enabled = true; + /** * 附加到的 VFXItem 对象 Transform 组件 */ @@ -19,53 +26,32 @@ export abstract class Component extends EffectsObject { return this.item.transform; } - onAttached () { } - onDestroy () { } - - override fromData (data: any): void { - super.fromData(data); - if (data.item) { - this.item = data.item; - } - } - - override dispose (): void { - this.onDestroy(); - if (this.item) { - removeItem(this.item.components, this); - } - } -} - -/** - * @since 2.0.0 - */ -export abstract class Behaviour extends Component { - isAwakeCalled = false; - isStartCalled = false; - - @serialize() - private _enabled = true; - /** * 组件是否可以更新,true 更新,false 不更新 */ get isActiveAndEnabled () { - return this.item.getVisible() && this.enabled; + return this.item.isActive && this.enabled; } get enabled () { return this._enabled; } + set enabled (value: boolean) { - this._enabled = value; - if (value) { - if (this.isActiveAndEnabled) { - this.onEnable(); - } - if (!this.isStartCalled) { - this.start(); - this.isStartCalled = true; + if (this.enabled !== value) { + this._enabled = value; + if (value) { + if (this.isActiveAndEnabled) { + this.enable(); + if (!this.isStartCalled) { + this.onStart(); + this.isStartCalled = true; + } + } + } else { + if (this.isEnableCalled) { + this.disable(); + } } } } @@ -73,47 +59,127 @@ export abstract class Behaviour extends Component { /** * 生命周期函数,初始化后调用,生命周期内只调用一次 */ - awake () { + onAwake () { // OVERRIDE } /** - * 在每次设置 enabled 为 true 时触发 + * 在 enabled 变为 true 时触发 */ onEnable () { // OVERRIDE } + + /** + * 在 enabled 变为 false 时触发 + */ + onDisable () { + // OVERRIDE + } + /** * 生命周期函数,在第一次 update 前调用,生命周期内只调用一次 */ - start () { + onStart () { // OVERRIDE } + /** * 生命周期函数,每帧调用一次 */ - update (dt: number) { + onUpdate (dt: number) { // OVERRIDE } + /** * 生命周期函数,每帧调用一次,在 update 之后调用 */ - lateUpdate (dt: number) { + onLateUpdate (dt: number) { // OVERRIDE } - override onAttached (): void { - this.item.itemBehaviours.push(this); - if (!this.isAwakeCalled) { - this.awake(); - this.isAwakeCalled = true; + /** + * 生命周期函数,在组件销毁时调用 + */ + onDestroy () { + // OVERRIDE + } + + /** + * @internal + */ + enable () { + if (this.item.composition) { + this.item.composition.sceneTicking.addComponent(this); + this.isEnableCalled = true; + } + this.onEnable(); + } + + /** + * @internal + */ + disable () { + this.onDisable(); + if (this.item.composition) { + this.isEnableCalled = false; + this.item.composition.sceneTicking.removeComponent(this); + } + } + + setVFXItem (item: VFXItem) { + this.item = item; + if (item.isDuringPlay) { + if (!this.isAwakeCalled) { + this.onAwake(); + this.isAwakeCalled = true; + } + if (item.isActive && this.enabled) { + this.start(); + this.enable(); + } + } + } + + override fromData (data: any): void { + super.fromData(data); + if (data.item) { + this.item = data.item; } } override dispose (): void { + if (this.isEnableCalled) { + this.disable(); + } + if (this.isAwakeCalled) { + this.isAwakeCalled = false; + this.onDestroy(); + } if (this.item) { - removeItem(this.item.itemBehaviours, this); + removeItem(this.item.components, this); } + } + + private start () { + if (this.isStartCalled) { + return; + } + this.isStartCalled = true; + this.onStart(); + } +} + +/** + * @since 2.0.0 + */ +export abstract class Behaviour extends Component { + + override setVFXItem (item: VFXItem): void { + super.setVFXItem(item); + } + + override dispose (): void { super.dispose(); } } diff --git a/packages/effects-core/src/components/effect-component.ts b/packages/effects-core/src/components/effect-component.ts index 9bbffead6..692af9387 100644 --- a/packages/effects-core/src/components/effect-component.ts +++ b/packages/effects-core/src/components/effect-component.ts @@ -1,141 +1,33 @@ +import { Vector4 } from '@galacean/effects-math/es/core/vector4'; import * as spec from '@galacean/effects-specification'; -import { Matrix4 } from '@galacean/effects-math/es/core/matrix4'; -import type { TriangleLike } from '@galacean/effects-math/es/core/type'; -import { Vector3 } from '@galacean/effects-math/es/core/vector3'; -import { effectsClass, serialize } from '../decorators'; +import { effectsClass } from '../decorators'; import type { Engine } from '../engine'; -import type { Material, MaterialDestroyOptions } from '../material'; -import type { BoundingBoxTriangle, HitTestTriangleParams } from '../plugins'; -import { HitTestType } from '../plugins'; -import type { MeshDestroyOptions, Renderer } from '../render'; -import type { Geometry } from '../render'; -import { DestroyOptions } from '../utils'; -import { RendererComponent } from './renderer-component'; +import { MeshComponent } from './mesh-component'; /** * @since 2.0.0 */ @effectsClass(spec.DataType.EffectComponent) -export class EffectComponent extends RendererComponent { - /** - * Mesh 的世界矩阵 - */ - worldMatrix = Matrix4.fromIdentity(); - /** - * Mesh 的 Geometry - */ - @serialize() - geometry: Geometry; - - private triangles: TriangleLike[] = []; - private destroyed = false; - // TODO: 抽象到射线碰撞检测组件 - private hitTestGeometry: Geometry; +export class EffectComponent extends MeshComponent { constructor (engine: Engine) { super(engine); this.name = 'EffectComponent'; - this._priority = 0; } - override start (): void { + override onStart (): void { this.item.getHitTestParams = this.getHitTestParams; } - override render (renderer: Renderer) { - if (renderer.renderingData.currentFrame.globalUniforms) { - renderer.setGlobalMatrix('effects_ObjectToWorld', this.transform.getWorldMatrix()); - } - renderer.drawGeometry(this.geometry, this.material); - } - - /** - * 设置当前 Mesh 的材质 - * @param material - 要设置的材质 - * @param destroy - 可选的材质销毁选项 - */ - setMaterial (material: Material, destroy?: MaterialDestroyOptions | DestroyOptions.keep) { - if (destroy !== DestroyOptions.keep) { - this.material.dispose(destroy); - } - this.material = material; - } - - // TODO 点击测试后续抽象一个 Collider 组件 - getHitTestParams = (force?: boolean): HitTestTriangleParams | void => { - const area = this.getBoundingBox(); - - if (area) { - return { - type: area.type, - triangles: area.area, - }; - } - }; - - getBoundingBox (): BoundingBoxTriangle | void { - const worldMatrix = this.transform.getWorldMatrix(); - - if (this.hitTestGeometry !== this.geometry) { - this.triangles = geometryToTriangles(this.geometry); - this.hitTestGeometry = this.geometry; - } - const area = []; - - for (const triangle of this.triangles) { - area.push({ p0: triangle.p0, p1: triangle.p1, p2: triangle.p2 }); - } - - area.forEach(triangle => { - triangle.p0 = worldMatrix.transformPoint(triangle.p0 as Vector3, new Vector3()); - triangle.p1 = worldMatrix.transformPoint(triangle.p1 as Vector3, new Vector3()); - triangle.p2 = worldMatrix.transformPoint(triangle.p2 as Vector3, new Vector3()); - }); + override onUpdate (dt: number): void { + const time = this.item.time; + const _Time = this.material.getVector4('_Time') ?? new Vector4(); - return { - type: HitTestType.triangle, - area, - }; + this.material.setVector4('_Time', _Time.set(time / 20, time, time * 2, time * 3)); } override fromData (data: unknown): void { super.fromData(data); this.material = this.materials[0]; } - - override toData (): void { - this.taggedProperties.id = this.guid; - } - - /** - * 销毁当前资源 - * @param options - 可选的销毁选项 - */ - override dispose (options?: MeshDestroyOptions) { - if (this.destroyed) { - return; - } - this.destroyed = true; - - super.dispose(); - } -} - -function geometryToTriangles (geometry: Geometry) { - const indices = geometry.getIndexData() ?? []; - const vertices = geometry.getAttributeData('aPos') ?? []; - const res: TriangleLike[] = []; - - for (let i = 0; i < indices.length; i += 3) { - const index0 = indices[i] * 3; - const index1 = indices[i + 1] * 3; - const index2 = indices[i + 2] * 3; - const p0 = { x: vertices[index0], y: vertices[index0 + 1], z: vertices[index0 + 2] }; - const p1 = { x: vertices[index1], y: vertices[index1 + 1], z: vertices[index1 + 2] }; - const p2 = { x: vertices[index2], y: vertices[index2 + 1], z: vertices[index2 + 2] }; - - res.push({ p0, p1, p2 }); - } - - return res; -} +} \ No newline at end of file diff --git a/packages/effects-core/src/components/fake-3d-component.ts b/packages/effects-core/src/components/fake-3d-component.ts new file mode 100644 index 000000000..3b913ce3e --- /dev/null +++ b/packages/effects-core/src/components/fake-3d-component.ts @@ -0,0 +1,108 @@ +import { effectsClass, serialize } from '../decorators'; +import { Component } from './component'; +import { EffectComponent } from './effect-component'; + +@effectsClass('Fake3DComponent') +export class Fake3DComponent extends Component { + @serialize() + loop = false; + + @serialize() + amountOfMotion = 1.0; + + @serialize() + animationLength = 2.0; + + @serialize() + mode = Fake3DAnimationMode.Linear; + + @serialize() + startPositionX = 0; + @serialize() + startPositionY = 0; + @serialize() + startPositionZ = 0; + + @serialize() + endPositionX = 0; + @serialize() + endPositionY = 0; + @serialize() + endPositionZ = 0; + + @serialize() + amplitudeX = 0; + @serialize() + amplitudeY = 0; + @serialize() + amplitudeZ = 0; + + @serialize() + phaseX = 0; + @serialize() + phaseY = 0; + @serialize() + phaseZ = 0; + + effectComponent: EffectComponent; + + override onStart (): void { + this.effectComponent = this.item.getComponent(EffectComponent); + } + + override onUpdate (dt: number): void { + this.updateFake3D(); + } + + updateFake3D () { + if (!this.effectComponent) { + return; + } + + const time = this.item.time % this.animationLength / this.animationLength; + + let _PosX = 0; + let _PosY = 0; + let _PosZ = 0; + + switch (this.mode) { + case Fake3DAnimationMode.Circular:{ + const PI = Math.PI; + + _PosX = Math.sin(2.0 * PI * (time + this.phaseX)) * this.amplitudeX; + _PosY = Math.sin(2.0 * PI * (time + this.phaseY)) * this.amplitudeY; + _PosZ = Math.sin(2.0 * PI * (time + this.phaseZ)) * this.amplitudeZ; + + break; + } + case Fake3DAnimationMode.Linear:{ + let localTime = time; + + if (this.loop) { + if (localTime > 0.5) { + localTime = 1 - localTime; + } + + localTime *= 2; + } + + _PosX = this.startPositionX * (1 - localTime) + localTime * this.endPositionX; + _PosY = this.startPositionY * (1 - localTime) + localTime * this.endPositionY; + _PosZ = this.startPositionZ * (1 - localTime) + localTime * this.endPositionZ; + + break; + } + } + + const material = this.effectComponent.material; + + material.setFloat('_PosX', _PosX * this.amountOfMotion); + material.setFloat('_PosY', _PosY * this.amountOfMotion); + material.setFloat('_PosZ', _PosZ * this.amountOfMotion); + } +} + +export enum Fake3DAnimationMode { + Circular, + Linear +} \ No newline at end of file diff --git a/packages/effects-core/src/components/index.ts b/packages/effects-core/src/components/index.ts index eed835cb6..bacc83b6c 100644 --- a/packages/effects-core/src/components/index.ts +++ b/packages/effects-core/src/components/index.ts @@ -1,4 +1,7 @@ export * from './renderer-component'; export * from './component'; export * from './effect-component'; -export * from './post-process-volume'; \ No newline at end of file +export * from './post-process-volume'; +export * from './base-render-component'; +export * from './shape-component'; +export * from './fake-3d-component'; diff --git a/packages/effects-core/src/components/mesh-component.ts b/packages/effects-core/src/components/mesh-component.ts new file mode 100644 index 000000000..864b17bf5 --- /dev/null +++ b/packages/effects-core/src/components/mesh-component.ts @@ -0,0 +1,52 @@ +import { serialize } from '../decorators'; +import type { BoundingBoxTriangle, HitTestTriangleParams } from '../plugins'; +import { MeshCollider } from '../plugins'; +import type { Geometry } from '../render/geometry'; +import type { Renderer } from '../render/renderer'; +import { RendererComponent } from './renderer-component'; + +/** + * Mesh 组件 + */ +export class MeshComponent extends RendererComponent { + /** + * 渲染的 Geometry + */ + @serialize() + protected geometry: Geometry; + /** + * 用于点击测试的碰撞器 + */ + protected meshCollider = new MeshCollider(); + + override render (renderer: Renderer) { + if (renderer.renderingData.currentFrame.globalUniforms) { + renderer.setGlobalMatrix('effects_ObjectToWorld', this.transform.getWorldMatrix()); + } + renderer.drawGeometry(this.geometry, this.material); + } + + // TODO 点击测试后续抽象一个 Collider 组件 + getHitTestParams = (force?: boolean): HitTestTriangleParams | void => { + const worldMatrix = this.transform.getWorldMatrix(); + + this.meshCollider.setGeometry(this.geometry, worldMatrix); + const area = this.meshCollider.getBoundingBoxData(); + + if (area) { + return { + type: area.type, + triangles: area.area, + }; + } + }; + + getBoundingBox (): BoundingBoxTriangle | void { + const worldMatrix = this.transform.getWorldMatrix(); + + this.meshCollider.setGeometry(this.geometry, worldMatrix); + const boundingBox = this.meshCollider.getBoundingBox(); + + return boundingBox; + } +} diff --git a/packages/effects-core/src/components/post-process-volume.ts b/packages/effects-core/src/components/post-process-volume.ts index a6f06c8b3..2d14a4e8b 100644 --- a/packages/effects-core/src/components/post-process-volume.ts +++ b/packages/effects-core/src/components/post-process-volume.ts @@ -1,53 +1,59 @@ +import * as spec from '@galacean/effects-specification'; import { effectsClass, serialize } from '../decorators'; import { Behaviour } from './component'; +import type { Engine } from '../engine'; -// TODO spec 增加 DataType -@effectsClass('PostProcessVolume') +/** + * @since 2.1.0 + */ +@effectsClass(spec.DataType.PostProcessVolume) export class PostProcessVolume extends Behaviour { - @serialize() - useHDR = true; - // Bloom @serialize() - useBloom = true; + bloom: spec.Bloom; @serialize() - threshold = 1.0; + vignette: spec.Vignette; @serialize() - bloomIntensity = 1.0; + tonemapping: spec.Tonemapping; - // ColorAdjustments @serialize() - brightness = 1.0; + colorAdjustments: spec.ColorAdjustments; - @serialize() - saturation = 1.0; + constructor (engine: Engine) { + super(engine); - @serialize() - contrast = 1.0; - - // Vignette - @serialize() - vignetteIntensity = 0.2; + this.bloom = { + threshold: 0, + intensity: 0, + active: false, + }; - @serialize() - vignetteSmoothness = 0.4; + this.vignette = { + intensity: 0, + smoothness: 0, + roundness: 0, + active: false, + }; - @serialize() - vignetteRoundness = 1.0; + this.tonemapping = { + active: false, + }; - // ToneMapping - @serialize() - useToneMapping: boolean = true; // 1: true, 0: false + this.colorAdjustments = { + brightness: 0, + saturation: 0, + contrast: 0, + active: false, + }; + } - override start (): void { + override onStart (): void { const composition = this.item.composition; if (composition) { - composition.globalVolume = this; - - composition.createRenderFrame(); + composition.renderFrame.globalVolume = this; } } } diff --git a/packages/effects-core/src/components/renderer-component.ts b/packages/effects-core/src/components/renderer-component.ts index 9b996b2ff..9b635dc17 100644 --- a/packages/effects-core/src/components/renderer-component.ts +++ b/packages/effects-core/src/components/renderer-component.ts @@ -2,6 +2,7 @@ import { serialize } from '../decorators'; import type { Material } from '../material'; import type { Renderer } from '../render'; import { removeItem } from '../utils'; +import type { VFXItem } from '../vfx-item'; import { Component } from './component'; /** @@ -9,7 +10,6 @@ import { Component } from './component'; * @since 2.0.0 */ export class RendererComponent extends Component { - isStartCalled = false; @serialize() materials: Material[] = []; @@ -17,9 +17,6 @@ export class RendererComponent extends Component { @serialize() protected _priority = 0; - @serialize() - protected _enabled = true; - get priority (): number { return this._priority; } @@ -27,23 +24,6 @@ export class RendererComponent extends Component { this._priority = value; } - get enabled () { - return this._enabled; - } - set enabled (value: boolean) { - this._enabled = value; - if (value) { - this.onEnable(); - } - } - - /** - * 组件是否可以更新,true 更新,false 不更新 - */ - get isActiveAndEnabled () { - return this.item.getVisible() && this.enabled; - } - get material (): Material { return this.materials[0]; } @@ -55,20 +35,21 @@ export class RendererComponent extends Component { } } - onEnable () { } - - start () { } - - update (dt: number) { } - - lateUpdate (dt: number) { } - render (renderer: Renderer): void { } - override onAttached (): void { + override setVFXItem (item: VFXItem): void { + super.setVFXItem(item); this.item.rendererComponents.push(this); } + override onEnable (): void { + this.item.composition?.renderFrame.addMeshToDefaultRenderPass(this); + } + + override onDisable (): void { + this.item.composition?.renderFrame.removeMeshFromDefaultRenderPass(this); + } + override fromData (data: unknown): void { super.fromData(data); } diff --git a/packages/effects-core/src/components/shape-component.ts b/packages/effects-core/src/components/shape-component.ts new file mode 100644 index 000000000..d07e7af2d --- /dev/null +++ b/packages/effects-core/src/components/shape-component.ts @@ -0,0 +1,284 @@ +import { Color } from '@galacean/effects-math/es/core/color'; +import * as spec from '@galacean/effects-specification'; +import { effectsClass } from '../decorators'; +import type { Engine } from '../engine'; +import { glContext } from '../gl'; +import type { MaterialProps } from '../material'; +import { Material, setMaskMode } from '../material'; +import { GraphicsPath } from '../plugins/shape/graphics-path'; +import type { ShapePath } from '../plugins/shape/shape-path'; +import { Geometry, GLSLVersion } from '../render'; +import { MeshComponent } from './mesh-component'; +import { StarType } from '../plugins/shape/poly-star'; + +interface CurveData { + point: spec.Vector2Data, + controlPoint1: spec.Vector2Data, + controlPoint2: spec.Vector2Data, +} + +/** + * 图形组件 + * @since 2.1.0 + */ +@effectsClass('ShapeComponent') +export class ShapeComponent extends MeshComponent { + + private path = new GraphicsPath(); + private curveValues: CurveData[] = []; + private data: spec.ShapeComponentData; + private animated = true; + + private vert = ` +precision highp float; + +attribute vec3 aPos;//x y + +uniform mat4 effects_MatrixVP; +uniform mat4 effects_MatrixInvV; +uniform mat4 effects_ObjectToWorld; + +void main() { + vec4 pos = vec4(aPos.xyz, 1.0); + gl_Position = effects_MatrixVP * effects_ObjectToWorld * pos; +} +`; + + private frag = ` +precision highp float; + +uniform vec4 _Color; + +void main() { + vec4 color = _Color; + color.rgb *= color.a; + gl_FragColor = color; +} +`; + + /** + * + * @param engine + */ + constructor (engine: Engine) { + super(engine); + + if (!this.geometry) { + this.geometry = Geometry.create(engine, { + attributes: { + aPos: { + type: glContext.FLOAT, + size: 3, + data: new Float32Array([ + -0.5, 0.5, 0, //左上 + -0.5, -0.5, 0, //左下 + 0.5, 0.5, 0, //右上 + 0.5, -0.5, 0, //右下 + ]), + }, + aUV: { + type: glContext.FLOAT, + size: 2, + data: new Float32Array(), + }, + }, + mode: glContext.TRIANGLES, + drawCount: 4, + }); + } + + if (!this.material) { + const materialProps: MaterialProps = { + shader: { + vertex: this.vert, + fragment: this.frag, + glslVersion: GLSLVersion.GLSL1, + }, + }; + + this.material = Material.create(engine, materialProps); + this.material.setColor('_Color', new Color(1, 1, 1, 1)); + this.material.depthMask = false; + this.material.depthTest = true; + this.material.blending = true; + } + } + + override onStart (): void { + this.item.getHitTestParams = this.getHitTestParams; + } + + override onUpdate (dt: number): void { + if (this.animated) { + this.buildPath(this.data); + this.buildGeometryFromPath(this.path.shapePath); + } + } + + private buildGeometryFromPath (shapePath: ShapePath) { + const shapePrimitives = shapePath.shapePrimitives; + const vertices: number[] = []; + const indices: number[] = []; + + // triangulate shapePrimitive + for (const shapePrimitive of shapePrimitives) { + const shape = shapePrimitive.shape; + const points: number[] = []; + const indexOffset = indices.length; + const vertOffset = vertices.length / 2; + + shape.build(points); + + shape.triangulate(points, vertices, vertOffset, indices, indexOffset); + } + + const vertexCount = vertices.length / 2; + + // get the current attribute and index arrays from the geometry, avoiding re-creation + let positionArray = this.geometry.getAttributeData('aPos'); + let uvArray = this.geometry.getAttributeData('aUV'); + let indexArray = this.geometry.getIndexData(); + + if (!positionArray || positionArray.length < vertexCount * 3) { + positionArray = new Float32Array(vertexCount * 3); + } + if (!uvArray || uvArray.length < vertexCount * 2) { + uvArray = new Float32Array(vertexCount * 2); + } + if (!indexArray) { + indexArray = new Uint16Array(indices.length); + } + + // set position and uv attribute array + for (let i = 0; i < vertexCount; i++) { + const pointsOffset = i * 3; + const positionArrayOffset = i * 2; + const uvOffset = i * 2; + + positionArray[pointsOffset] = vertices[positionArrayOffset]; + positionArray[pointsOffset + 1] = vertices[positionArrayOffset + 1]; + positionArray[pointsOffset + 2] = 0; + + uvArray[uvOffset] = positionArray[pointsOffset]; + uvArray[uvOffset + 1] = positionArray[pointsOffset + 1]; + } + + // set index array + indexArray.set(indices); + + // rewrite to geometry + this.geometry.setAttributeData('aPos', positionArray); + this.geometry.setAttributeData('aUV', uvArray); + this.geometry.setIndexData(indexArray); + this.geometry.setDrawCount(indices.length); + } + + private buildPath (data: spec.ShapeComponentData) { + this.path.clear(); + + const shapeData = data; + + switch (shapeData.type) { + case spec.ShapePrimitiveType.Custom: { + const customData = shapeData as spec.CustomShapeData; + const points = customData.points; + const easingIns = customData.easingIns; + const easingOuts = customData.easingOuts; + + this.curveValues = []; + + for (const shape of customData.shapes) { + this.setFillColor(shape.fill); + + const indices = shape.indexes; + + for (let i = 1; i < indices.length; i++) { + const pointIndex = indices[i]; + const lastPointIndex = indices[i - 1]; + + this.curveValues.push({ + point: points[pointIndex.point], + controlPoint1: easingOuts[lastPointIndex.easingOut], + controlPoint2: easingIns[pointIndex.easingIn], + }); + } + + // Push the last curve + this.curveValues.push({ + point: points[indices[0].point], + controlPoint1: easingOuts[indices[indices.length - 1].easingOut], + controlPoint2: easingIns[indices[0].easingIn], + }); + } + + this.path.moveTo(this.curveValues[this.curveValues.length - 1].point.x, this.curveValues[this.curveValues.length - 1].point.y); + + for (const curveValue of this.curveValues) { + const point = curveValue.point; + const control1 = curveValue.controlPoint1; + const control2 = curveValue.controlPoint2; + + this.path.bezierCurveTo(control1.x, control1.y, control2.x, control2.y, point.x, point.y, 1); + } + + break; + } + case spec.ShapePrimitiveType.Ellipse: { + const ellipseData = shapeData as spec.EllipseData; + + this.path.ellipse(0, 0, ellipseData.xRadius, ellipseData.yRadius); + + this.setFillColor(ellipseData.fill); + + break; + } + case spec.ShapePrimitiveType.Rectangle: { + const rectangleData = shapeData as spec.RectangleData; + + this.path.rect(-rectangleData.width / 2, -rectangleData.height / 2, rectangleData.width, rectangleData.height); + + this.setFillColor(rectangleData.fill); + + break; + } + case spec.ShapePrimitiveType.Star: { + const starData = shapeData as spec.StarData; + + this.path.polyStar(starData.pointCount, starData.outerRadius, starData.innerRadius, starData.outerRoundness, starData.innerRoundness, StarType.Star); + + this.setFillColor(starData.fill); + + break; + } + case spec.ShapePrimitiveType.Polygon: { + const polygonData = shapeData as spec.PolygonData; + + this.path.polyStar(polygonData.pointCount, polygonData.radius, polygonData.radius, polygonData.roundness, polygonData.roundness, StarType.Polygon); + + this.setFillColor(polygonData.fill); + + break; + } + } + } + + private setFillColor (fill?: spec.ShapeFillParam) { + if (fill) { + const color = fill.color; + + this.material.setColor('_Color', new Color(color.r, color.g, color.b, color.a)); + } + } + + override fromData (data: spec.ShapeComponentData): void { + super.fromData(data); + this.data = data; + + const material = this.material; + + //@ts-expect-error // TODO 新版蒙版上线后重构 + material.stencilRef = data.renderer.mask !== undefined ? [data.renderer.mask, data.renderer.mask] : undefined; + //@ts-expect-error // TODO 新版蒙版上线后重构 + setMaskMode(material, data.renderer.maskMode); + } +} \ No newline at end of file diff --git a/packages/effects-core/src/composition-source-manager.ts b/packages/effects-core/src/composition-source-manager.ts index eaddcd57b..648643a27 100644 --- a/packages/effects-core/src/composition-source-manager.ts +++ b/packages/effects-core/src/composition-source-manager.ts @@ -4,21 +4,23 @@ import type { Engine } from './engine'; import { passRenderLevel } from './pass-render-level'; import type { PluginSystem } from './plugin-system'; import type { Scene, SceneRenderLevel } from './scene'; -import type { ShapeData } from './shape'; import { getGeometryByShape } from './shape'; import type { Texture } from './texture'; import type { Disposable } from './utils'; -import { isObject } from './utils'; -import type { VFXItemProps } from './vfx-item'; +import type { VFXItemData } from './asset-loader'; let listOrder = 0; +interface RendererOptionsWithMask extends spec.RendererOptions { + mask?: number, +} + export interface ContentOptions { id: string, duration: number, name: string, endBehavior: spec.EndBehavior, - items: VFXItemProps[], + items: spec.DataPath[], camera: spec.CameraOptions, startTime: number, timelineAsset: spec.DataPath, @@ -30,18 +32,19 @@ export interface ContentOptions { */ export class CompositionSourceManager implements Disposable { composition?: spec.CompositionData; - refCompositions: Map = new Map(); - sourceContent?: ContentOptions; - refCompositionProps: Map = new Map(); + sourceContent?: spec.CompositionData; + refCompositionProps: Map = new Map(); renderLevel?: SceneRenderLevel; pluginSystem?: PluginSystem; totalTime: number; - imgUsage: Record; + imgUsage: Record = {}; textures: Texture[]; jsonScene?: spec.JSONScene; mask = 0; engine: Engine; + private refCompositions: Map = new Map(); + constructor ( scene: Scene, engine: Engine, @@ -49,7 +52,7 @@ export class CompositionSourceManager implements Disposable { this.engine = engine; // 资源 const { jsonScene, renderLevel, textureOptions, pluginSystem, totalTime } = scene; - const { compositions, imgUsage, compositionId } = jsonScene; + const { compositions, compositionId } = jsonScene; if (!textureOptions) { throw new Error('scene.textures expected.'); @@ -71,44 +74,45 @@ export class CompositionSourceManager implements Disposable { this.renderLevel = renderLevel; this.pluginSystem = pluginSystem; this.totalTime = totalTime ?? 0; - this.imgUsage = imgUsage ?? {}; this.textures = cachedTextures; listOrder = 0; this.sourceContent = this.getContent(this.composition); } - private getContent (composition: spec.CompositionData): ContentOptions { - const { id, duration, name, endBehavior, camera, startTime = 0 } = composition; - const items = this.assembleItems(composition); - - return { + private getContent (composition: spec.CompositionData): spec.CompositionData { + const compositionData: spec.CompositionData = { ...composition, - id, - duration, - name, - endBehavior: isNaN(endBehavior) ? spec.EndBehavior.freeze : endBehavior, - // looping, - items, - camera, - startTime, }; + + this.assembleItems(compositionData); + + if (isNaN(compositionData.endBehavior)) { + compositionData.endBehavior = spec.EndBehavior.freeze; + } + + if (!compositionData.startTime) { + compositionData.startTime = 0; + } + + return compositionData; } private assembleItems (composition: spec.CompositionData) { - const items: any[] = []; - this.mask++; - const componentMap: Record = {}; + const componentMap: Record = {}; + const items: spec.DataPath[] = []; + + if (!this.jsonScene) { + return; + } - //@ts-expect-error for (const component of this.jsonScene.components) { componentMap[component.id] = component; } for (const itemDataPath of composition.items) { - //@ts-expect-error - const sourceItemData: VFXItemProps = this.engine.jsonSceneData[itemDataPath.id]; - const itemProps: Record = sourceItemData; + const sourceItemData = this.engine.jsonSceneData[itemDataPath.id] as VFXItemData; + const itemProps = sourceItemData; if (passRenderLevel(sourceItemData.renderLevel, this.renderLevel)) { itemProps.listIndex = listOrder++; @@ -116,10 +120,12 @@ export class CompositionSourceManager implements Disposable { if ( itemProps.type === spec.ItemType.sprite || itemProps.type === spec.ItemType.particle || - itemProps.type === spec.ItemType.spine + itemProps.type === spec.ItemType.spine || + //@ts-expect-error + itemProps.type === spec.ItemType.shape ) { for (const componentPath of itemProps.components) { - const componentData = componentMap[componentPath.id]; + const componentData = componentMap[componentPath.id] as spec.SpriteComponentData | spec.ParticleSystemData; this.preProcessItemContent(componentData); } @@ -128,70 +134,74 @@ export class CompositionSourceManager implements Disposable { // 处理预合成的渲染顺序 if (itemProps.type === spec.ItemType.composition) { const refId = (sourceItemData.content as spec.CompositionContent).options.refId; + const composition = this.refCompositions.get(refId); - if (!this.refCompositions.get(refId)) { + if (!composition) { throw new Error(`Invalid ref composition id: ${refId}.`); } - const ref = this.getContent(this.refCompositions.get(refId)!); + const ref = this.getContent(composition); if (!this.refCompositionProps.has(refId)) { - this.refCompositionProps.set(refId, ref as unknown as VFXItemProps); + this.refCompositionProps.set(refId, ref); } } - - items.push(itemProps as VFXItemProps); + items.push(itemDataPath); } } - - return items; + composition.items = items; } - private preProcessItemContent (renderContent: any) { + private preProcessItemContent ( + renderContent: spec.SpriteComponentData | spec.ParticleSystemData | spec.ParticleContent, + ) { if (renderContent.renderer) { renderContent.renderer = this.changeTex(renderContent.renderer); - if (!renderContent.renderer.mask) { + if (!('mask' in renderContent.renderer)) { this.processMask(renderContent.renderer); } - const split = renderContent.splits && !renderContent.textureSheetAnimation && renderContent.splits[0]; + const split = renderContent.splits && !renderContent.textureSheetAnimation ? renderContent.splits[0] : undefined; + const shape = renderContent.renderer.shape; + let shapeData; - if (Number.isInteger(renderContent.renderer.shape)) { - // TODO: scene.shapes 类型问题? - renderContent.renderer.shape = getGeometryByShape(this.jsonScene?.shapes[renderContent.renderer.shape] as unknown as ShapeData, split); - } else if (renderContent.renderer.shape && isObject(renderContent.renderer.shape)) { - renderContent.renderer.shape = getGeometryByShape(renderContent.renderer.shape, split); + if (Number.isInteger(shape)) { + shapeData = this.jsonScene?.shapes[shape as number]; + } else { + shapeData = shape as spec.ShapeGeometry; + } + + if (shapeData !== undefined) { + if (!('aPoint' in shapeData && 'index' in shapeData)) { + // @ts-expect-error 类型转换问题 + renderContent.renderer.shape = getGeometryByShape(shapeData, split); + } } } - if (renderContent.trails) { + if ('trails' in renderContent && renderContent.trails !== undefined) { renderContent.trails = this.changeTex(renderContent.trails); } } - private changeTex (renderer: Record) { + private changeTex (renderer: T) { if (!renderer.texture) { return renderer; } - //@ts-expect-error const texIdx = renderer.texture.id; if (texIdx !== undefined) { - //@ts-expect-error - this.addTextureUsage(texIdx) || texIdx; + this.addTextureUsage(texIdx); } return renderer; } - private addTextureUsage (texIdx: number) { - const texId = texIdx; - // FIXME: imageUsage 取自 scene.imgUsage,类型为 Record,这里给的 number,类型对不上 - const imageUsage = this.imgUsage as unknown as Record ?? {}; + private addTextureUsage (texId: string) { + const imageUsage = this.imgUsage ?? {}; if (texId && imageUsage) { - // eslint-disable-next-line no-prototype-builtins - if (!imageUsage.hasOwnProperty(texId)) { + if (!Object.prototype.hasOwnProperty.call(imageUsage, texId)) { imageUsage[texId] = 0; } imageUsage[texId]++; @@ -201,8 +211,8 @@ export class CompositionSourceManager implements Disposable { /** * 处理蒙版和遮挡关系写入 stencil 的 ref 值 */ - private processMask (renderer: Record) { - const maskMode: spec.MaskMode = renderer.maskMode; + private processMask (renderer: RendererOptionsWithMask) { + const maskMode = renderer.maskMode; if (maskMode === spec.MaskMode.NONE) { return; diff --git a/packages/effects-core/src/composition.ts b/packages/effects-core/src/composition.ts index 28bd537d1..abbe9241e 100644 --- a/packages/effects-core/src/composition.ts +++ b/packages/effects-core/src/composition.ts @@ -1,6 +1,5 @@ import * as spec from '@galacean/effects-specification'; import type { Ray } from '@galacean/effects-math/es/core/ray'; -import type { Vector3 } from '@galacean/effects-math/es/core/vector3'; import type { Matrix4 } from '@galacean/effects-math/es/core/matrix4'; import { Camera } from './camera'; import { CompositionComponent } from './comp-vfx-item'; @@ -11,7 +10,7 @@ import type { PluginSystem } from './plugin-system'; import type { EventSystem, Plugin, Region } from './plugins'; import type { MeshRendererOptions, Renderer } from './render'; import { RenderFrame } from './render'; -import type { Scene, SceneType } from './scene'; +import type { Scene } from './scene'; import type { Texture } from './texture'; import { TextureLoadAction, TextureSourceType } from './texture'; import type { Disposable, LostHandler } from './utils'; @@ -20,15 +19,29 @@ import type { VFXItemProps } from './vfx-item'; import { VFXItem } from './vfx-item'; import type { CompositionEvent } from './events'; import { EventEmitter } from './events'; -import type { PostProcessVolume } from './components/post-process-volume'; +import type { PostProcessVolume } from './components'; +import { SceneTicking } from './composition/scene-ticking'; +import { SerializationHelper } from './serialization-helper'; +/** + * 合成统计信息 + */ export interface CompositionStatistic { - loadTime: number, loadStart: number, + loadTime: number, + /** + * Shader 编译耗时 + */ + compileTime: number, + /** + * 从加载到渲染第一帧的时间(含 Shader 编译) + */ firstFrameTime: number, - precompileTime: number, } +/** + * 合成消息对象 + */ export interface MessageItem { id: string, name: string, @@ -36,13 +49,6 @@ export interface MessageItem { compositionId: string, } -export interface CompItemClickedData { - name: string, - id: string, - hitPositions: Vector3[], - position: Vector3, -} - /** * */ @@ -52,6 +58,9 @@ export interface CompositionHitTestOptions { skip?: (item: VFXItem) => boolean, } +/** + * + */ export interface CompositionProps { reusable?: boolean, baseRenderOrder?: number, @@ -70,6 +79,10 @@ export interface CompositionProps { */ export class Composition extends EventEmitter> implements Disposable, LostHandler { renderer: Renderer; + /** + * + */ + sceneTicking = new SceneTicking(); /** * 当前帧的渲染数据对象 */ @@ -98,13 +111,17 @@ export class Composition extends EventEmitter> imp * 是否播放完成后销毁 texture 对象 */ keepResource: boolean; - // 3D 模式下创建的场景相机 需要最后更新参数, TODO: 太 hack 了, 待移除 - extraCamera: VFXItem; /** * 合成内的元素否允许点击、拖拽交互 * @since 1.6.0 */ interactive: boolean; + + /** + * 合成是否结束 + */ + isEnded = false; + compositionSourceManager: CompositionSourceManager; /** * 合成id @@ -129,7 +146,7 @@ export class Composition extends EventEmitter> imp /** * 是否在合成结束时自动销毁引用的纹理,合成重播时不销毁 */ - autoRefTex: boolean; + readonly autoRefTex: boolean; /** * 当前合成名称 */ @@ -145,7 +162,7 @@ export class Composition extends EventEmitter> imp /** * 合成对应的 url 或者 JSON */ - readonly url: SceneType; + readonly url: Scene.LoadType; /** * 合成根元素 */ @@ -157,19 +174,19 @@ export class Composition extends EventEmitter> imp /** * 预合成的合成属性,在 content 中会被其元素属性覆盖 */ - refCompositionProps: Map = new Map(); + readonly refCompositionProps: Map = new Map(); /** * 合成的相机对象 */ readonly camera: Camera; /** - * 合成全局时间 + * 后处理渲染配置 */ - globalTime: number; + globalVolume?: PostProcessVolume; /** - * 后处理渲染配置 + * 是否开启后处理 */ - globalVolume: PostProcessVolume; + postProcessingEnabled = false; protected rendererOptions: MeshRendererOptions | null; // TODO: 待优化 @@ -190,8 +207,8 @@ export class Composition extends EventEmitter> imp */ private paused = false; private lastVideoUpdateTime = 0; - // private readonly event: EventSystem; - // texInfo的类型有点不明确,改成不会提前删除texture + private isEndCalled = false; + private readonly texInfo: Record; /** * 合成中消息元素创建/销毁时触发的回调 @@ -219,7 +236,7 @@ export class Composition extends EventEmitter> imp } = props; this.compositionSourceManager = new CompositionSourceManager(scene, renderer.engine); - scene.jsonScene.imgUsage = undefined; + if (reusable) { this.keepResource = true; scene.textures = undefined; @@ -227,6 +244,8 @@ export class Composition extends EventEmitter> imp } const { sourceContent, pluginSystem, imgUsage, totalTime, refCompositionProps } = this.compositionSourceManager; + this.postProcessingEnabled = scene.jsonScene.renderSettings?.postProcessingEnabled ?? false; + assertExist(sourceContent); this.renderer = renderer; this.refCompositionProps = refCompositionProps; @@ -234,23 +253,26 @@ export class Composition extends EventEmitter> imp this.rootItem = new VFXItem(this.getEngine(), sourceContent as unknown as VFXItemProps); this.rootItem.name = 'rootItem'; this.rootItem.composition = this; - this.rootComposition = this.rootItem.addComponent(CompositionComponent); - this.rootComposition.startTime = sourceContent.startTime; - this.rootComposition.data = sourceContent; - const imageUsage = (!reusable && imgUsage) as unknown as Record; + // Spawn rootCompositionComponent + this.rootComposition = this.rootItem.addComponent(CompositionComponent); this.width = width; this.height = height; this.renderOrder = baseRenderOrder; this.id = sourceContent.id; this.renderer = renderer; - this.texInfo = imageUsage ?? {}; + this.texInfo = !reusable ? imgUsage : {}; this.event = event; - this.statistic = { loadTime: totalTime ?? 0, loadStart: scene.startTime ?? 0, firstFrameTime: 0, precompileTime: scene.timeInfos['asyncCompile'] ?? scene.timeInfos['syncCompile'] }; + this.statistic = { + loadStart: scene.startTime ?? 0, + loadTime: totalTime ?? 0, + compileTime: 0, + firstFrameTime: 0, + }; this.reusable = reusable; this.speed = speed; - this.autoRefTex = !this.keepResource && imageUsage && this.rootItem.endBehavior !== spec.EndBehavior.restart; + this.autoRefTex = !this.keepResource && this.texInfo && this.rootItem.endBehavior !== spec.EndBehavior.restart; this.name = sourceContent.name; this.pluginSystem = pluginSystem as PluginSystem; this.pluginSystem.initializeComposition(this, scene); @@ -260,19 +282,14 @@ export class Composition extends EventEmitter> imp }); this.url = scene.url; this.assigned = true; - this.globalTime = 0; this.interactive = true; this.handleItemMessage = handleItemMessage; this.createRenderFrame(); this.rendererOptions = null; + SerializationHelper.deserialize(sourceContent as unknown as spec.EffectsObjectData, this.rootComposition); this.rootComposition.createContent(); + this.buildItemTree(this.rootItem); - this.callAwake(this.rootItem); - this.rootItem.onEnd = () => { - window.setTimeout(() => { - this.emit('end', { composition: this }); - }, 0); - }; this.pluginSystem.resetComposition(this, this.renderFrame); } @@ -301,7 +318,7 @@ export class Composition extends EventEmitter> imp * 获取合成开始渲染的时间 */ get startTime () { - return this.rootComposition.startTime ?? 0; + return this.rootComposition.startTime; } /** @@ -321,7 +338,6 @@ export class Composition extends EventEmitter> imp set viewportMatrix (matrix: Matrix4) { this.camera.setViewportMatrix(matrix); } - get viewportMatrix () { return this.camera.getViewportMatrix(); } @@ -372,7 +388,7 @@ export class Composition extends EventEmitter> imp */ setVisible (visible: boolean) { this.items.forEach(item => { - item.setVisible(visible); + item.setActive(visible); }); } @@ -385,7 +401,7 @@ export class Composition extends EventEmitter> imp } play () { - if (this.rootItem.ended && this.reusable) { + if (this.isEnded && this.reusable) { this.restart(); } if (this.rootComposition.isStartCalled) { @@ -440,6 +456,7 @@ export class Composition extends EventEmitter> imp renderer: this.renderer, keepColorBuffer: this.keepColorBuffer, globalVolume: this.globalVolume, + postProcessingEnabled: this.postProcessingEnabled, }); // TODO 考虑放到构造函数 this.renderFrame.cachedTextures = this.textures; @@ -456,16 +473,13 @@ export class Composition extends EventEmitter> imp if (pause) { this.resume(); } - if (!this.rootComposition.isStartCalled) { - this.rootComposition.start(); - this.rootComposition.isStartCalled = true; - } this.setSpeed(1); this.forwardTime(time + this.startTime); this.setSpeed(speed); if (pause) { this.pause(); } + this.emit('goto', { time }); } addItem (item: VFXItem) { @@ -495,16 +509,15 @@ export class Composition extends EventEmitter> imp */ protected reset () { this.rendererOptions = null; - this.globalTime = 0; - this.rootItem.ended = false; + this.isEnded = false; + this.isEndCalled = false; + this.rootComposition.time = 0; this.pluginSystem.resetComposition(this, this.renderFrame); } prepareRender () { const frame = this.renderFrame; - frame._renderPasses[0].meshes.length = 0; - this.postLoaders.length = 0; this.pluginSystem.plugins.forEach(loader => { if (loader.prepareRenderFrame(this, frame)) { @@ -512,22 +525,9 @@ export class Composition extends EventEmitter> imp } }); - this.gatherRendererComponent(this.rootItem, frame); this.postLoaders.forEach(loader => loader.postProcessFrame(this, frame)); } - protected gatherRendererComponent (vfxItem: VFXItem, renderFrame: RenderFrame) { - for (const rendererComponent of vfxItem.rendererComponents) { - if (rendererComponent.isActiveAndEnabled) { - renderFrame.addMeshToDefaultRenderPass(rendererComponent); - } - } - - for (const item of vfxItem.children) { - this.gatherRendererComponent(item, renderFrame); - } - } - /** * 合成更新,针对所有 item 的更新 * @param deltaTime - 更新的时间步长 @@ -537,65 +537,35 @@ export class Composition extends EventEmitter> imp return; } - const time = this.getUpdateTime(deltaTime * this.speed); + const dt = this.getUpdateTime(deltaTime * this.speed); - this.globalTime += time; - this.updateRootComposition(); + this.updateRootComposition(dt / 1000); this.updateVideo(); // 更新 model-tree-plugin this.updatePluginLoaders(deltaTime); // scene VFXItem components lifetime function. - this.callStart(this.rootItem); - this.callUpdate(this.rootItem, time); - this.callLateUpdate(this.rootItem, time); + if (!this.rootItem.isDuringPlay) { + this.callAwake(this.rootItem); + this.rootItem.beginPlay(); + } + this.sceneTicking.update.tick(dt); + this.sceneTicking.lateUpdate.tick(dt); this.updateCamera(); this.prepareRender(); + if (this.isEnded && !this.isEndCalled) { + this.isEndCalled = true; + this.emit('end', { composition: this }); + } if (this.shouldDispose()) { this.dispose(); } } - private toLocalTime (time: number) { - let localTime = time - this.rootItem.start; - const duration = this.rootItem.duration; - - if (localTime - duration > 0.001) { - if (!this.rootItem.ended) { - this.rootItem.ended = true; - this.emit('end', { composition: this }); - } - - switch (this.rootItem.endBehavior) { - case spec.EndBehavior.restart: { - localTime = localTime % duration; - this.restart(); - - break; - } - case spec.EndBehavior.freeze: { - localTime = Math.min(duration, localTime); - - break; - } - case spec.EndBehavior.forward: { - - break; - } - case spec.EndBehavior.destroy: { - - break; - } - } - } - - return localTime; - } - private shouldDispose () { - return this.rootItem.ended && this.rootItem.endBehavior === spec.EndBehavior.destroy && !this.reusable; + return this.isEnded && this.rootItem.endBehavior === spec.EndBehavior.destroy && !this.reusable; } private getUpdateTime (t: number) { @@ -610,10 +580,10 @@ export class Composition extends EventEmitter> imp } private callAwake (item: VFXItem) { - for (const itemBehaviour of item.itemBehaviours) { - if (!itemBehaviour.isAwakeCalled) { - itemBehaviour.awake(); - itemBehaviour.isAwakeCalled = true; + for (const component of item.components) { + if (!component.isAwakeCalled) { + component.onAwake(); + component.isAwakeCalled = true; } } for (const child of item.children) { @@ -621,69 +591,6 @@ export class Composition extends EventEmitter> imp } } - private callStart (item: VFXItem) { - for (const itemBehaviour of item.itemBehaviours) { - if (itemBehaviour.isActiveAndEnabled && !itemBehaviour.isStartCalled) { - itemBehaviour.start(); - itemBehaviour.isStartCalled = true; - } - } - for (const rendererComponent of item.rendererComponents) { - if (rendererComponent.isActiveAndEnabled && !rendererComponent.isStartCalled) { - rendererComponent.start(); - rendererComponent.isStartCalled = true; - } - } - for (const child of item.children) { - this.callStart(child); - } - } - - private callUpdate (item: VFXItem, dt: number) { - for (const itemBehaviour of item.itemBehaviours) { - if (itemBehaviour.isActiveAndEnabled && itemBehaviour.isStartCalled) { - itemBehaviour.update(dt); - } - } - for (const rendererComponent of item.rendererComponents) { - if (rendererComponent.isActiveAndEnabled && rendererComponent.isStartCalled) { - rendererComponent.update(dt); - } - } - for (const child of item.children) { - if (VFXItem.isComposition(child)) { - if ( - child.ended && - child.endBehavior === spec.EndBehavior.restart - ) { - child.ended = false; - // TODO K帧动画在元素重建后需要 tick ,否则会导致元素位置和 k 帧第一帧位置不一致 - this.callUpdate(child, 0); - } else { - this.callUpdate(child, dt); - } - } else { - this.callUpdate(child, dt); - } - } - } - - private callLateUpdate (item: VFXItem, dt: number) { - for (const itemBehaviour of item.itemBehaviours) { - if (itemBehaviour.isActiveAndEnabled && itemBehaviour.isStartCalled) { - itemBehaviour.lateUpdate(dt); - } - } - for (const rendererComponent of item.rendererComponents) { - if (rendererComponent.isActiveAndEnabled && rendererComponent.isStartCalled) { - rendererComponent.lateUpdate(dt); - } - } - for (const child of item.children) { - this.callLateUpdate(child, dt); - } - } - /** * 构建父子树,同时保存到 itemCacheMap 中便于查找 */ @@ -693,7 +600,6 @@ export class Composition extends EventEmitter> imp } const itemMap = new Map(); - const contentItems = compVFXItem.getComponent(CompositionComponent).items; for (const item of contentItems) { @@ -704,18 +610,11 @@ export class Composition extends EventEmitter> imp if (item.parentId === undefined) { item.setParent(compVFXItem); } else { - // 兼容 treeItem 子元素的 parentId 带 '^' - const parentId = this.getParentIdWithoutSuffix(item.parentId); - const parent = itemMap.get(parentId); + const parent = itemMap.get(item.parentId); if (parent) { - if (VFXItem.isTree(parent) && item.parentId.includes('^')) { - item.parent = parent; - item.transform.parentTransform = parent.getNodeTransform(item.parentId); - } else { - item.parent = parent; - item.transform.parentTransform = parent.transform; - } + item.parent = parent; + item.transform.parentTransform = parent.transform; parent.children.push(item); } else { throw new Error('The element references a non-existent element, please check the data.'); @@ -730,12 +629,6 @@ export class Composition extends EventEmitter> imp } } - private getParentIdWithoutSuffix (id: string) { - const idx = id.lastIndexOf('^'); - - return idx > -1 ? id.substring(0, idx) : id; - } - /** * 更新视频数据到纹理 * @override @@ -769,11 +662,53 @@ export class Composition extends EventEmitter> imp /** * 更新主合成组件 */ - private updateRootComposition () { + private updateRootComposition (deltaTime: number) { if (this.rootComposition.isActiveAndEnabled) { - const localTime = this.toLocalTime(this.globalTime / 1000); + + let localTime = this.time + deltaTime - this.rootItem.start; + let isEnded = false; + + const duration = this.rootItem.duration; + const endBehavior = this.rootItem.endBehavior; + + if (localTime - duration > 0.001) { + + isEnded = true; + + switch (endBehavior) { + case spec.EndBehavior.restart: { + localTime = localTime % duration; + this.restart(); + + break; + } + case spec.EndBehavior.freeze: { + localTime = Math.min(duration, localTime); + + break; + } + case spec.EndBehavior.forward: { + + break; + } + case spec.EndBehavior.destroy: { + + break; + } + } + } this.rootComposition.time = localTime; + + // end state changed, handle onEnd flags + if (this.isEnded !== isEnded) { + if (isEnded) { + this.isEnded = true; + } else { + this.isEnded = false; + this.isEndCalled = false; + } + } } } diff --git a/packages/effects-core/src/composition/scene-ticking.ts b/packages/effects-core/src/composition/scene-ticking.ts new file mode 100644 index 000000000..a90c26ece --- /dev/null +++ b/packages/effects-core/src/composition/scene-ticking.ts @@ -0,0 +1,116 @@ +import { Component } from '../components'; + +/** + * + */ +export class SceneTicking { + update: UpdateTickData = new UpdateTickData(); + lateUpdate: LateUpdateTickData = new LateUpdateTickData(); + + /** + * + * @param obj + */ + addComponent (obj: Component): void { + if (obj.onUpdate !== Component.prototype.onUpdate) { + this.update.addComponent(obj); + } + if (obj.onLateUpdate !== Component.prototype.onLateUpdate) { + this.lateUpdate.addComponent(obj); + } + } + + /** + * + * @param obj + */ + removeComponent (obj: Component): void { + if (obj.onUpdate !== Component.prototype.onUpdate) { + this.update.removeComponent(obj); + } + if (obj.onLateUpdate !== Component.prototype.onLateUpdate) { + this.lateUpdate.removeComponent(obj); + } + } + + /** + * + */ + clear (): void { + this.update.clear(); + this.lateUpdate.clear(); + } +} + +class TickData { + components: Component[] = []; + ticks: ((dt: number) => void)[] = []; + + constructor () { + } + + tick (dt: number) { + this.tickComponents(this.components, dt); + + for (let i = 0;i < this.ticks.length;i++) { + this.ticks[i](dt); + } + } + + tickComponents (components: Component[], dt: number): void { + // To be implemented in derived classes + } + + addComponent (component: Component): void { + if (!this.components.includes(component)) { + this.components.push(component); + } + } + + removeComponent (component: Component): void { + const index = this.components.indexOf(component); + + if (index > -1) { + this.components.splice(index, 1); + } + } + + addTick (method: (dt: number) => void, callee: object) { + const tick = method.bind(callee); + + if (!this.ticks.includes(tick)) { + this.ticks.push(tick); + } + } + + clear (): void { + this.components = []; + } +} + +class UpdateTickData extends TickData { + override tickComponents (components: Component[], dt: number): void { + for (const component of components) { + component.onUpdate(dt); + } + } +} + +class LateUpdateTickData extends TickData { + override tickComponents (components: Component[], dt: number): void { + for (const component of components) { + component.onLateUpdate(dt); + } + } +} + +// function compareComponents (a: Component, b: Component): number { +// const itemA = a.item; +// const itemB = b.item; + +// if (VFXItem.isAncestor(itemA, itemB)) { +// return -1; +// } else { +// return 1; +// } +// } diff --git a/packages/effects-core/src/downloader.ts b/packages/effects-core/src/downloader.ts index 8aefc9310..4b9607269 100644 --- a/packages/effects-core/src/downloader.ts +++ b/packages/effects-core/src/downloader.ts @@ -2,15 +2,6 @@ import { isAndroid } from './utils'; type SuccessHandler = (data: T) => void; type ErrorHandler = (status: number, responseText: string) => void; -/** - * - */ -// type VideoLoadOptions = { -// /** -// * 视频是否循环播放 -// */ -// loop?: boolean, -// }; /** * JSON 值,它可以是字符串、数字、布尔值、对象或者 JSON 值的数组。 @@ -129,8 +120,7 @@ export async function loadWebPOptional (png: string, webp?: string) { const image = await loadImage(webp); return { image, url: webp }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e: any) { + } catch (_) { webPFailed = true; const image = await loadImage(png); @@ -154,8 +144,7 @@ export async function loadAVIFOptional (png: string, avif?: string) { const image = await loadImage(avif); return { image, url: avif }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e: any) { + } catch (_) { avifFailed = true; const image = await loadImage(png); @@ -288,8 +277,7 @@ export async function loadMedia (url: string | string[], loadFn: (url: string) = if (Array.isArray(url)) { try { return await loadFn(url[0]); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e: any) { + } catch (_) { return await loadFn(url[1]); } } diff --git a/packages/effects-core/src/effects-object.ts b/packages/effects-core/src/effects-object.ts index 8e07dfcd2..7aa8582ed 100644 --- a/packages/effects-core/src/effects-object.ts +++ b/packages/effects-core/src/effects-object.ts @@ -6,6 +6,11 @@ import { generateGUID } from './utils'; * @since 2.0.0 */ export abstract class EffectsObject { + /** + * + * @param obj + * @returns + */ static is (obj: unknown): obj is EffectsObject { return obj instanceof EffectsObject && 'guid' in obj; } @@ -16,6 +21,10 @@ export abstract class EffectsObject { */ readonly taggedProperties: Record; + /** + * + * @param engine + */ constructor ( public engine: Engine, ) { @@ -24,16 +33,27 @@ export abstract class EffectsObject { this.engine.addInstance(this); } + /** + * + * @returns + */ getInstanceId () { return this.guid; } + /** + * + * @param guid + */ setInstanceId (guid: string) { this.engine.removeInstance(this.guid); this.guid = guid; this.engine.addInstance(this); } + /** + * + */ toData () { } /** @@ -47,5 +67,8 @@ export abstract class EffectsObject { } } + /** + * + */ dispose () { } } diff --git a/packages/effects-core/src/engine.ts b/packages/effects-core/src/engine.ts index 1cfebc048..22ca7db9a 100644 --- a/packages/effects-core/src/engine.ts +++ b/packages/effects-core/src/engine.ts @@ -15,9 +15,15 @@ import { EffectsPackage } from './effects-package'; * Engine 基类,负责维护所有 GPU 资源的管理及销毁 */ export class Engine implements Disposable { + /** + * 渲染器 + */ renderer: Renderer; emptyTexture: Texture; transparentTexture: Texture; + /** + * GPU 能力 + */ gpuCapability: GPUCapability; jsonSceneData: SceneData; objectInstance: Record; @@ -36,6 +42,9 @@ export class Engine implements Disposable { protected meshes: Mesh[] = []; protected renderPasses: RenderPass[] = []; + /** + * + */ constructor () { this.jsonSceneData = {}; this.objectInstance = {}; diff --git a/packages/effects-core/src/events/event-emitter.ts b/packages/effects-core/src/events/event-emitter.ts index 19a95130e..f4778511d 100644 --- a/packages/effects-core/src/events/event-emitter.ts +++ b/packages/effects-core/src/events/event-emitter.ts @@ -1,3 +1,6 @@ +/** + * + */ export type EventEmitterListener

= (...callbackArgs: P) => void; /** @@ -10,6 +13,9 @@ export type EventEmitterOptions = { once?: boolean, }; +/** + * 事件监听器 + */ export class EventEmitter> { private listeners: Record, options?: EventEmitterOptions }>> = {}; diff --git a/packages/effects-core/src/events/types.ts b/packages/effects-core/src/events/types.ts index 6d1f4a164..d9945e1a8 100644 --- a/packages/effects-core/src/events/types.ts +++ b/packages/effects-core/src/events/types.ts @@ -16,7 +16,7 @@ export type ItemEvent = { }; /** - * Compositio 可以绑定的事件 + * Composition 可以绑定的事件 */ export type CompositionEvent = { /** @@ -33,4 +33,9 @@ export type CompositionEvent = { * 合成行为为销毁/冻结时只会触发一次 */ ['end']: [endInfo: { composition: C }], + /** + * 时间跳转事件 + * 用于在合成中跳转到指定时间 + */ + ['goto']: [gotoInfo: { time: number }], }; diff --git a/packages/effects-core/src/fallback/migration.ts b/packages/effects-core/src/fallback/migration.ts index 0710d2dfb..d18097b78 100644 --- a/packages/effects-core/src/fallback/migration.ts +++ b/packages/effects-core/src/fallback/migration.ts @@ -3,8 +3,7 @@ import type { SpineContent, TimelineAssetData, } from '@galacean/effects-specification'; import { - DataType, END_BEHAVIOR_FREEZE, END_BEHAVIOR_PAUSE, END_BEHAVIOR_PAUSE_AND_DESTROY, - EndBehavior, ItemType, + DataType, END_BEHAVIOR_PAUSE, END_BEHAVIOR_PAUSE_AND_DESTROY, EndBehavior, ItemType, } from '@galacean/effects-specification'; import { generateGUID } from '../utils'; import { convertAnchor, ensureFixedNumber, ensureFixedVec3 } from './utils'; @@ -110,7 +109,7 @@ export function version30Migration (json: JSONSceneLegacy): JSONScene { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison composition.endBehavior === END_BEHAVIOR_PAUSE ) { - composition.endBehavior = END_BEHAVIOR_FREEZE; + composition.endBehavior = EndBehavior.freeze; } // 过滤掉滤镜元素 @@ -361,7 +360,7 @@ export function version30Migration (json: JSONSceneLegacy): JSONScene { break; case ItemType.spine: - item.content.dataType = 'SpineComponent'; + item.content.dataType = DataType.SpineComponent; break; } diff --git a/packages/effects-core/src/fallback/particle.ts b/packages/effects-core/src/fallback/particle.ts index 4f42fbb07..06cda1abc 100644 --- a/packages/effects-core/src/fallback/particle.ts +++ b/packages/effects-core/src/fallback/particle.ts @@ -1,5 +1,5 @@ import type { ParticleContent, ParticleShape, ParticleShapeSphere, ColorOverLifetime } from '@galacean/effects-specification'; -import { ShapeType } from '@galacean/effects-specification'; +import { ParticleEmitterShapeType } from '@galacean/effects-specification'; import { deleteEmptyValue, ensureColorExpression, ensureFixedNumber, ensureFixedNumberWithRandom, ensureFixedVec3, ensureNumberExpression, getGradientColor, objectValueToNumber, @@ -9,7 +9,7 @@ export function getStandardParticleContent (particle: any): ParticleContent { const options = particle.options; const transform = particle.transform; let shape: ParticleShape = { - type: ShapeType.NONE, + type: ParticleEmitterShapeType.NONE, }; if (particle.shape) { @@ -17,7 +17,7 @@ export function getStandardParticleContent (particle: any): ParticleContent { shape = { ...particle.shape, - type: ShapeType[shapeType as keyof typeof ShapeType], + type: ParticleEmitterShapeType[shapeType as keyof typeof ParticleEmitterShapeType], }; if (particle.shape.upDirection) { const [x, y, z] = particle.shape.upDirection; diff --git a/packages/effects-core/src/image-asset.ts b/packages/effects-core/src/image-asset.ts deleted file mode 100644 index 17e938b96..000000000 --- a/packages/effects-core/src/image-asset.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { EffectsObject } from './effects-object'; -import type { ImageSource } from './scene'; - -export class ImageAsset extends EffectsObject { - data: ImageSource; -} diff --git a/packages/effects-core/src/index.ts b/packages/effects-core/src/index.ts index f3af6899b..b42626b64 100644 --- a/packages/effects-core/src/index.ts +++ b/packages/effects-core/src/index.ts @@ -10,6 +10,8 @@ import { VFXItem } from './vfx-item'; export * as math from '@galacean/effects-math/es/core/index'; export * as spec from '@galacean/effects-specification'; +export * from './asset'; +export * from './binary-asset'; export * from './asset-loader'; export * from './asset-manager'; export * from './camera'; @@ -45,10 +47,10 @@ export * from './ticker'; export * from './transform'; export * from './utils'; export * from './vfx-item'; -export * from './binary-asset'; export * from './effects-object'; export * from './effects-package'; export * from './events'; +export * from './pass-render-level'; registerPlugin('camera', CameraVFXItemLoader, VFXItem, true); registerPlugin('text', TextLoader, VFXItem, true); diff --git a/packages/effects-core/src/material/material.ts b/packages/effects-core/src/material/material.ts index a057118ff..e185ddd74 100644 --- a/packages/effects-core/src/material/material.ts +++ b/packages/effects-core/src/material/material.ts @@ -62,7 +62,6 @@ let seed = 1; * Material 抽象类 */ export abstract class Material extends EffectsObject implements Disposable { - shader: Shader; shaderVariant: ShaderVariant; // TODO: 待移除 @@ -76,6 +75,9 @@ export abstract class Material extends EffectsObject implements Disposable { protected destroyed = false; protected initialized = false; + protected shaderDirty = true; + + private _shader: Shader; /** * @@ -106,6 +108,40 @@ export abstract class Material extends EffectsObject implements Disposable { } } + get shader () { + return this._shader; + } + + set shader (value: Shader) { + if (this._shader === value) { + return; + } + this._shader = value; + this.shaderDirty = true; + } + + /** + * 材质的主纹理 + */ + get mainTexture () { + return this.getTexture('_MainTex') as Texture; + } + + set mainTexture (value: Texture) { + this.setTexture('_MainTex', value); + } + + /** + * 材质的主颜色 + */ + get color () { + return this.getColor('_Color') as Color; + } + + set color (value: Color) { + this.setColor('_Color', value); + } + /******** effects-core 中会调用 引擎必须实现 ***********************/ /** * 设置 Material 的颜色融合开关 diff --git a/packages/effects-core/src/math/index.ts b/packages/effects-core/src/math/index.ts index 8afd58095..f6d084b57 100644 --- a/packages/effects-core/src/math/index.ts +++ b/packages/effects-core/src/math/index.ts @@ -1,4 +1,4 @@ export * from './float16array-wrapper'; export * from './translate'; export * from './utils'; -export * from './value-getter'; +export * from './value-getters'; \ No newline at end of file diff --git a/packages/effects-core/src/math/translate.ts b/packages/effects-core/src/math/translate.ts index 927c94f87..0da07cd80 100644 --- a/packages/effects-core/src/math/translate.ts +++ b/packages/effects-core/src/math/translate.ts @@ -2,7 +2,7 @@ import { Euler } from '@galacean/effects-math/es/core/euler'; import { Matrix4 } from '@galacean/effects-math/es/core/matrix4'; import { Vector3 } from '@galacean/effects-math/es/core/vector3'; import type { ItemLinearVelOverLifetime } from '../plugins'; -import type { ValueGetter } from './value-getter'; +import type { ValueGetter } from './value-getters'; export function translatePoint (x: number, y: number): number[] { const origin = [-.5, .5, -.5, -.5, .5, .5, .5, -.5]; diff --git a/packages/effects-core/src/math/value-getters/color-curve.ts b/packages/effects-core/src/math/value-getters/color-curve.ts new file mode 100644 index 000000000..7f64a0986 --- /dev/null +++ b/packages/effects-core/src/math/value-getters/color-curve.ts @@ -0,0 +1,32 @@ +import { Color } from '@galacean/effects-math/es/core/color'; +import type { BezierCurve } from './value-getter'; +import { ValueGetter } from './value-getter'; +import type * as spec from '@galacean/effects-specification'; +import { createValueGetter } from './value-getter-map'; + +export class ColorCurve extends ValueGetter { + private value = new Color(); + + private rCurve: BezierCurve; + private gCurve: BezierCurve; + private bCurve: BezierCurve; + private aCurve: BezierCurve; + + override onCreate (arg: spec.ColorCurveData) { + this.rCurve = createValueGetter(arg[0]) as BezierCurve; + this.gCurve = createValueGetter(arg[1]) as BezierCurve; + this.bCurve = createValueGetter(arg[2]) as BezierCurve; + this.aCurve = createValueGetter(arg[3]) as BezierCurve; + } + + override getValue (t: number): Color { + const r = this.rCurve.getValue(t); + const g = this.gCurve.getValue(t); + const b = this.bCurve.getValue(t); + const a = this.aCurve.getValue(t); + + this.value.set(r, g, b, a); + + return this.value; + } +} \ No newline at end of file diff --git a/packages/effects-core/src/math/value-getters/index.ts b/packages/effects-core/src/math/value-getters/index.ts new file mode 100644 index 000000000..0c1d8ca92 --- /dev/null +++ b/packages/effects-core/src/math/value-getters/index.ts @@ -0,0 +1,4 @@ +export * from './value-getter'; +export * from './color-curve'; +export * from './vector4-curve'; +export * from './value-getter-map'; diff --git a/packages/effects-core/src/math/value-getters/value-getter-map.ts b/packages/effects-core/src/math/value-getters/value-getter-map.ts new file mode 100644 index 000000000..d97a37edf --- /dev/null +++ b/packages/effects-core/src/math/value-getters/value-getter-map.ts @@ -0,0 +1,93 @@ +import * as spec from '@galacean/effects-specification'; +import { colorToArr, isFunction } from '../../utils'; +import { BezierCurve, BezierCurvePath, BezierCurveQuat, GradientValue, LinearValue, LineSegments, PathSegments, RandomSetValue, RandomValue, RandomVectorValue, StaticValue } from './value-getter'; +import { Vector3 } from '@galacean/effects-math/es/core/vector3'; +import { Quaternion } from '@galacean/effects-math/es/core/quaternion'; +import { ColorCurve } from './color-curve'; +import { Vector4Curve } from './vector4-curve'; +import { ValueGetter } from './value-getter'; +import { HELP_LINK } from '../../constants'; + +const map: Record = { + [spec.ValueType.RANDOM] (props: number[][]) { + if (props[0] instanceof Array) { + return new RandomVectorValue(props); + } + + return new RandomValue(props); + }, + [spec.ValueType.CONSTANT] (props: number) { + return new StaticValue(props); + }, + [spec.ValueType.CONSTANT_VEC2] (props: number) { + return new StaticValue(props); + }, + [spec.ValueType.CONSTANT_VEC3] (props: number) { + return new StaticValue(props); + }, + [spec.ValueType.CONSTANT_VEC4] (props: number) { + return new StaticValue(props); + }, + [spec.ValueType.RGBA_COLOR] (props: number) { + return new StaticValue(props); + }, + [spec.ValueType.COLORS] (props: number[][]) { + return new RandomSetValue(props.map(c => colorToArr(c, false))); + }, + [spec.ValueType.LINE] (props: number[][]) { + if (props.length === 2 && props[0][0] === 0 && props[1][0] === 1) { + return new LinearValue([props[0][1], props[1][1]]); + } + + return new LineSegments(props); + }, + [spec.ValueType.GRADIENT_COLOR] (props: number[][] | Record) { + return new GradientValue(props); + }, + [spec.ValueType.LINEAR_PATH] (pros: number[][][]) { + return new PathSegments(pros); + }, + [spec.ValueType.BEZIER_CURVE] (props: number[][][]) { + if (props.length === 1) { + return new StaticValue(props[0][1][1]); + } + + return new BezierCurve(props); + }, + [spec.ValueType.BEZIER_CURVE_PATH] (props: number[][][]) { + if (props[0].length === 1) { + return new StaticValue(new Vector3(...props[1][0])); + } + + return new BezierCurvePath(props); + }, + [spec.ValueType.BEZIER_CURVE_QUAT] (props: number[][][]) { + if (props[0].length === 1) { + return new StaticValue(new Quaternion(...props[1][0])); + } + + return new BezierCurveQuat(props); + }, + [spec.ValueType.COLOR_CURVE] (props: spec.ColorCurveData) { + return new ColorCurve(props); + }, + [spec.ValueType.VECTOR4_CURVE] (props: spec.Vector4CurveData) { + return new Vector4Curve(props); + }, +}; + +export function createValueGetter (args: any): ValueGetter { + if (!args || !isNaN(+args)) { + return new StaticValue(args || 0); + } + + if (args instanceof ValueGetter) { + return args; + } + + if (isFunction(map[args[0]])) { + return map[args[0]](args[1]); + } else { + throw new Error(`ValueType: ${args[0]} is not supported, see ${HELP_LINK['ValueType: 21/22 is not supported']}.`); + } +} \ No newline at end of file diff --git a/packages/effects-core/src/math/value-getter.ts b/packages/effects-core/src/math/value-getters/value-getter.ts similarity index 88% rename from packages/effects-core/src/math/value-getter.ts rename to packages/effects-core/src/math/value-getters/value-getter.ts index 4d96f0edb..9b1c2568c 100644 --- a/packages/effects-core/src/math/value-getter.ts +++ b/packages/effects-core/src/math/value-getters/value-getter.ts @@ -3,13 +3,12 @@ import type { Vector2 } from '@galacean/effects-math/es/core/vector2'; import { Vector3 } from '@galacean/effects-math/es/core/vector3'; import { Quaternion } from '@galacean/effects-math/es/core/quaternion'; import * as spec from '@galacean/effects-specification'; -import { randomInRange, colorToArr, colorStopsFromGradient, interpolateColor, isFunction } from '../utils'; -import type { ColorStop } from '../utils'; -import type { BezierEasing } from './bezier'; -import { BezierPath, buildEasingCurve, BezierQuat } from './bezier'; -import { Float16ArrayWrapper } from './float16array-wrapper'; -import { numberToFix } from './utils'; -import { HELP_LINK } from '../constants'; +import { colorStopsFromGradient, interpolateColor } from '../../utils'; +import type { ColorStop } from '../../utils'; +import type { BezierEasing } from '../bezier'; +import { BezierPath, buildEasingCurve, BezierQuat } from '../bezier'; +import { Float16ArrayWrapper } from '../float16array-wrapper'; +import { numberToFix } from '../utils'; interface KeyFrameMeta { curves: ValueGetter[], @@ -145,8 +144,16 @@ export class RandomValue extends ValueGetter { this.max = props[1]; } - override getValue (time?: number): number { - return randomInRange(this.min, this.max); + override getValue (time?: number, seed?: number): number { + const randomSeed = seed ?? Math.random(); + + return this.min + randomSeed * (this.max - this.min); + } + + override getIntegrateValue (t0: number, t1: number, timeScale?: number): number { + const seed = timeScale ?? 1.0; + + return (this.min + seed * (this.max - this.min)) * (t1 - t0); } override toUniform () { @@ -406,8 +413,11 @@ export class BezierCurve extends ValueGetter { timeInterval: number, valueInterval: number, curve: BezierEasing, + timeStart: number, + timeEnd: number, }>; keys: number[][]; + keyTimeData: string[]; override onCreate (props: spec.BezierKeyframeValue[]) { const keyframes = props; @@ -430,14 +440,21 @@ export class BezierCurve extends ValueGetter { timeInterval, valueInterval, curve, + timeStart:Number(s.x), + timeEnd:Number(e.x), }; } + this.keyTimeData = Object.keys(this.curveMap); } override getValue (time: number) { let result = 0; - const keyTimeData = Object.keys(this.curveMap); - const keyTimeStart = Number(keyTimeData[0].split('&')[0]); - const keyTimeEnd = Number(keyTimeData[keyTimeData.length - 1].split('&')[1]); + const keyTimeData = this.keyTimeData; + + const keyTimeStart = this.curveMap[keyTimeData[0]].timeStart; + const keyTimeEnd = this.curveMap[keyTimeData[keyTimeData.length - 1]].timeEnd; + + // const keyTimeStart = Number(keyTimeData[0].split('&')[0]); + // const keyTimeEnd = Number(keyTimeData[keyTimeData.length - 1].split('&')[1]); if (time <= keyTimeStart) { return this.getCurveValue(keyTimeData[0], keyTimeStart); @@ -447,7 +464,10 @@ export class BezierCurve extends ValueGetter { } for (let i = 0; i < keyTimeData.length; i++) { - const [xMin, xMax] = keyTimeData[i].split('&'); + const xMin = this.curveMap[keyTimeData[i]].timeStart; + const xMax = this.curveMap[keyTimeData[i]].timeEnd; + + // const [xMin, xMax] = keyTimeData[i].split('&'); if (time >= Number(xMin) && time < Number(xMax)) { result = this.getCurveValue(keyTimeData[i], time); @@ -464,13 +484,14 @@ export class BezierCurve extends ValueGetter { let result = 0; const keyTimeData = Object.keys(this.curveMap); - const keyTimeStart = Number(keyTimeData[0].split('&')[0]); + const keyTimeStart = this.curveMap[keyTimeData[0]].timeStart; if (time <= keyTimeStart) { return 0; } for (let i = 0; i < keyTimeData.length; i++) { - const [xMin, xMax] = keyTimeData[i].split('&'); + const xMin = this.curveMap[keyTimeData[i]].timeStart; + const xMax = this.curveMap[keyTimeData[i]].timeEnd; if (time >= Number(xMax)) { result += ts * this.getCurveIntegrateValue(keyTimeData[i], Number(xMax)); @@ -818,84 +839,6 @@ export class BezierCurveQuat extends ValueGetter { } } -const map: Record = { - [spec.ValueType.RANDOM] (props: number[][]) { - if (props[0] instanceof Array) { - return new RandomVectorValue(props); - } - - return new RandomValue(props); - }, - [spec.ValueType.CONSTANT] (props: number) { - return new StaticValue(props); - }, - [spec.ValueType.CONSTANT_VEC2] (props: number) { - return new StaticValue(props); - }, - [spec.ValueType.CONSTANT_VEC3] (props: number) { - return new StaticValue(props); - }, - [spec.ValueType.CONSTANT_VEC4] (props: number) { - return new StaticValue(props); - }, - [spec.ValueType.RGBA_COLOR] (props: number) { - return new StaticValue(props); - }, - [spec.ValueType.COLORS] (props: number[][]) { - return new RandomSetValue(props.map(c => colorToArr(c, false))); - }, - [spec.ValueType.LINE] (props: number[][]) { - if (props.length === 2 && props[0][0] === 0 && props[1][0] === 1) { - return new LinearValue([props[0][1], props[1][1]]); - } - - return new LineSegments(props); - }, - [spec.ValueType.GRADIENT_COLOR] (props: number[][] | Record) { - return new GradientValue(props); - }, - [spec.ValueType.LINEAR_PATH] (pros: number[][][]) { - return new PathSegments(pros); - }, - [spec.ValueType.BEZIER_CURVE] (props: number[][][]) { - if (props.length === 1) { - return new StaticValue(props[0][1][1]); - } - - return new BezierCurve(props); - }, - [spec.ValueType.BEZIER_CURVE_PATH] (props: number[][][]) { - if (props[0].length === 1) { - return new StaticValue(new Vector3(...props[1][0])); - } - - return new BezierCurvePath(props); - }, - [spec.ValueType.BEZIER_CURVE_QUAT] (props: number[][][]) { - if (props[0].length === 1) { - return new StaticValue(new Quaternion(...props[1][0])); - } - - return new BezierCurveQuat(props); - }, -}; - -export function createValueGetter (args: any): ValueGetter { - if (!args || !isNaN(+args)) { - return new StaticValue(args || 0); - } - - if (args instanceof ValueGetter) { - return args; - } - - if (isFunction(map[args[0]])) { - return map[args[0]](args[1]); - } else { - throw new Error(`ValueType: ${args[0]} is not supported, see ${HELP_LINK['ValueType: 21/22 is not supported']}.`); - } -} - function lineSegIntegrate (t: number, t0: number, t1: number, y0: number, y1: number) { const h = t - t0; diff --git a/packages/effects-core/src/math/value-getters/vector4-curve.ts b/packages/effects-core/src/math/value-getters/vector4-curve.ts new file mode 100644 index 000000000..cf6ce8125 --- /dev/null +++ b/packages/effects-core/src/math/value-getters/vector4-curve.ts @@ -0,0 +1,32 @@ +import { Vector4 } from '@galacean/effects-math/es/core/vector4'; +import type { BezierCurve } from './value-getter'; +import { ValueGetter } from './value-getter'; +import type * as spec from '@galacean/effects-specification'; +import { createValueGetter } from './value-getter-map'; + +export class Vector4Curve extends ValueGetter { + private value = new Vector4(); + + private xCurve: BezierCurve; + private yCurve: BezierCurve; + private zCurve: BezierCurve; + private wCurve: BezierCurve; + + override onCreate (arg: spec.Vector4CurveData) { + this.xCurve = createValueGetter(arg[0]) as BezierCurve; + this.yCurve = createValueGetter(arg[1]) as BezierCurve; + this.zCurve = createValueGetter(arg[2]) as BezierCurve; + this.wCurve = createValueGetter(arg[3]) as BezierCurve; + } + + override getValue (t: number): Vector4 { + const x = this.xCurve.getValue(t); + const y = this.yCurve.getValue(t); + const z = this.zCurve.getValue(t); + const w = this.wCurve.getValue(t); + + this.value.set(x, y, z, w); + + return this.value; + } +} \ No newline at end of file diff --git a/packages/effects-core/src/plugin-system.ts b/packages/effects-core/src/plugin-system.ts index c3106252d..1d21cd70c 100644 --- a/packages/effects-core/src/plugin-system.ts +++ b/packages/effects-core/src/plugin-system.ts @@ -4,8 +4,7 @@ import type { Plugin, PluginConstructor } from './plugins'; import type { RenderFrame, Renderer } from './render'; import type { Scene, SceneLoadOptions } from './scene'; import { addItem, removeItem, logger } from './utils'; -import type { VFXItemConstructor, VFXItemProps } from './vfx-item'; -import { VFXItem } from './vfx-item'; +import type { VFXItemConstructor } from './vfx-item'; export const pluginLoaderMap: Record = {}; export const defaultPlugins: string[] = []; @@ -92,29 +91,27 @@ export class PluginSystem { this.plugins.forEach(loader => loader.onCompositionReset(comp, renderFrame)); } - createPluginItem (name: string, props: VFXItemProps, composition: Composition): VFXItem { - const CTRL = pluginCtrlMap[name]; - - if (!CTRL) { - throw new Error(`The plugin '${name}' does not have a registered constructor.`); - } - const engine = composition.getEngine(); - const item = new CTRL(engine, props, composition); - - item.composition = composition; + async processRawJSON (json: spec.JSONScene, options: SceneLoadOptions): Promise { + return this.callStatic('processRawJSON', json, options); + } - if (!(item instanceof VFXItem)) { - throw new Error(`The plugin '${name}' invalid constructor type.`); - } + async processAssets (json: spec.JSONScene, options?: SceneLoadOptions) { + return this.callStatic<{ assets: spec.AssetBase[], loadedAssets: unknown[] }>('processAssets', json, options); + } - return item; + async precompile ( + compositions: spec.CompositionData[], + renderer: Renderer, + options?: PrecompileOptions, + ) { + return this.callStatic('precompile', compositions, renderer, options); } - async processRawJSON (json: spec.JSONScene, options: SceneLoadOptions): Promise { - return this.callStatic('processRawJSON', json, options); + async loadResources (scene: Scene, options: SceneLoadOptions) { + return this.callStatic('prepareResource', scene, options); } - private async callStatic (name: string, ...args: any[]): Promise { + private async callStatic (name: string, ...args: any[]): Promise { const pendings = []; const plugins = this.plugins; @@ -123,30 +120,17 @@ export class PluginSystem { const ctrl = pluginLoaderMap[plugin.name]; if (name in ctrl) { - pendings.push(Promise.resolve(ctrl[name]?.(...args))); + pendings.push(Promise.resolve(ctrl[name]?.(...args))); } } return Promise.all(pendings); } - - async precompile ( - compositions: spec.CompositionData[], - renderer: Renderer, - options?: PrecompileOptions, - ) { - return this.callStatic('precompile', compositions, renderer, options); - } - - async loadResources (scene: Scene, options: SceneLoadOptions) { - return this.callStatic('prepareResource', scene, options); - } } const pluginInfoMap: Record = { 'alipay-downgrade': '@galacean/effects-plugin-alipay-downgrade', 'editor-gizmo': '@galacean/effects-plugin-editor-gizmo', - 'tree': '@galacean/effects-plugin-model', 'model': '@galacean/effects-plugin-model', 'orientation-transformer': '@galacean/effects-plugin-orientation-transformer', 'spine': '@galacean/effects-plugin-spine', diff --git a/packages/effects-core/src/plugins/cal/calculate-item.ts b/packages/effects-core/src/plugins/cal/calculate-item.ts index 54948c858..55f4b346b 100644 --- a/packages/effects-core/src/plugins/cal/calculate-item.ts +++ b/packages/effects-core/src/plugins/cal/calculate-item.ts @@ -1,11 +1,13 @@ -import type { Euler, Vector3 } from '@galacean/effects-math/es/core/index'; +import * as spec from '@galacean/effects-specification'; +import type { Euler } from '@galacean/effects-math/es/core/euler'; +import type { Vector3 } from '@galacean/effects-math/es/core/vector3'; import { effectsClass } from '../../decorators'; import type { ValueGetter } from '../../math'; -import type { VFXItem } from '../../vfx-item'; +import { VFXItem } from '../../vfx-item'; import { ParticleSystem } from '../particle/particle-system'; import { ParticleBehaviourPlayableAsset } from '../particle/particle-vfx-item'; -import { TrackAsset } from '../timeline/track'; -import type { TimelineAsset } from './timeline-asset'; +import { TrackAsset } from '../timeline'; +import type { TimelineAsset } from '../timeline'; /** * 基础位移属性数据 @@ -28,17 +30,24 @@ export type ItemLinearVelOverLifetime = { /** * @since 2.0.0 */ -@effectsClass('ObjectBindingTrack') +@effectsClass(spec.DataType.ObjectBindingTrack) export class ObjectBindingTrack extends TrackAsset { + override updateAnimatedObject (): void { + } + create (timelineAsset: TimelineAsset): void { - const boundItem = this.binding as VFXItem; + if (!(this.boundObject instanceof VFXItem)) { + return; + } + + const boundItem = this.boundObject; - // 添加粒子动画 clip + // 添加粒子动画 clip // TODO 待移除 if (boundItem.getComponent(ParticleSystem)) { const particleTrack = timelineAsset.createTrack(TrackAsset, this, 'ParticleTrack'); - particleTrack.binding = this.binding; + particleTrack.boundObject = this.boundObject; const particleClip = particleTrack.createClip(ParticleBehaviourPlayableAsset); particleClip.start = boundItem.start; diff --git a/packages/effects-core/src/plugins/cal/calculate-loader.ts b/packages/effects-core/src/plugins/cal/calculate-loader.ts index a79ee4d5c..e2d6d1af8 100644 --- a/packages/effects-core/src/plugins/cal/calculate-loader.ts +++ b/packages/effects-core/src/plugins/cal/calculate-loader.ts @@ -1,3 +1,3 @@ -import { AbstractPlugin } from '../index'; +import { AbstractPlugin } from '../plugin'; export class CalculateLoader extends AbstractPlugin {} diff --git a/packages/effects-core/src/plugins/cal/calculate-vfx-item.ts b/packages/effects-core/src/plugins/cal/calculate-vfx-item.ts index 99176d750..aae57bedf 100644 --- a/packages/effects-core/src/plugins/cal/calculate-vfx-item.ts +++ b/packages/effects-core/src/plugins/cal/calculate-vfx-item.ts @@ -50,10 +50,10 @@ export class TransformAnimationPlayable extends AnimationPlayable { startSpeed: number; data: TransformPlayableAssetData; private velocity: Vector3; - private binding: VFXItem; + private boundObject: VFXItem; start (): void { - const boundItem = this.binding; + const boundItem = this.boundObject; const scale = boundItem.transform.scale; this.originalTransform = { @@ -133,15 +133,15 @@ export class TransformAnimationPlayable extends AnimationPlayable { } override processFrame (context: FrameContext): void { - if (!this.binding) { + if (!this.boundObject) { const boundObject = context.output.getUserData(); if (boundObject instanceof VFXItem) { - this.binding = boundObject; + this.boundObject = boundObject; this.start(); } } - if (this.binding && this.binding.composition) { + if (this.boundObject && this.boundObject.composition) { this.sampleAnimation(); } } @@ -150,7 +150,7 @@ export class TransformAnimationPlayable extends AnimationPlayable { * 应用时间轴K帧数据到对象 */ private sampleAnimation () { - const boundItem = this.binding; + const boundItem = this.boundObject; const duration = boundItem.duration; let life = this.time / duration; @@ -203,7 +203,7 @@ export class TransformAnimationPlayable extends AnimationPlayable { } } -@effectsClass('TransformPlayableAsset') +@effectsClass(spec.DataType.TransformPlayableAsset) export class TransformPlayableAsset extends PlayableAsset { transformAnimationData: TransformPlayableAssetData; @@ -241,10 +241,17 @@ export interface TransformPlayableAssetData extends spec.EffectsObjectData { export class ActivationPlayable extends Playable { override processFrame (context: FrameContext): void { + const vfxItem = context.output.getUserData(); + + if (!(vfxItem instanceof VFXItem)) { + return; + } + + vfxItem.time = this.time; } } -@effectsClass('ActivationPlayableAsset') +@effectsClass(spec.DataType.ActivationPlayableAsset) export class ActivationPlayableAsset extends PlayableAsset { override createPlayable (graph: PlayableGraph): Playable { return new ActivationPlayable(graph); diff --git a/packages/effects-core/src/plugins/cal/playable-graph.ts b/packages/effects-core/src/plugins/cal/playable-graph.ts index f9ac2bdc9..6e66e5b35 100644 --- a/packages/effects-core/src/plugins/cal/playable-graph.ts +++ b/packages/effects-core/src/plugins/cal/playable-graph.ts @@ -67,6 +67,7 @@ export class Playable implements Disposable { onPlayablePlayFlag = true; onPlayablePauseFlag = false; + private duration = 0; private destroyed = false; private inputs: Playable[] = []; private inputOuputPorts: number[] = []; @@ -78,7 +79,7 @@ export class Playable implements Disposable { /** * 当前本地播放的时间 */ - protected time: number; + protected time: number = 0; constructor (graph: PlayableGraph, inputCount = 0) { graph.addPlayable(this); @@ -185,6 +186,14 @@ export class Playable implements Disposable { return this.time; } + setDuration (duration: number) { + this.duration = duration; + } + + getDuration () { + return this.duration; + } + getPlayState () { return this.playState; } @@ -347,7 +356,7 @@ export class PlayableOutput { }; } - setSourcePlayeble (playable: Playable, port = 0) { + setSourcePlayable (playable: Playable, port = 0) { this.sourcePlayable = playable; this.sourceOutputPort = port; } diff --git a/packages/effects-core/src/plugins/cal/timeline-asset.ts b/packages/effects-core/src/plugins/cal/timeline-asset.ts deleted file mode 100644 index 791d5b5e0..000000000 --- a/packages/effects-core/src/plugins/cal/timeline-asset.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type * as spec from '@galacean/effects-specification'; -import { effectsClass, serialize } from '../../decorators'; -import { VFXItem } from '../../vfx-item'; -import type { RuntimeClip, TrackAsset } from '../timeline/track'; -import { ObjectBindingTrack } from './calculate-item'; -import type { FrameContext, PlayableGraph } from './playable-graph'; -import { Playable, PlayableAsset, PlayableTraversalMode } from './playable-graph'; -import type { Constructor } from '../../utils'; - -@effectsClass('TimelineAsset') -export class TimelineAsset extends PlayableAsset { - @serialize() - tracks: TrackAsset[] = []; - - override createPlayable (graph: PlayableGraph): Playable { - const timelinePlayable = new TimelinePlayable(graph); - - timelinePlayable.setTraversalMode(PlayableTraversalMode.Passthrough); - for (const track of this.tracks) { - if (track instanceof ObjectBindingTrack) { - track.create(this); - } - } - timelinePlayable.compileTracks(graph, this.tracks); - - return timelinePlayable; - } - - createTrack (classConstructor: Constructor, parent: TrackAsset, name?: string): T { - const newTrack = new classConstructor(this.engine); - - newTrack.name = name ? name : classConstructor.name; - parent.addChild(newTrack); - - return newTrack; - } - - override fromData (data: spec.TimelineAssetData): void { - } -} - -export class TimelinePlayable extends Playable { - clips: RuntimeClip[] = []; - - override prepareFrame (context: FrameContext): void { - this.evaluate(); - } - - evaluate () { - const time = this.getTime(); - - // TODO search active clips - - for (const clip of this.clips) { - clip.evaluateAt(time); - } - } - - compileTracks (graph: PlayableGraph, tracks: TrackAsset[]) { - this.sortTracks(tracks); - const outputTrack: TrackAsset[] = []; - - for (const masterTrack of tracks) { - outputTrack.push(masterTrack); - this.addSubTracksRecursive(masterTrack, outputTrack); - } - - for (const track of outputTrack) { - const trackMixPlayable = track.createPlayableGraph(graph, this.clips); - - this.addInput(trackMixPlayable, 0); - const trackOutput = track.createOutput(); - - trackOutput.setUserData(track.binding); - - graph.addOutput(trackOutput); - trackOutput.setSourcePlayeble(this, this.getInputCount() - 1); - } - } - - private sortTracks (tracks: TrackAsset[]) { - const sortedTracks = []; - - for (let i = 0; i < tracks.length; i++) { - sortedTracks.push(new TrackSortWrapper(tracks[i], i)); - } - sortedTracks.sort(compareTracks); - tracks.length = 0; - for (const trackWrapper of sortedTracks) { - tracks.push(trackWrapper.track); - } - } - - private addSubTracksRecursive (track: TrackAsset, allTracks: TrackAsset[]) { - for (const subTrack of track.getChildTracks()) { - allTracks.push(subTrack); - } - for (const subTrack of track.getChildTracks()) { - this.addSubTracksRecursive(subTrack, allTracks); - } - } -} - -export class TrackSortWrapper { - track: TrackAsset; - originalIndex: number; - - constructor (track: TrackAsset, originalIndex: number) { - this.track = track; - this.originalIndex = originalIndex; - } -} - -function isAncestor ( - ancestorCandidate: VFXItem, - descendantCandidate: VFXItem, -) { - let current = descendantCandidate.parent; - - while (current) { - if (current === ancestorCandidate) { - return true; - } - current = current.parent; - } - - return false; -} - -function compareTracks (a: TrackSortWrapper, b: TrackSortWrapper): number { - const bindingA = a.track.binding; - const bindingB = b.track.binding; - - if (!(bindingA instanceof VFXItem) || !(bindingB instanceof VFXItem)) { - return a.originalIndex - b.originalIndex; - } - - if (isAncestor(bindingA, bindingB)) { - return -1; - } else if (isAncestor(bindingB, bindingA)) { - return 1; - } else { - return a.originalIndex - b.originalIndex; // 非父子关系的元素保持原始顺序 - } -} diff --git a/packages/effects-core/src/plugins/camera/camera-controller-node.ts b/packages/effects-core/src/plugins/camera/camera-controller-node.ts index 843802897..09a617fdd 100644 --- a/packages/effects-core/src/plugins/camera/camera-controller-node.ts +++ b/packages/effects-core/src/plugins/camera/camera-controller-node.ts @@ -18,7 +18,7 @@ export class CameraController extends Behaviour { } } - override update () { + override onUpdate () { if (this.item.composition && this.item.transform.getValid()) { const camera = this.item.composition.camera; diff --git a/packages/effects-core/src/plugins/camera/camera-vfx-item-loader.ts b/packages/effects-core/src/plugins/camera/camera-vfx-item-loader.ts index 78e842718..0aef82bbc 100644 --- a/packages/effects-core/src/plugins/camera/camera-vfx-item-loader.ts +++ b/packages/effects-core/src/plugins/camera/camera-vfx-item-loader.ts @@ -1,4 +1,4 @@ -import { AbstractPlugin } from '../index'; +import { AbstractPlugin } from '../plugin'; export class CameraVFXItemLoader extends AbstractPlugin { } diff --git a/packages/effects-core/src/plugins/index.ts b/packages/effects-core/src/plugins/index.ts index 1f28aa49c..f8522dde7 100644 --- a/packages/effects-core/src/plugins/index.ts +++ b/packages/effects-core/src/plugins/index.ts @@ -7,6 +7,7 @@ export * from './interact/interact-loader'; export * from './interact/interact-mesh'; export * from './interact/interact-vfx-item'; export * from './interact/interact-item'; +export * from './interact/mesh-collider'; export * from './sprite/sprite-loader'; export * from './sprite/sprite-item'; export * from './sprite/sprite-mesh'; @@ -18,12 +19,5 @@ export * from './particle/particle-system-renderer'; export * from './cal/calculate-loader'; export * from './cal/calculate-vfx-item'; export * from './cal/calculate-item'; -export * from './timeline/track'; -export * from './timeline/tracks/transform-track'; -export * from './timeline/tracks/activation-track'; -export * from './timeline/tracks/sprite-color-track'; -export * from './timeline/tracks/sub-composition-track'; -export * from './timeline/playables/sub-composition-playable-asset'; -export * from './cal/timeline-asset'; - +export * from './timeline'; export * from './text'; diff --git a/packages/effects-core/src/plugins/interact/interact-item.ts b/packages/effects-core/src/plugins/interact/interact-item.ts index e2268400a..bdc458081 100644 --- a/packages/effects-core/src/plugins/interact/interact-item.ts +++ b/packages/effects-core/src/plugins/interact/interact-item.ts @@ -13,6 +13,7 @@ import type { Renderer } from '../../render'; import { effectsClass } from '../../decorators'; /** + * 交互组件 * @since 2.0.0 */ @effectsClass(spec.DataType.InteractComponent) @@ -30,10 +31,21 @@ export class InteractComponent extends RendererComponent { * 拖拽的距离映射系数,越大越容易拖动 */ dragRatio: number[] = [1, 1]; + /** + * 拖拽X范围 + */ + dragRange: { + dxRange: [min: number, max: number], + dyRange: [min: number, max: number], + } = { + dxRange: [0, 0], + dyRange: [0, 0], + }; + + private duringPlay = false; /** 是否响应点击和拖拽交互事件 */ private _interactive = true; - private hasBeenAddedToComposition = false; set interactive (enable: boolean) { this._interactive = enable; @@ -47,7 +59,23 @@ export class InteractComponent extends RendererComponent { return this._interactive; } - override start (): void { + getDragRangeX (): [min: number, max: number] { + return this.dragRange.dxRange; + } + + setDragRangeX (min: number, max: number) { + this.dragRange.dxRange = [min, max]; + } + + getDragRangeY (): [min: number, max: number] { + return this.dragRange.dyRange; + } + + setDragRangeY (min: number, max: number) { + this.dragRange.dyRange = [min, max]; + } + + override onStart (): void { const options = this.item.props.content.options as spec.DragInteractOption; const { env } = this.item.engine.renderer; const composition = this.item.composition; @@ -73,26 +101,42 @@ export class InteractComponent extends RendererComponent { this.materials = this.previewContent.mesh.materials; } this.item.getHitTestParams = this.getHitTestParams; - this.item.onEnd = () => { - if (this.item && this.item.composition) { + } + + override onDisable (): void { + super.onDisable(); + if (this.item && this.item.composition) { + if (this.duringPlay && !this.item.transform.getValid()) { this.item.composition.removeInteractiveItem(this.item, (this.item.props as spec.InteractItem).content.options.type); - this.clickable = false; - this.hasBeenAddedToComposition = false; - this.previewContent?.mesh.dispose(); - this.endDragTarget(); + this.duringPlay = false; } - }; + this.clickable = false; + this.previewContent?.mesh.dispose(); + this.endDragTarget(); + } } - override update (dt: number): void { - this.previewContent?.updateMesh(); - if (!this.hasBeenAddedToComposition && this.item.composition) { + override onEnable (): void { + super.onEnable(); + const { type } = this.interactData.options as spec.ClickInteractOption; + + if (type === spec.InteractType.CLICK) { + this.clickable = true; + } + } + + override onUpdate (dt: number): void { + this.duringPlay = true; + + // trigger messageBegin when item enter + if (this.item.time > 0 && this.item.time - dt / 1000 <= 0) { const options = this.item.props.content.options as spec.DragInteractOption; - this.item.composition.addInteractiveItem(this.item, options.type); - this.hasBeenAddedToComposition = true; + this.item.composition?.addInteractiveItem(this.item, options.type); } + this.previewContent?.updateMesh(); + if (!this.dragEvent || !this.bouncingArg) { return; } @@ -128,7 +172,6 @@ export class InteractComponent extends RendererComponent { return; } - const options = (this.item.props as spec.InteractItem).content.options as spec.DragInteractOption; const { position, fov } = evt.cameraParam; const dy = event.dy; const dx = event.dx * event.width / event.height; @@ -136,24 +179,20 @@ export class InteractComponent extends RendererComponent { const sp = Math.tan(fov * Math.PI / 180 / 2) * Math.abs(depth); const height = dy * sp; const width = dx * sp; + const { dxRange, dyRange } = this.dragRange; let nx = position[0] - this.dragRatio[0] * width; let ny = position[1] - this.dragRatio[1] * height; - if (options.dxRange) { - const [min, max] = options.dxRange; + const [xMin, xMax] = dxRange; + const [yMin, yMax] = dyRange; - nx = clamp(nx, min, max); - if (nx !== min && nx !== max && min !== max) { - event.origin?.preventDefault(); - } + nx = clamp(nx, xMin, xMax); + ny = clamp(ny, yMin, yMax); + if (nx !== xMin && nx !== xMax && xMin !== xMax) { + event.origin?.preventDefault(); } - if (options.dyRange) { - const [min, max] = options.dyRange; - - ny = clamp(ny, min, max); - if (ny !== min && ny !== max && min !== max) { - event.origin?.preventDefault(); - } + if (ny !== yMin && ny !== yMax && yMin !== yMax) { + event.origin?.preventDefault(); } this.item.composition.camera.position = new Vector3(nx, ny, depth); } @@ -162,7 +201,6 @@ export class InteractComponent extends RendererComponent { if (options.target !== 'camera') { return; } - let dragEvent: Partial | null; const handlerMap: Record void> = { touchstart: (event: TouchEventType) => { @@ -250,6 +288,16 @@ export class InteractComponent extends RendererComponent { override fromData (data: spec.InteractContent): void { super.fromData(data); this.interactData = data; + if (data.options.type === spec.InteractType.DRAG) { + const options = data.options as spec.DragInteractOption; + + if (options.dxRange) { + this.dragRange.dxRange = options.dxRange; + } + if (options.dyRange) { + this.dragRange.dyRange = options.dyRange; + } + } } canInteract (): boolean { diff --git a/packages/effects-core/src/plugins/interact/interact-loader.ts b/packages/effects-core/src/plugins/interact/interact-loader.ts index 1f8d94834..fa296e8da 100644 --- a/packages/effects-core/src/plugins/interact/interact-loader.ts +++ b/packages/effects-core/src/plugins/interact/interact-loader.ts @@ -1,4 +1,4 @@ -import { AbstractPlugin } from '../index'; +import { AbstractPlugin } from '../plugin'; export class InteractLoader extends AbstractPlugin { } diff --git a/packages/effects-core/src/plugins/interact/mesh-collider.ts b/packages/effects-core/src/plugins/interact/mesh-collider.ts new file mode 100644 index 000000000..616b92820 --- /dev/null +++ b/packages/effects-core/src/plugins/interact/mesh-collider.ts @@ -0,0 +1,93 @@ +import type { TriangleLike } from '@galacean/effects-math/es/core/type'; +import type { Geometry } from '../../render/geometry'; +import type { BoundingBoxTriangle } from './click-handler'; +import { HitTestType } from './click-handler'; +import type { Matrix4 } from '@galacean/effects-math/es/core/matrix4'; +import { Vector3 } from '@galacean/effects-math/es/core/vector3'; + +/** + * + */ +export class MeshCollider { + private boundingBoxData: BoundingBoxTriangle; + private geometry: Geometry; + private triangles: TriangleLike[] = []; + + getBoundingBoxData (): BoundingBoxTriangle { + return this.boundingBoxData; + } + + getBoundingBox (): BoundingBoxTriangle { + let maxX = -Number.MAX_VALUE; + let maxY = -Number.MAX_VALUE; + + let minX = Number.MAX_VALUE; + let minY = Number.MAX_VALUE; + + for (const triangle of this.boundingBoxData.area) { + maxX = Math.max(triangle.p0.x, triangle.p1.x, triangle.p2.x, maxX); + maxY = Math.max(triangle.p0.y, triangle.p1.y, triangle.p2.y, maxY); + minX = Math.min(triangle.p0.x, triangle.p1.x, triangle.p2.x, minX); + minY = Math.min(triangle.p0.y, triangle.p1.y, triangle.p2.y, minY); + } + + const area = []; + + const point0 = new Vector3(minX, maxY, 0); + const point1 = new Vector3(maxX, maxY, 0); + const point2 = new Vector3(maxX, minY, 0); + const point3 = new Vector3(minX, minY, 0); + + area.push({ p0: point0, p1: point1, p2: point2 }); + area.push({ p0: point0, p1: point2, p2: point3 }); + + return { + type: HitTestType.triangle, + area, + }; + } + + setGeometry (geometry: Geometry, worldMatrix?: Matrix4) { + if (this.geometry !== geometry) { + this.triangles = this.geometryToTriangles(geometry); + this.geometry = geometry; + } + const area = []; + + for (const triangle of this.triangles) { + area.push({ p0: triangle.p0, p1: triangle.p1, p2: triangle.p2 }); + } + + if (worldMatrix) { + area.forEach(triangle => { + triangle.p0 = worldMatrix.transformPoint(triangle.p0 as Vector3, new Vector3()); + triangle.p1 = worldMatrix.transformPoint(triangle.p1 as Vector3, new Vector3()); + triangle.p2 = worldMatrix.transformPoint(triangle.p2 as Vector3, new Vector3()); + }); + } + + this.boundingBoxData = { + type: HitTestType.triangle, + area, + }; + } + + private geometryToTriangles (geometry: Geometry) { + const indices = geometry.getIndexData() ?? []; + const vertices = geometry.getAttributeData('aPos') ?? []; + const res: TriangleLike[] = []; + + for (let i = 0; i < indices.length; i += 3) { + const index0 = indices[i] * 3; + const index1 = indices[i + 1] * 3; + const index2 = indices[i + 2] * 3; + const p0 = { x: vertices[index0], y: vertices[index0 + 1], z: vertices[index0 + 2] }; + const p1 = { x: vertices[index1], y: vertices[index1 + 1], z: vertices[index1 + 2] }; + const p2 = { x: vertices[index2], y: vertices[index2 + 1], z: vertices[index2 + 2] }; + + res.push({ p0, p1, p2 }); + } + + return res; + } +} diff --git a/packages/effects-core/src/plugins/particle/particle-mesh.ts b/packages/effects-core/src/plugins/particle/particle-mesh.ts index c20374549..d094985af 100644 --- a/packages/effects-core/src/plugins/particle/particle-mesh.ts +++ b/packages/effects-core/src/plugins/particle/particle-mesh.ts @@ -5,6 +5,8 @@ import { Quaternion } from '@galacean/effects-math/es/core/quaternion'; import { Vector2 } from '@galacean/effects-math/es/core/vector2'; import { Vector3 } from '@galacean/effects-math/es/core/vector3'; import { Vector4 } from '@galacean/effects-math/es/core/vector4'; +import { Matrix3 } from '@galacean/effects-math/es/core/matrix3'; +import { clamp } from '@galacean/effects-math/es/core/utils'; import type { Engine } from '../../engine'; import { getConfig, RENDER_PREFER_LOOKUP_TEXTURE } from '../../config'; import { PLAYER_OPTIONS_ENV_EDITOR } from '../../constants'; @@ -14,6 +16,7 @@ import { } from '../../material'; import { createKeyFrameMeta, createValueGetter, ValueGetter, getKeyFrameMetaByRawValue, + RandomValue, } from '../../math'; import type { Attribute, GPUCapability, GeometryProps, ShaderMacros, SharedShaderWithSource, @@ -132,6 +135,13 @@ export class ParticleMesh implements ParticleMeshData { readonly maxCount: number; readonly anchor: Vector2; + private cachedRotationVector3 = new Vector3(); + private cachedRotationMatrix = new Matrix3(); + private cachedLinearMove = new Vector3(); + private tempMatrix3 = new Matrix3(); + + VERT_MAX_KEY_FRAME_COUNT = 0; + constructor ( engine: Engine, props: ParticleMeshProps, @@ -289,6 +299,7 @@ export class ParticleMesh implements ParticleMeshData { ['VERT_MAX_KEY_FRAME_COUNT', vertexKeyFrameMeta.max], ['FRAG_MAX_KEY_FRAME_COUNT', fragmentKeyFrameMeta.max], ); + this.VERT_MAX_KEY_FRAME_COUNT = vertexKeyFrameMeta.max; const fragment = particleFrag; const originalVertex = `#define LOOKUP_TEXTURE_CURVE ${vertex_lookup_texture}\n${particleVert}`; @@ -325,7 +336,7 @@ export class ParticleMesh implements ParticleMeshData { material.blending = true; material.depthTest = true; - material.depthMask = !!(occlusion); + material.depthMask = !!occlusion; material.stencilRef = mask ? [mask, mask] : undefined; setMaskMode(material, maskMode); setBlendMode(material, blending); @@ -391,6 +402,7 @@ export class ParticleMesh implements ParticleMeshData { this.orbitalVelOverLifetime = orbitalVelOverLifetime; this.orbitalVelOverLifetime = orbitalVelOverLifetime; this.gravityModifier = gravityModifier; + this.rotationOverLifetime = rotationOverLifetime; this.maxCount = maxCount; // this.duration = duration; this.textureOffsets = textureFlip ? [0, 0, 1, 0, 0, 1, 1, 1] : [0, 1, 0, 0, 1, 1, 1, 0]; @@ -440,15 +452,22 @@ export class ParticleMesh implements ParticleMeshData { geometry.setIndexData(new index.constructor(0)); } - minusTime (time: number) { - const data = this.geometry.getAttributeData('aOffset'); + onUpdate (dt: number) { + const aPosArray = this.geometry.getAttributeData('aPos') as Float32Array; // vector3 + const vertexCount = Math.ceil(aPosArray.length / 12); - assertExist(data); + this.applyTranslation(vertexCount, dt); + this.applyRotation(vertexCount, dt); + this.applyLinearMove(vertexCount, dt); + } - for (let i = 0; i < data.length; i += 4) { - data[i + 2] -= time; + minusTime (time: number) { + const aOffset = this.geometry.getAttributeData('aOffset') as Float32Array; + + for (let i = 0; i < aOffset.length; i += 4) { + aOffset[i + 2] -= time; } - this.geometry.setAttributeData('aOffset', data); + this.geometry.setAttributeData('aOffset', aOffset); this.time -= time; } @@ -479,6 +498,9 @@ export class ParticleMesh implements ParticleMeshData { aPos: new Float32Array(48), aRot: new Float32Array(32), aOffset: new Float32Array(16), + aTranslation: new Float32Array(12), + aLinearMove:new Float32Array(12), + aRotation0: new Float32Array(36), }; const useSprite = this.useSprite; @@ -578,6 +600,291 @@ export class ParticleMesh implements ParticleMeshData { geometry.setDrawCount(this.particleCount * 6); } } + + private applyTranslation (vertexCount: number, deltaTime: number) { + const localTime = this.time; + let aTranslationArray = this.geometry.getAttributeData('aTranslation') as Float32Array; + const aVelArray = this.geometry.getAttributeData('aVel') as Float32Array; // vector3 + const aOffsetArray = this.geometry.getAttributeData('aOffset') as Float32Array; + + if (aTranslationArray.length < vertexCount * 3) { + aTranslationArray = this.expandArray(aTranslationArray, vertexCount * 3); + } + // const velocity = this.cachedVelocity; + let velocityX = 0; + let velocityY = 0; + let velocityZ = 0; + const uAcceleration = this.mesh.material.getVector4('uAcceleration'); + const uGravityModifierValue = this.mesh.material.getVector4('uGravityModifierValue'); + + for (let i = 0; i < vertexCount; i += 4) { + const velOffset = i * 12 + 3; + + velocityX = aVelArray[velOffset]; + velocityY = aVelArray[velOffset + 1]; + velocityZ = aVelArray[velOffset + 2]; + // velocity.set(aVelArray[velOffset], aVelArray[velOffset + 1], aVelArray[velOffset + 2]); + const dt = localTime - aOffsetArray[i * 4 + 2];// 相对delay的时间 + const duration = aOffsetArray[i * 4 + 3]; + + if (uAcceleration && uGravityModifierValue) { + const d = this.gravityModifier.getIntegrateValue(0, dt, duration); + // const acc = this.tempVector3.set(uAcceleration.x * d, uAcceleration.y * d, uAcceleration.z * d); + const accX = uAcceleration.x * d; + const accY = uAcceleration.y * d; + const accZ = uAcceleration.z * d; + + // speedIntegrate = speedOverLifetime.getIntegrateValue(0, time, duration); + if (this.speedOverLifetime) { + // dt / dur 归一化 + const speed = this.speedOverLifetime.getValue(dt / duration); + + velocityX = velocityX * speed + accX; + velocityY = velocityY * speed + accY; + velocityZ = velocityZ * speed + accZ; + // velocity.multiply(speed).add(acc); + } else { + velocityX = velocityX + accX; + velocityY = velocityY + accY; + velocityZ = velocityZ + accZ; + // velocity.add(acc); + } + } + + const aTranslationOffset = i * 3; + + if (aOffsetArray[i * 4 + 2] < localTime) { + // const translation = velocity.multiply(deltaTime / 1000); + const aTranslationX = velocityX * (deltaTime / 1000); + const aTranslationY = velocityY * (deltaTime / 1000); + const aTranslationZ = velocityZ * (deltaTime / 1000); + + aTranslationArray[aTranslationOffset] += aTranslationX; + aTranslationArray[aTranslationOffset + 1] += aTranslationY; + aTranslationArray[aTranslationOffset + 2] += aTranslationZ; + + aTranslationArray[aTranslationOffset + 3] += aTranslationX; + aTranslationArray[aTranslationOffset + 4] += aTranslationY; + aTranslationArray[aTranslationOffset + 5] += aTranslationZ; + + aTranslationArray[aTranslationOffset + 6] += aTranslationX; + aTranslationArray[aTranslationOffset + 7] += aTranslationY; + aTranslationArray[aTranslationOffset + 8] += aTranslationZ; + + aTranslationArray[aTranslationOffset + 9] += aTranslationX; + aTranslationArray[aTranslationOffset + 10] += aTranslationY; + aTranslationArray[aTranslationOffset + 11] += aTranslationZ; + } + } + this.geometry.setAttributeData('aTranslation', aTranslationArray); + } + + private applyRotation (vertexCount: number, deltaTime: number) { + let aRotationArray = this.geometry.getAttributeData('aRotation0') as Float32Array; + const aOffsetArray = this.geometry.getAttributeData('aOffset') as Float32Array; + const aRotArray = this.geometry.getAttributeData('aRot') as Float32Array; // vector3 + const aSeedArray = this.geometry.getAttributeData('aSeed') as Float32Array; // float + const localTime = this.time; + const aRotationMatrix = this.cachedRotationMatrix; + + if (aRotationArray.length < vertexCount * 9) { + aRotationArray = this.expandArray(aRotationArray, vertexCount * 9); + } + + for (let i = 0; i < vertexCount; i += 4) { + const time = localTime - aOffsetArray[i * 4 + 2]; + const duration = aOffsetArray[i * 4 + 3]; + const life = clamp(time / duration, 0.0, 1.0); + const aRotOffset = i * 8; + const aRot = this.cachedRotationVector3.set(aRotArray[aRotOffset], aRotArray[aRotOffset + 1], aRotArray[aRotOffset + 2]); + const aSeed = aSeedArray[i * 8 + 3]; + + const rotation = aRot; + + if (!this.rotationOverLifetime) { + aRotationMatrix.setZero(); + } else if (this.rotationOverLifetime.asRotation) { + // Adjust rotation based on the specified lifetime components + if (this.rotationOverLifetime.x) { + if (this.rotationOverLifetime.x instanceof RandomValue) { + rotation.x += this.rotationOverLifetime.x.getValue(life, aSeed); + } else { + rotation.x += this.rotationOverLifetime.x.getValue(life); + } + } + if (this.rotationOverLifetime.y) { + if (this.rotationOverLifetime.y instanceof RandomValue) { + rotation.y += this.rotationOverLifetime.y.getValue(life, aSeed); + } else { + rotation.y += this.rotationOverLifetime.y.getValue(life); + } + } + if (this.rotationOverLifetime.z) { + if (this.rotationOverLifetime.z instanceof RandomValue) { + rotation.z += this.rotationOverLifetime.z.getValue(life, aSeed); + } else { + rotation.z += this.rotationOverLifetime.z.getValue(life); + } + } + } else { + // Adjust rotation based on the specified lifetime components + if (this.rotationOverLifetime.x) { + if (this.rotationOverLifetime.x instanceof RandomValue) { + rotation.x += this.rotationOverLifetime.x.getIntegrateValue(0.0, life, aSeed) * duration; + } else { + rotation.x += this.rotationOverLifetime.x.getIntegrateValue(0.0, life, duration) * duration; + } + } + if (this.rotationOverLifetime.y) { + if (this.rotationOverLifetime.y instanceof RandomValue) { + rotation.y += this.rotationOverLifetime.y.getIntegrateValue(0.0, life, aSeed) * duration; + } else { + rotation.y += this.rotationOverLifetime.y.getIntegrateValue(0.0, life, duration) * duration; + } + } + if (this.rotationOverLifetime.z) { + if (this.rotationOverLifetime.z instanceof RandomValue) { + rotation.z += this.rotationOverLifetime.z.getIntegrateValue(0.0, life, aSeed) * duration; + } else { + rotation.z += this.rotationOverLifetime.z.getIntegrateValue(0.0, life, duration) * duration; + } + } + } + + // If the rotation vector is zero, return the identity matrix + if (rotation.dot(rotation) === 0.0) { + aRotationMatrix.identity(); + } + + const d2r = Math.PI / 180; + const rotationXD2r = rotation.x * d2r; + const rotationYD2r = rotation.y * d2r; + const rotationZD2r = rotation.z * d2r; + + const sinRX = Math.sin(rotationXD2r); + const sinRY = Math.sin(rotationYD2r); + const sinRZ = Math.sin(rotationZD2r); + + const cosRX = Math.cos(rotationXD2r); + const cosRY = Math.cos(rotationYD2r); + const cosRZ = Math.cos(rotationZD2r); + + // rotZ * rotY * rotX + aRotationMatrix.set(cosRZ, -sinRZ, 0., sinRZ, cosRZ, 0., 0., 0., 1.); //rotZ + aRotationMatrix.multiply(this.tempMatrix3.set(cosRY, 0., sinRY, 0., 1., 0., -sinRY, 0, cosRY)); //rotY + aRotationMatrix.multiply(this.tempMatrix3.set(1., 0., 0., 0, cosRX, -sinRX, 0., sinRX, cosRX)); //rotX + + const aRotationOffset = i * 9; + const matrixArray = aRotationMatrix.elements; + + aRotationArray.set(matrixArray, aRotationOffset); + if (i + 4 <= vertexCount) { + aRotationArray.set(matrixArray, aRotationOffset + 9); + aRotationArray.set(matrixArray, aRotationOffset + 18); + aRotationArray.set(matrixArray, aRotationOffset + 27); + } + } + + this.geometry.setAttributeData('aRotation0', aRotationArray); + } + + private applyLinearMove (vertexCount: number, deltaTime: number) { + let aLinearMoveArray = this.geometry.getAttributeData('aLinearMove') as Float32Array; + const aOffsetArray = this.geometry.getAttributeData('aOffset') as Float32Array; + const aSeedArray = this.geometry.getAttributeData('aSeed') as Float32Array; // float + const localTime = this.time; + + if (aLinearMoveArray.length < vertexCount * 3) { + aLinearMoveArray = this.expandArray(aLinearMoveArray, vertexCount * 3); + } + + const linearMove = this.cachedLinearMove; + + if (this.linearVelOverLifetime && this.linearVelOverLifetime.enabled) { + for (let i = 0; i < vertexCount; i += 4) { + const time = localTime - aOffsetArray[i * 4 + 2]; + const duration = aOffsetArray[i * 4 + 3]; + // const life = math.clamp(time / duration, 0.0, 1.0); + const lifetime = time / duration; + const aSeed = aSeedArray[i * 8 + 3]; + + linearMove.setZero(); + + if (this.linearVelOverLifetime.asMovement) { + if (this.linearVelOverLifetime.x) { + if (this.linearVelOverLifetime.x instanceof RandomValue) { + linearMove.x = this.linearVelOverLifetime.x.getValue(lifetime, aSeed); + } else { + linearMove.x = this.linearVelOverLifetime.x.getValue(lifetime); + } + } + if (this.linearVelOverLifetime.y) { + if (this.linearVelOverLifetime.y instanceof RandomValue) { + linearMove.y = this.linearVelOverLifetime.y.getValue(lifetime, aSeed); + } else { + linearMove.y = this.linearVelOverLifetime.y.getValue(lifetime); + } + } + if (this.linearVelOverLifetime.z) { + if (this.linearVelOverLifetime.z instanceof RandomValue) { + linearMove.z = this.linearVelOverLifetime.z.getValue(lifetime, aSeed); + } else { + linearMove.z = this.linearVelOverLifetime.z.getValue(lifetime); + } + } + } else { + // Adjust rotation based on the specified lifetime components + if (this.linearVelOverLifetime.x) { + if (this.linearVelOverLifetime.x instanceof RandomValue) { + linearMove.x = this.linearVelOverLifetime.x.getIntegrateValue(0.0, time, aSeed); + } else { + linearMove.x = this.linearVelOverLifetime.x.getIntegrateValue(0.0, time, duration); + } + } + if (this.linearVelOverLifetime.y) { + if (this.linearVelOverLifetime.y instanceof RandomValue) { + linearMove.y = this.linearVelOverLifetime.y.getIntegrateValue(0.0, time, aSeed); + } else { + linearMove.y = this.linearVelOverLifetime.y.getIntegrateValue(0.0, time, duration); + } + } + if (this.linearVelOverLifetime.z) { + if (this.linearVelOverLifetime.z instanceof RandomValue) { + linearMove.z = this.linearVelOverLifetime.z.getIntegrateValue(0.0, time, aSeed); + } else { + linearMove.z = this.linearVelOverLifetime.z.getIntegrateValue(0.0, time, duration); + } + } + } + const aLinearMoveOffset = i * 3; + + aLinearMoveArray[aLinearMoveOffset] = linearMove.x; + aLinearMoveArray[aLinearMoveOffset + 1] = linearMove.y; + aLinearMoveArray[aLinearMoveOffset + 2] = linearMove.z; + + aLinearMoveArray[aLinearMoveOffset + 3] = linearMove.x; + aLinearMoveArray[aLinearMoveOffset + 4] = linearMove.y; + aLinearMoveArray[aLinearMoveOffset + 5] = linearMove.z; + + aLinearMoveArray[aLinearMoveOffset + 6] = linearMove.x; + aLinearMoveArray[aLinearMoveOffset + 7] = linearMove.y; + aLinearMoveArray[aLinearMoveOffset + 8] = linearMove.z; + + aLinearMoveArray[aLinearMoveOffset + 9] = linearMove.x; + aLinearMoveArray[aLinearMoveOffset + 10] = linearMove.y; + aLinearMoveArray[aLinearMoveOffset + 11] = linearMove.z; + } + } + this.geometry.setAttributeData('aLinearMove', aLinearMoveArray); + } + + private expandArray (array: Float32Array, newSize: number): Float32Array { + const newArr = new Float32Array(newSize); + + newArr.set(array); + + return newArr; + } } const gl2UniformSlots = [10, 32, 64, 160]; @@ -612,6 +919,11 @@ function generateGeometryProps ( aColor: { size: 4, offset: 4 * bpe, stride: 8 * bpe, dataSource: 'aRot' }, // aOffset: { size: 4, stride: 4 * bpe, data: new Float32Array(0) }, + aTranslation: { size: 3, data: new Float32Array(0) }, + aLinearMove: { size: 3, data: new Float32Array(0) }, + aRotation0: { size: 3, offset: 0, stride: 9 * bpe, data: new Float32Array(0) }, + aRotation1: { size: 3, offset: 3 * bpe, stride: 9 * bpe, dataSource: 'aRotation0' }, + aRotation2: { size: 3, offset: 6 * bpe, stride: 9 * bpe, dataSource: 'aRotation0' }, }; if (useSprite) { diff --git a/packages/effects-core/src/plugins/particle/particle-system-renderer.ts b/packages/effects-core/src/plugins/particle/particle-system-renderer.ts index b2f50788e..aed38342a 100644 --- a/packages/effects-core/src/plugins/particle/particle-system-renderer.ts +++ b/packages/effects-core/src/plugins/particle/particle-system-renderer.ts @@ -47,18 +47,19 @@ export class ParticleSystemRenderer extends RendererComponent { this.meshes = meshes; } - override start (): void { + override onStart (): void { this._priority = this.item.renderOrder; this.particleMesh.gravityModifier.scaleXCoord(this.item.duration); for (const mesh of this.meshes) { - mesh.start(); + mesh.onStart(); } } - override update (dt: number): void { + override onUpdate (dt: number): void { const time = this.particleMesh.time; + const uParams = this.particleMesh.mesh.material.getVector4('uParams') ?? new Vector4(); - this.particleMesh.mesh.material.setVector4('uParams', new Vector4(time, this.item.duration, 0, 0)); + this.particleMesh.mesh.material.setVector4('uParams', uParams.set(time, this.item.duration, 0, 0)); } override render (renderer: Renderer): void { @@ -74,6 +75,7 @@ export class ParticleSystemRenderer extends RendererComponent { updateTime (now: number, delta: number) { this.particleMesh.time = now; + this.particleMesh.onUpdate(delta); if (this.trailMesh) { this.trailMesh.time = now; this.trailMesh.onUpdate(delta); diff --git a/packages/effects-core/src/plugins/particle/particle-system.ts b/packages/effects-core/src/plugins/particle/particle-system.ts index c53eceb14..226403f39 100644 --- a/packages/effects-core/src/plugins/particle/particle-system.ts +++ b/packages/effects-core/src/plugins/particle/particle-system.ts @@ -8,11 +8,11 @@ import type { Engine } from '../../engine'; import type { ValueGetter } from '../../math'; import { calculateTranslation, createValueGetter, ensureVec3 } from '../../math'; import type { Mesh } from '../../render'; -import type { ShapeGenerator, ShapeGeneratorOptions } from '../../shape'; +import type { ShapeGenerator, ShapeGeneratorOptions, ShapeParticle } from '../../shape'; import { createShape } from '../../shape'; import { Texture } from '../../texture'; import { Transform } from '../../transform'; -import { DestroyOptions, type color } from '../../utils'; +import { DestroyOptions } from '../../utils'; import type { BoundingBoxSphere, HitTestCustomParams } from '../interact/click-handler'; import { HitTestType } from '../interact/click-handler'; import { Burst } from './burst'; @@ -32,7 +32,7 @@ type ParticleOptions = { startSpeed: ValueGetter, startLifetime: ValueGetter, startDelay: ValueGetter, - startColor: ValueGetter, + startColor: ValueGetter, start3DRotation?: boolean, startRotationX?: ValueGetter, startRotationY?: ValueGetter, @@ -316,7 +316,7 @@ export class ParticleSystem extends Component { return this.renderer.getTextures(); } - start () { + startEmit () { if (!this.started || this.ended) { this.reset(); this.started = true; @@ -339,9 +339,10 @@ export class ParticleSystem extends Component { this.emission.bursts.forEach(b => b.reset()); this.frozen = false; this.ended = false; + this.destroyed = false; } - onUpdate (delta: number) { + update (delta: number) { if (this.started && !this.frozen) { const now = this.lastUpdate + delta / 1000; const options = this.options; @@ -657,7 +658,7 @@ export class ParticleSystem extends Component { return ret; } - initPoint (data: Record): Point { + initPoint (data: ShapeParticle): Point { const options = this.options; const lifetime = this.lifetime; const shape = this.shape; diff --git a/packages/effects-core/src/plugins/particle/particle-vfx-item.ts b/packages/effects-core/src/plugins/particle/particle-vfx-item.ts index 02f3cdb74..e2a75151f 100644 --- a/packages/effects-core/src/plugins/particle/particle-vfx-item.ts +++ b/packages/effects-core/src/plugins/particle/particle-vfx-item.ts @@ -20,7 +20,7 @@ export class ParticleBehaviourPlayable extends Playable { if (this.particleSystem) { this.particleSystem.name = boundObject.name; - this.particleSystem.start(); + this.particleSystem.startEmit(); this.particleSystem.initEmitterTransform(); } } @@ -44,7 +44,7 @@ export class ParticleBehaviourPlayable extends Playable { if (Math.abs(this.time - this.lastTime) < 0.001) { deltaTime = 0; } - particleSystem.onUpdate(deltaTime); + particleSystem.update(deltaTime); } this.lastTime = this.time; } diff --git a/packages/effects-core/src/plugins/plugin.ts b/packages/effects-core/src/plugins/plugin.ts index 855f32f63..32b249317 100644 --- a/packages/effects-core/src/plugins/plugin.ts +++ b/packages/effects-core/src/plugins/plugin.ts @@ -7,7 +7,7 @@ import type { Composition } from '../composition'; export interface Plugin { /** * plugin 的数组内排序,按照升序排列 - * 默认为100 + * @default 100 */ order: number, name: string, @@ -103,7 +103,7 @@ export abstract class AbstractPlugin implements Plugin { name = ''; /*** - * player.loadScene 函数调用的时候会触发此函数, + * loadScene 函数调用的时候会触发此函数, * 此阶段可以对资源 JSON 进行处理,替换调 JSON 中的数据,或者直接终止加载流程 * 一旦被 reject,加载过程将失败 * @param json 动画资源 @@ -112,7 +112,16 @@ export abstract class AbstractPlugin implements Plugin { static processRawJSON: (json: spec.JSONScene, options: SceneLoadOptions) => Promise; /** - * player.loadScene 函数调用的时候会触发此函数, + * loadScene 函数调用的时候会触发此函数, + * 此阶段可以加载插件所需类型资源,并返回原始资源和加载后的资源。 + * @param json + * @param options + * @returns + */ + static processAssets: (json: spec.JSONScene, options?: SceneLoadOptions) => Promise<{ assets: spec.AssetBase[], loadedAssets: unknown[] }>; + + /** + * loadScene 函数调用的时候会触发此函数, * 此阶段时,json 中的图片和二进制已经被加载完成,可以对加载好的资源做进一步处理, * 如果 promise 被 reject, loadScene 函数同样会被 reject,表示场景加载失败。 * 请记住,整个 load 阶段都不要创建 GL 相关的对象,只创建 JS 对象 diff --git a/packages/effects-core/src/plugins/shape/build-adaptive-bezier.ts b/packages/effects-core/src/plugins/shape/build-adaptive-bezier.ts new file mode 100644 index 000000000..9022fcb88 --- /dev/null +++ b/packages/effects-core/src/plugins/shape/build-adaptive-bezier.ts @@ -0,0 +1,216 @@ +// thanks to https://github.com/mattdesl/adaptive-bezier-curve +// for the original code! + +const RECURSION_LIMIT = 8; +const FLT_EPSILON = 1.19209290e-7; +const PATH_DISTANCE_EPSILON = 1.0; + +const curveAngleToleranceEpsilon = 0.01; +const mAngleTolerance = 0; +const mCuspLimit = 0; + +const defaultBezierSmoothness = 0.5; + +export function buildAdaptiveBezier ( + points: number[], + sX: number, sY: number, + cp1x: number, cp1y: number, + cp2x: number, cp2y: number, + eX: number, eY: number, + smoothness?: number, +) { + // TODO expose as a parameter + const scale = 5; + const smoothing = Math.min( + 0.99, // a value of 1.0 actually inverts smoothing, so we cap it at 0.99 + Math.max(0, smoothness ?? defaultBezierSmoothness) + ); + let distanceTolerance = (PATH_DISTANCE_EPSILON - smoothing) / scale; + + distanceTolerance *= distanceTolerance; + begin(sX, sY, cp1x, cp1y, cp2x, cp2y, eX, eY, points, distanceTolerance); + + return points; +} + +//// Based on: +//// https://github.com/pelson/antigrain/blob/master/agg-2.4/src/agg_curves.cpp + +function begin ( + sX: number, sY: number, + cp1x: number, cp1y: number, + cp2x: number, cp2y: number, + eX: number, eY: number, + points: number[], + distanceTolerance: number, +) { + // dont need to actually ad this! + // points.push(sX, sY); + recursive(sX, sY, cp1x, cp1y, cp2x, cp2y, eX, eY, points, distanceTolerance, 0); + points.push(eX, eY); +} + +// eslint-disable-next-line max-params +function recursive ( + x1: number, y1: number, + x2: number, y2: number, + x3: number, y3: number, + x4: number, y4: number, + points: number[], + distanceTolerance: number, + level: number, +) { + if (level > RECURSION_LIMIT) { return; } + + const pi = Math.PI; + + // Calculate all the mid-points of the line segments + // ---------------------- + const x12 = (x1 + x2) / 2; + const y12 = (y1 + y2) / 2; + const x23 = (x2 + x3) / 2; + const y23 = (y2 + y3) / 2; + const x34 = (x3 + x4) / 2; + const y34 = (y3 + y4) / 2; + const x123 = (x12 + x23) / 2; + const y123 = (y12 + y23) / 2; + const x234 = (x23 + x34) / 2; + const y234 = (y23 + y34) / 2; + const x1234 = (x123 + x234) / 2; + const y1234 = (y123 + y234) / 2; + + if (level > 0) { // Enforce subdivision first time + // Try to approximate the full cubic curve by a single straight line + // ------------------ + let dx = x4 - x1; + let dy = y4 - y1; + + const d2 = Math.abs(((x2 - x4) * dy) - ((y2 - y4) * dx)); + const d3 = Math.abs(((x3 - x4) * dy) - ((y3 - y4) * dx)); + + let da1; let da2; + + if (d2 > FLT_EPSILON && d3 > FLT_EPSILON) { + // Regular care + // ----------------- + if ((d2 + d3) * (d2 + d3) <= distanceTolerance * ((dx * dx) + (dy * dy))) { + // If the curvature doesn't exceed the distanceTolerance value + // we tend to finish subdivisions. + // ---------------------- + if (mAngleTolerance < curveAngleToleranceEpsilon) { + points.push(x1234, y1234); + + return; + } + + // Angle & Cusp Condition + // ---------------------- + const a23 = Math.atan2(y3 - y2, x3 - x2); + + da1 = Math.abs(a23 - Math.atan2(y2 - y1, x2 - x1)); + da2 = Math.abs(Math.atan2(y4 - y3, x4 - x3) - a23); + + if (da1 >= pi) { da1 = (2 * pi) - da1; } + if (da2 >= pi) { da2 = (2 * pi) - da2; } + + if (da1 + da2 < mAngleTolerance) { + // Finally we can stop the recursion + // ---------------------- + points.push(x1234, y1234); + + return; + } + + if (mCuspLimit !== 0.0) { + if (da1 > mCuspLimit) { + points.push(x2, y2); + + return; + } + + if (da2 > mCuspLimit) { + points.push(x3, y3); + + return; + } + } + } + } else if (d2 > FLT_EPSILON) { + // p1,p3,p4 are collinear, p2 is considerable + // ---------------------- + if (d2 * d2 <= distanceTolerance * ((dx * dx) + (dy * dy))) { + if (mAngleTolerance < curveAngleToleranceEpsilon) { + points.push(x1234, y1234); + + return; + } + + // Angle Condition + // ---------------------- + da1 = Math.abs(Math.atan2(y3 - y2, x3 - x2) - Math.atan2(y2 - y1, x2 - x1)); + if (da1 >= pi) { da1 = (2 * pi) - da1; } + + if (da1 < mAngleTolerance) { + points.push(x2, y2); + points.push(x3, y3); + + return; + } + + if (mCuspLimit !== 0.0) { + if (da1 > mCuspLimit) { + points.push(x2, y2); + + return; + } + } + } + } else if (d3 > FLT_EPSILON) { + // p1,p2,p4 are collinear, p3 is considerable + // ---------------------- + if (d3 * d3 <= distanceTolerance * ((dx * dx) + (dy * dy))) { + if (mAngleTolerance < curveAngleToleranceEpsilon) { + points.push(x1234, y1234); + + return; + } + + // Angle Condition + // ---------------------- + da1 = Math.abs(Math.atan2(y4 - y3, x4 - x3) - Math.atan2(y3 - y2, x3 - x2)); + if (da1 >= pi) { da1 = (2 * pi) - da1; } + + if (da1 < mAngleTolerance) { + points.push(x2, y2); + points.push(x3, y3); + + return; + } + + if (mCuspLimit !== 0.0) { + if (da1 > mCuspLimit) { + points.push(x3, y3); + + return; + } + } + } + } else { + // Collinear case + // ----------------- + dx = x1234 - ((x1 + x4) / 2); + dy = y1234 - ((y1 + y4) / 2); + if ((dx * dx) + (dy * dy) <= distanceTolerance) { + points.push(x1234, y1234); + + return; + } + } + } + + // Continue subdivision + // ---------------------- + recursive(x1, y1, x12, y12, x123, y123, x1234, y1234, points, distanceTolerance, level + 1); + recursive(x1234, y1234, x234, y234, x34, y34, x4, y4, points, distanceTolerance, level + 1); +} + diff --git a/packages/effects-core/src/plugins/shape/ellipse.ts b/packages/effects-core/src/plugins/shape/ellipse.ts new file mode 100644 index 000000000..ff9bb7d8c --- /dev/null +++ b/packages/effects-core/src/plugins/shape/ellipse.ts @@ -0,0 +1,297 @@ +import { ShapePrimitive } from './shape-primitive'; + +/** + * The Ellipse object is used to help draw graphics and can also be used to specify a hit area for containers. + */ +export class Ellipse extends ShapePrimitive { + /** + * The X coordinate of the center of this ellipse + * @default 0 + */ + x: number; + + /** + * The Y coordinate of the center of this ellipse + * @default 0 + */ + y: number; + + /** + * The half width of this ellipse + * @default 0 + */ + halfWidth: number; + + /** + * The half height of this ellipse + * @default 0 + */ + halfHeight: number; + + /** + * The type of the object, mainly used to avoid `instanceof` checks + * @default 'ellipse' + */ + readonly type = 'ellipse'; + + /** + * @param x - The X coordinate of the center of this ellipse + * @param y - The Y coordinate of the center of this ellipse + * @param halfWidth - The half width of this ellipse + * @param halfHeight - The half height of this ellipse + */ + constructor (x = 0, y = 0, halfWidth = 0, halfHeight = 0) { + super(); + this.x = x; + this.y = y; + this.halfWidth = halfWidth; + this.halfHeight = halfHeight; + } + + /** + * Creates a clone of this Ellipse instance + * @returns {Ellipse} A copy of the ellipse + */ + clone (): Ellipse { + return new Ellipse(this.x, this.y, this.halfWidth, this.halfHeight); + } + + /** + * Checks whether the x and y coordinates given are contained within this ellipse + * @param x - The X coordinate of the point to test + * @param y - The Y coordinate of the point to test + * @returns Whether the x/y coords are within this ellipse + */ + contains (x: number, y: number): boolean { + if (this.halfWidth <= 0 || this.halfHeight <= 0) { + return false; + } + + // normalize the coords to an ellipse with center 0,0 + let normx = ((x - this.x) / this.halfWidth); + let normy = ((y - this.y) / this.halfHeight); + + normx *= normx; + normy *= normy; + + return (normx + normy <= 1); + } + + /** + * Checks whether the x and y coordinates given are contained within this ellipse including stroke + * @param x - The X coordinate of the point to test + * @param y - The Y coordinate of the point to test + * @param width + * @returns Whether the x/y coords are within this ellipse + */ + strokeContains (x: number, y: number, width: number): boolean { + const { halfWidth, halfHeight } = this; + + if (halfWidth <= 0 || halfHeight <= 0) { + return false; + } + + const halfStrokeWidth = width / 2; + const innerA = halfWidth - halfStrokeWidth; + const innerB = halfHeight - halfStrokeWidth; + const outerA = halfWidth + halfStrokeWidth; + const outerB = halfHeight + halfStrokeWidth; + + const normalizedX = x - this.x; + const normalizedY = y - this.y; + + const innerEllipse = ((normalizedX * normalizedX) / (innerA * innerA)) + + ((normalizedY * normalizedY) / (innerB * innerB)); + const outerEllipse = ((normalizedX * normalizedX) / (outerA * outerA)) + + ((normalizedY * normalizedY) / (outerB * outerB)); + + return innerEllipse > 1 && outerEllipse <= 1; + } + + /** + * Returns the framing rectangle of the ellipse as a Rectangle object + * @param out + * @returns The framing rectangle + */ + // getBounds (out?: Rectangle): Rectangle { + // out = out || new Rectangle(); + + // out.x = this.x - this.halfWidth; + // out.y = this.y - this.halfHeight; + // out.width = this.halfWidth * 2; + // out.height = this.halfHeight * 2; + + // return out; + // } + + /** + * Copies another ellipse to this one. + * @param ellipse - The ellipse to copy from. + * @returns Returns itself. + */ + copyFrom (ellipse: Ellipse): this { + this.x = ellipse.x; + this.y = ellipse.y; + this.halfWidth = ellipse.halfWidth; + this.halfHeight = ellipse.halfHeight; + + return this; + } + + /** + * Copies this ellipse to another one. + * @param ellipse - The ellipse to copy to. + * @returns Returns given parameter. + */ + copyTo (ellipse: Ellipse): Ellipse { + ellipse.copyFrom(this); + + return ellipse; + } + + getX (): number { + return this.x; + } + + getY (): number { + return this.y; + } + + build (points: number[]) { + const x = this.x; + const y = this.y; + const rx = this.halfWidth; + const ry = this.halfHeight; + const dx = 0; + const dy = 0; + + if (!(rx >= 0 && ry >= 0 && dx >= 0 && dy >= 0)) { + return points; + } + + // Choose a number of segments such that the maximum absolute deviation from the circle is approximately 0.029 + const sampleDensity = 5; + const n = Math.ceil(sampleDensity * Math.sqrt(rx + ry)); + const m = (n * 8) + (dx ? 4 : 0) + (dy ? 4 : 0); + + if (m === 0) { + return points; + } + + if (n === 0) { + points[0] = points[6] = x + dx; + points[1] = points[3] = y + dy; + points[2] = points[4] = x - dx; + points[5] = points[7] = y - dy; + + return points; + } + + let j1 = 0; + let j2 = (n * 4) + (dx ? 2 : 0) + 2; + let j3 = j2; + let j4 = m; + + let x0 = dx + rx; + let y0 = dy; + let x1 = x + x0; + let x2 = x - x0; + let y1 = y + y0; + + points[j1++] = x1; + points[j1++] = y1; + points[--j2] = y1; + points[--j2] = x2; + + if (dy) { + const y2 = y - y0; + + points[j3++] = x2; + points[j3++] = y2; + points[--j4] = y2; + points[--j4] = x1; + } + + for (let i = 1; i < n; i++) { + const a = Math.PI / 2 * (i / n); + const x0 = dx + (Math.cos(a) * rx); + const y0 = dy + (Math.sin(a) * ry); + const x1 = x + x0; + const x2 = x - x0; + const y1 = y + y0; + const y2 = y - y0; + + points[j1++] = x1; + points[j1++] = y1; + points[--j2] = y1; + points[--j2] = x2; + points[j3++] = x2; + points[j3++] = y2; + points[--j4] = y2; + points[--j4] = x1; + } + + x0 = dx; + y0 = dy + ry; + x1 = x + x0; + x2 = x - x0; + y1 = y + y0; + const y2 = y - y0; + + points[j1++] = x1; + points[j1++] = y1; + points[--j4] = y2; + points[--j4] = x1; + + if (dx) { + points[j1++] = x2; + points[j1++] = y1; + points[--j4] = y2; + points[--j4] = x2; + } + + return points; + } + + triangulate (points: number[], vertices: number[], verticesOffset: number, indices: number[], indicesOffset: number) { + if (points.length === 0) { + return; + } + + // Compute center (average of all points) + let centerX = 0; let + centerY = 0; + + for (let i = 0; i < points.length; i += 2) { + centerX += points[i]; + centerY += points[i + 1]; + } + centerX /= (points.length / 2); + centerY /= (points.length / 2); + + // Set center vertex + let count = verticesOffset; + + vertices[count * 2] = centerX; + vertices[(count * 2) + 1] = centerY; + const centerIndex = count++; + + // Set edge vertices and indices + for (let i = 0; i < points.length; i += 2) { + vertices[count * 2] = points[i]; + vertices[(count * 2) + 1] = points[i + 1]; + + if (i > 0) { // Skip first point for indices + indices[indicesOffset++] = count; + indices[indicesOffset++] = centerIndex; + indices[indicesOffset++] = count - 1; + } + count++; + } + + // Connect last point to the first edge point + indices[indicesOffset++] = centerIndex + 1; + indices[indicesOffset++] = centerIndex; + indices[indicesOffset++] = count - 1; + } +} diff --git a/packages/effects-core/src/plugins/shape/graphics-path.ts b/packages/effects-core/src/plugins/shape/graphics-path.ts new file mode 100644 index 000000000..bc915125c --- /dev/null +++ b/packages/effects-core/src/plugins/shape/graphics-path.ts @@ -0,0 +1,147 @@ +/** + * Based on: + * https://github.com/pixijs/pixijs/blob/dev/src/scene/graphics/shared/path/GraphicsPath.ts + */ + +import type { Matrix4 } from '@galacean/effects-math/es/core/matrix4'; +import { ShapePath } from './shape-path'; +import type { StarType } from './poly-star'; + +export class GraphicsPath { + instructions: PathInstruction[] = []; + + private dirty = false; + private _shapePath: ShapePath; + + /** + * Provides access to the internal shape path, ensuring it is up-to-date with the current instructions. + * @returns The `ShapePath` instance associated with this `GraphicsPath`. + */ + get shapePath (): ShapePath { + if (!this._shapePath) { + this._shapePath = new ShapePath(this); + } + + if (this.dirty) { + this.dirty = false; + this._shapePath.buildPath(); + } + + return this._shapePath; + } + + /** + * Adds a cubic Bezier curve to the path. + * It requires three points: the first two are control points and the third one is the end point. + * The starting point is the last point in the current path. + * @param cp1x - The x-coordinate of the first control point. + * @param cp1y - The y-coordinate of the first control point. + * @param cp2x - The x-coordinate of the second control point. + * @param cp2y - The y-coordinate of the second control point. + * @param x - The x-coordinate of the end point. + * @param y - The y-coordinate of the end point. + * @param smoothness - Optional parameter to adjust the smoothness of the curve. + * @returns The instance of the current object for chaining. + */ + bezierCurveTo ( + cp1x: number, cp1y: number, cp2x: number, cp2y: number, + x: number, y: number, + smoothness?: number, + ): GraphicsPath { + this.instructions.push({ action: 'bezierCurveTo', data: [cp1x, cp1y, cp2x, cp2y, x, y, smoothness] }); + + this.dirty = true; + + return this; + } + + /** + * Sets the starting point for a new sub-path. Any subsequent drawing commands are considered part of this path. + * @param x - The x-coordinate for the starting point. + * @param y - The y-coordinate for the starting point. + * @returns The instance of the current object for chaining. + */ + moveTo (x: number, y: number): GraphicsPath { + this.instructions.push({ action: 'moveTo', data: [x, y] }); + + this.dirty = true; + + return this; + } + + /** + * Draws an ellipse at the specified location and with the given x and y radii. + * An optional transformation can be applied, allowing for rotation, scaling, and translation. + * @param x - The x-coordinate of the center of the ellipse. + * @param y - The y-coordinate of the center of the ellipse. + * @param radiusX - The horizontal radius of the ellipse. + * @param radiusY - The vertical radius of the ellipse. + * @param transform - An optional `Matrix` object to apply a transformation to the ellipse. This can include rotations. + * @returns The instance of the current object for chaining. + */ + ellipse (x: number, y: number, radiusX: number, radiusY: number, transform?: Matrix4) { + this.instructions.push({ action: 'ellipse', data: [x, y, radiusX, radiusY, transform] }); + + this.dirty = true; + + return this; + } + + /** + * Draws a rectangle shape. This method adds a new rectangle path to the current drawing. + * @param x - The x-coordinate of the upper-left corner of the rectangle. + * @param y - The y-coordinate of the upper-left corner of the rectangle. + * @param w - The width of the rectangle. + * @param h - The height of the rectangle. + * @param transform - An optional `Matrix` object to apply a transformation to the rectangle. + * @returns The instance of the current object for chaining. + */ + rect (x: number, y: number, w: number, h: number, transform?: Matrix4): this { + this.instructions.push({ action: 'rect', data: [x, y, w, h, transform] }); + + this.dirty = true; + + return this; + } + + polyStar (pointCount: number, outerRadius: number, innerRadius: number, outerRoundness: number, innerRoundness: number, starType: StarType, transform?: Matrix4) { + this.instructions.push({ action: 'polyStar', data: [pointCount, outerRadius, innerRadius, outerRoundness, innerRoundness, starType, transform] }); + + this.dirty = true; + + return this; + } + + clear (): GraphicsPath { + this.instructions.length = 0; + this.dirty = true; + + return this; + } +} + +export interface PathInstruction { + action: + | 'moveTo' + | 'lineTo' + | 'quadraticCurveTo' + | 'bezierCurveTo' + | 'arc' + | 'closePath' + | 'addPath' + | 'arcTo' + | 'ellipse' + | 'rect' + | 'roundRect' + | 'arcToSvg' + | 'poly' + | 'circle' + | 'regularPoly' + | 'roundPoly' + | 'roundShape' + | 'filletRect' + | 'chamferRect' + | 'polyStar' + , + data: any[], +} diff --git a/packages/effects-core/src/plugins/shape/point-data.ts b/packages/effects-core/src/plugins/shape/point-data.ts new file mode 100644 index 000000000..0d9ae7df7 --- /dev/null +++ b/packages/effects-core/src/plugins/shape/point-data.ts @@ -0,0 +1,7 @@ +export interface PointData { + /** X coord */ + x: number, + + /** Y coord */ + y: number, +} diff --git a/packages/effects-core/src/plugins/shape/point-like.ts b/packages/effects-core/src/plugins/shape/point-like.ts new file mode 100644 index 000000000..7bcd35791 --- /dev/null +++ b/packages/effects-core/src/plugins/shape/point-like.ts @@ -0,0 +1,33 @@ +import type { PointData } from './point-data'; + +/** + * Common interface for points. Both Point and ObservablePoint implement it + */ +export interface PointLike extends PointData { + /** + * Copies x and y from the given point + * @param p - The point to copy from + * @returns Returns itself. + */ + copyFrom: (p: PointData) => this, + /** + * Copies x and y into the given point + * @param p - The point to copy.fds + * @returns Given point with values updated + */ + copyTo: (p: T) => T, + /** + * Returns true if the given point is equal to this point + * @param p - The point to check + * @returns Whether the given point equal to this point + */ + equals: (p: PointData) => boolean, + /** + * Sets the point to a new x and y position. + * If y is omitted, both x and y will be set to x. + * @param {number} [x=0] - position of the point on the x axis + * @param {number} [y=x] - position of the point on the y axis + */ + set: (x?: number, y?: number) => void, +} + diff --git a/packages/effects-core/src/plugins/shape/point.ts b/packages/effects-core/src/plugins/shape/point.ts new file mode 100644 index 000000000..bc36718d4 --- /dev/null +++ b/packages/effects-core/src/plugins/shape/point.ts @@ -0,0 +1,93 @@ +import type { PointData } from './point-data'; +import type { PointLike } from './point-like'; + +/** + * The Point object represents a location in a two-dimensional coordinate system, where `x` represents + * the position on the horizontal axis and `y` represents the position on the vertical axis. + */ +export class Point implements PointLike { + /** + * Position of the point on the x axis + */ + x = 0; + /** + * Position of the point on the y axis + */ + y = 0; + + /** + * Creates a new `Point` + * @param {number} [x=0] - position of the point on the x axis + * @param {number} [y=0] - position of the point on the y axis + */ + constructor (x = 0, y = 0) { + this.x = x; + this.y = y; + } + + /** + * Creates a clone of this point + * @returns A clone of this point + */ + clone (): Point { + return new Point(this.x, this.y); + } + + /** + * Copies `x` and `y` from the given point into this point + * @param p - The point to copy from + * @returns The point instance itself + */ + copyFrom (p: PointData): this { + this.set(p.x, p.y); + + return this; + } + + /** + * Copies this point's x and y into the given point (`p`). + * @param p - The point to copy to. Can be any of type that is or extends `PointData` + * @returns The point (`p`) with values updated + */ + copyTo (p: T): T { + p.set(this.x, this.y); + + return p; + } + + /** + * Accepts another point (`p`) and returns `true` if the given point is equal to this point + * @param p - The point to check + * @returns Returns `true` if both `x` and `y` are equal + */ + equals (p: PointData): boolean { + return (p.x === this.x) && (p.y === this.y); + } + + /** + * Sets the point to a new `x` and `y` position. + * If `y` is omitted, both `x` and `y` will be set to `x`. + * @param {number} [x=0] - position of the point on the `x` axis + * @param {number} [y=x] - position of the point on the `y` axis + * @returns The point instance itself + */ + set (x = 0, y: number = x): this { + this.x = x; + this.y = y; + + return this; + } + + /** + * A static Point object with `x` and `y` values of `0`. Can be used to avoid creating new objects multiple times. + * @readonly + */ + static get shared (): Point { + tempPoint.x = 0; + tempPoint.y = 0; + + return tempPoint; + } +} + +const tempPoint = new Point(); diff --git a/packages/effects-core/src/plugins/shape/poly-star.ts b/packages/effects-core/src/plugins/shape/poly-star.ts new file mode 100644 index 000000000..5c699ca20 --- /dev/null +++ b/packages/effects-core/src/plugins/shape/poly-star.ts @@ -0,0 +1,195 @@ +import { buildAdaptiveBezier } from './build-adaptive-bezier'; +import { ShapePrimitive } from './shape-primitive'; +import { triangulate } from './triangulate'; + +export enum StarType { + Star, + Polygon, +} + +export class PolyStar extends ShapePrimitive { + /** + * bezier 顶点 + */ + private v: number[] = []; + /** + * bezier 缓入点 + */ + private in: number[] = []; + /** + * bezier 缓出点 + */ + private out: number[] = []; + + /** + * + * @param pointCount - 多边形顶点数量 + * @param outerRadius - 外半径大小 + * @param innerRadius - 内半径大小 + * @param outerRoundness - 外顶点圆滑度百分比 + * @param innerRoundness - 内顶点圆滑度百分比 + * @param starType - PolyStar 类型 + */ + constructor ( + public pointCount = 0, + public outerRadius = 0, + public innerRadius = 0, + public outerRoundness = 0, + public innerRoundness = 0, + public starType = StarType.Star, + ) { + super(); + } + + override clone (): ShapePrimitive { + const polyStar = new PolyStar( + this.pointCount, + this.outerRadius, + this.innerRadius, + this.outerRoundness, + this.innerRoundness, + this.starType + ); + + return polyStar; + } + + override copyFrom (source: PolyStar): void { + this.pointCount = source.pointCount; + this.outerRadius = source.outerRadius; + this.innerRadius = source.innerRadius; + this.outerRoundness = source.outerRoundness; + this.innerRoundness = source.innerRoundness; + this.starType = source.starType; + } + + override copyTo (destination: PolyStar): void { + destination.copyFrom(this); + } + + override build (points: number[]): void { + switch (this.starType) { + case StarType.Star: { + this.buildStarPath(); + + break; + } + case StarType.Polygon: { + this.buildPolygonPath(); + + break; + } + } + + const smoothness = 1; + + for (let i = 0; i < this.v.length - 2; i += 2) { + buildAdaptiveBezier( + points, + this.v[i], this.v[i + 1], + this.out[i], this.out[i + 1], this.in[i + 2], this.in[i + 3], this.v[i + 2], this.v[i + 3], + smoothness + ); + } + + // draw last curve + const lastIndex = this.v.length - 1; + + buildAdaptiveBezier( + points, + this.v[lastIndex - 1], this.v[lastIndex], + this.out[lastIndex - 1], this.out[lastIndex], this.in[0], this.in[1], this.v[0], this.v[1], + smoothness + ); + + } + + override triangulate (points: number[], vertices: number[], verticesOffset: number, indices: number[], indicesOffset: number): void { + const triangles = triangulate([points]); + + for (let i = 0; i < triangles.length; i++) { + vertices[verticesOffset + i] = triangles[i]; + } + + const vertexCount = triangles.length / 2; + + for (let i = 0; i < vertexCount; i++) { + indices[indicesOffset + i] = i; + } + } + + private buildStarPath () { + this.v = []; + this.in = []; + this.out = []; + + const numPts = Math.floor(this.pointCount) * 2; + const angle = (Math.PI * 2) / numPts; + let longFlag = true; + const longRad = this.outerRadius; + const shortRad = this.innerRadius; + const longRound = this.outerRoundness / 100; + const shortRound = this.innerRoundness / 100; + const longPerimSegment = (2 * Math.PI * longRad) / (numPts * 2); + const shortPerimSegment = (2 * Math.PI * shortRad) / (numPts * 2); + let i; + let rad; + let roundness; + let perimSegment; + let currentAng = -Math.PI / 2; + + const dir = 1; + + for (i = 0; i < numPts; i++) { + rad = longFlag ? longRad : shortRad; + roundness = longFlag ? longRound : shortRound; + perimSegment = longFlag ? longPerimSegment : shortPerimSegment; + const x = rad * Math.cos(currentAng); + const y = rad * Math.sin(currentAng); + const ox = x === 0 && y === 0 ? 0 : y / Math.sqrt(x * x + y * y); + const oy = x === 0 && y === 0 ? 0 : -x / Math.sqrt(x * x + y * y); + const offset = i * 2; + + this.v[offset] = x; + this.v[offset + 1] = y; + this.in[offset] = x + ox * perimSegment * roundness * dir; + this.in[offset + 1] = y + oy * perimSegment * roundness * dir; + this.out[offset] = x - ox * perimSegment * roundness * dir; + this.out[offset + 1] = y - oy * perimSegment * roundness * dir; + longFlag = !longFlag; + currentAng += angle * dir; + } + } + + private buildPolygonPath () { + this.v = []; + this.in = []; + this.out = []; + + const numPts = Math.floor(this.pointCount); + const angle = (Math.PI * 2) / numPts; + const rad = this.outerRadius; + const roundness = this.outerRoundness / 100; + const perimSegment = (2 * Math.PI * rad) / (numPts * 4); + let i; + let currentAng = -Math.PI * 0.5; + const dir = 1; + + for (i = 0; i < numPts; i++) { + const x = rad * Math.cos(currentAng); + const y = rad * Math.sin(currentAng); + const ox = x === 0 && y === 0 ? 0 : y / Math.sqrt(x * x + y * y); + const oy = x === 0 && y === 0 ? 0 : -x / Math.sqrt(x * x + y * y); + + const offset = i * 2; + + this.v[offset] = x; + this.v[offset + 1] = y; + this.in[offset] = x + ox * perimSegment * roundness * dir; + this.in[offset + 1] = y + oy * perimSegment * roundness * dir; + this.out[offset] = x - ox * perimSegment * roundness * dir; + this.out[offset + 1] = y - oy * perimSegment * roundness * dir; + currentAng += angle * dir; + } + } +} diff --git a/packages/effects-core/src/plugins/shape/polygon.ts b/packages/effects-core/src/plugins/shape/polygon.ts new file mode 100644 index 000000000..4dec0c395 --- /dev/null +++ b/packages/effects-core/src/plugins/shape/polygon.ts @@ -0,0 +1,167 @@ +/** + * Based on: + * https://github.com/pixijs/pixijs/blob/dev/src/maths/shapes/Polygon.ts + */ + +import { ShapePrimitive } from './shape-primitive'; +import type { PointData } from './point-data'; +import { triangulate } from './triangulate'; + +/** + * A class to define a shape via user defined coordinates. + */ +export class Polygon extends ShapePrimitive { + /** + * An array of the points of this polygon. + */ + points: number[] = []; + + /** + * `false` after moveTo, `true` after `closePath`. In all other cases it is `true`. + */ + closePath: boolean = false; + + constructor (points: PointData[] | number[]); + constructor (...points: PointData[] | number[]); + /** + * @param points - This can be an array of Points + * that form the polygon, a flat array of numbers that will be interpreted as [x,y, x,y, ...], or + * the arguments passed can be all the points of the polygon e.g. + * `new Polygon(new Point(), new Point(), ...)`, or the arguments passed can be flat + * x,y values e.g. `new Polygon(x,y, x,y, x,y, ...)` where `x` and `y` are Numbers. + */ + constructor (...points: (PointData[] | number[])[] | PointData[] | number[]) { + super(); + let flat = Array.isArray(points[0]) ? points[0] : points; + + // if this is an array of points, convert it to a flat array of numbers + if (typeof flat[0] !== 'number') { + const p: number[] = []; + + for (let i = 0, il = flat.length; i < il; i++) { + p.push((flat[i] as PointData).x, (flat[i] as PointData).y); + } + + flat = p; + } + + this.points = flat as number[]; + this.closePath = true; + } + + /** + * Creates a clone of this polygon. + * @returns - A copy of the polygon. + */ + clone (): Polygon { + const points = this.points.slice(); + const polygon = new Polygon(points); + + polygon.closePath = this.closePath; + + return polygon; + } + + /** + * Checks whether the x and y coordinates passed to this function are contained within this polygon. + * @param x - The X coordinate of the point to test. + * @param y - The Y coordinate of the point to test. + * @returns - Whether the x/y coordinates are within this polygon. + */ + contains (x: number, y: number): boolean { + let inside = false; + + // use some raycasting to test hits + // https://github.com/substack/point-in-polygon/blob/master/index.js + const length = this.points.length / 2; + + for (let i = 0, j = length - 1; i < length; j = i++) { + const xi = this.points[i * 2]; + const yi = this.points[(i * 2) + 1]; + const xj = this.points[j * 2]; + const yj = this.points[(j * 2) + 1]; + const intersect = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * ((y - yi) / (yj - yi))) + xi); + + if (intersect) { + inside = !inside; + } + } + + return inside; + } + + /** + * Copies another polygon to this one. + * @param polygon - The polygon to copy from. + * @returns Returns itself. + */ + copyFrom (polygon: Polygon): this { + this.points = polygon.points.slice(); + this.closePath = polygon.closePath; + + return this; + } + + /** + * Copies this polygon to another one. + * @param polygon - The polygon to copy to. + * @returns Returns given parameter. + */ + copyTo (polygon: Polygon): Polygon { + polygon.copyFrom(this); + + return polygon; + } + + /** + * Get the last X coordinate of the polygon + * @readonly + */ + get lastX (): number { + return this.points[this.points.length - 2]; + } + + /** + * Get the last Y coordinate of the polygon + * @readonly + */ + get lastY (): number { + return this.points[this.points.length - 1]; + } + + /** + * Get the first X coordinate of the polygon + * @readonly + */ + getX (): number { + return this.points[this.points.length - 2]; + } + /** + * Get the first Y coordinate of the polygon + * @readonly + */ + getY (): number { + return this.points[this.points.length - 1]; + } + + override build (points: number[]): void { + for (let i = 0; i < this.points.length; i++) { + points[i] = this.points[i]; + } + } + + override triangulate (points: number[], vertices: number[], verticesOffset: number, indices: number[], indicesOffset: number): void { + const triangles = triangulate([points]); + + for (let i = 0; i < triangles.length; i++) { + vertices[verticesOffset + i] = triangles[i]; + } + + const vertexCount = triangles.length / 2; + + for (let i = 0; i < vertexCount; i++) { + indices[indicesOffset + i] = i; + } + } +} + diff --git a/packages/effects-core/src/plugins/shape/rectangle.ts b/packages/effects-core/src/plugins/shape/rectangle.ts new file mode 100644 index 000000000..bf414ada4 --- /dev/null +++ b/packages/effects-core/src/plugins/shape/rectangle.ts @@ -0,0 +1,414 @@ +import { ShapePrimitive } from './shape-primitive'; + +// const tempPoints = [new Point(), new Point(), new Point(), new Point()]; + +/** + * The `Rectangle` object is an area defined by its position, as indicated by its upper-left corner + * point (`x`, `y`) and by its `width` and its `height`. + */ +export class Rectangle extends ShapePrimitive { + /** + * The X coordinate of the upper-left corner of the rectangle + * @default 0 + */ + x: number; + + /** + * The Y coordinate of the upper-left corner of the rectangle + * @default 0 + */ + y: number; + + /** + * The overall width of this rectangle + * @default 0 + */ + width: number; + + /** + * The overall height of this rectangle + * @default 0 + */ + height: number; + + /** + * @param x - The X coordinate of the upper-left corner of the rectangle + * @param y - The Y coordinate of the upper-left corner of the rectangle + * @param width - The overall width of the rectangle + * @param height - The overall height of the rectangle + */ + constructor (x: string | number = 0, y: string | number = 0, width: string | number = 0, height: string | number = 0) { + super(); + this.x = Number(x); + this.y = Number(y); + this.width = Number(width); + this.height = Number(height); + } + + /** Returns the left edge of the rectangle. */ + get left (): number { + return this.x; + } + + /** Returns the right edge of the rectangle. */ + get right (): number { + return this.x + this.width; + } + + /** Returns the top edge of the rectangle. */ + get top (): number { + return this.y; + } + + /** Returns the bottom edge of the rectangle. */ + get bottom (): number { + return this.y + this.height; + } + + /** Determines whether the Rectangle is empty. */ + isEmpty (): boolean { + return this.left === this.right || this.top === this.bottom; + } + + /** A constant empty rectangle. This is a new object every time the property is accessed */ + static get EMPTY (): Rectangle { + return new Rectangle(0, 0, 0, 0); + } + + /** + * Creates a clone of this Rectangle + * @returns a copy of the rectangle + */ + clone (): Rectangle { + return new Rectangle(this.x, this.y, this.width, this.height); + } + + /** + * Converts a Bounds object to a Rectangle object. + * @param bounds - The bounds to copy and convert to a rectangle. + * @returns Returns itself. + */ + // copyFromBounds (bounds: Bounds): this { + // this.x = bounds.minX; + // this.y = bounds.minY; + // this.width = bounds.maxX - bounds.minX; + // this.height = bounds.maxY - bounds.minY; + + // return this; + // } + + /** + * Copies another rectangle to this one. + * @param rectangle - The rectangle to copy from. + * @returns Returns itself. + */ + copyFrom (rectangle: Rectangle): Rectangle { + this.x = rectangle.x; + this.y = rectangle.y; + this.width = rectangle.width; + this.height = rectangle.height; + + return this; + } + + /** + * Copies this rectangle to another one. + * @param rectangle - The rectangle to copy to. + * @returns Returns given parameter. + */ + copyTo (rectangle: Rectangle): Rectangle { + rectangle.copyFrom(this); + + return rectangle; + } + + /** + * Checks whether the x and y coordinates given are contained within this Rectangle + * @param x - The X coordinate of the point to test + * @param y - The Y coordinate of the point to test + * @returns Whether the x/y coordinates are within this Rectangle + */ + contains (x: number, y: number): boolean { + if (this.width <= 0 || this.height <= 0) { + return false; + } + + if (x >= this.x && x < this.x + this.width) { + if (y >= this.y && y < this.y + this.height) { + return true; + } + } + + return false; + } + + /** + * Checks whether the x and y coordinates given are contained within this rectangle including the stroke. + * @param x - The X coordinate of the point to test + * @param y - The Y coordinate of the point to test + * @param strokeWidth - The width of the line to check + * @returns Whether the x/y coordinates are within this rectangle + */ + strokeContains (x: number, y: number, strokeWidth: number): boolean { + const { width, height } = this; + + if (width <= 0 || height <= 0) { return false; } + + const _x = this.x; + const _y = this.y; + + const outerLeft = _x - (strokeWidth / 2); + const outerRight = _x + width + (strokeWidth / 2); + const outerTop = _y - (strokeWidth / 2); + const outerBottom = _y + height + (strokeWidth / 2); + const innerLeft = _x + (strokeWidth / 2); + const innerRight = _x + width - (strokeWidth / 2); + const innerTop = _y + (strokeWidth / 2); + const innerBottom = _y + height - (strokeWidth / 2); + + return (x >= outerLeft && x <= outerRight && y >= outerTop && y <= outerBottom) + && !(x > innerLeft && x < innerRight && y > innerTop && y < innerBottom); + } + /** + * Determines whether the `other` Rectangle transformed by `transform` intersects with `this` Rectangle object. + * Returns true only if the area of the intersection is >0, this means that Rectangles + * sharing a side are not overlapping. Another side effect is that an arealess rectangle + * (width or height equal to zero) can't intersect any other rectangle. + * @param {Rectangle} other - The Rectangle to intersect with `this`. + * @param {Matrix} transform - The transformation matrix of `other`. + * @returns {boolean} A value of `true` if the transformed `other` Rectangle intersects with `this`; otherwise `false`. + */ + // intersects (other: Rectangle, transform?: Matrix4): boolean { + // if (!transform) { + // const x0 = this.x < other.x ? other.x : this.x; + // const x1 = this.right > other.right ? other.right : this.right; + + // if (x1 <= x0) { + // return false; + // } + + // const y0 = this.y < other.y ? other.y : this.y; + // const y1 = this.bottom > other.bottom ? other.bottom : this.bottom; + + // return y1 > y0; + // } + + // const x0 = this.left; + // const x1 = this.right; + // const y0 = this.top; + // const y1 = this.bottom; + + // if (x1 <= x0 || y1 <= y0) { + // return false; + // } + + // const lt = tempPoints[0].set(other.left, other.top); + // const lb = tempPoints[1].set(other.left, other.bottom); + // const rt = tempPoints[2].set(other.right, other.top); + // const rb = tempPoints[3].set(other.right, other.bottom); + + // if (rt.x <= lt.x || lb.y <= lt.y) { + // return false; + // } + + // const s = Math.sign((transform.a * transform.d) - (transform.b * transform.c)); + + // if (s === 0) { + // return false; + // } + + // transform.apply(lt, lt); + // transform.apply(lb, lb); + // transform.apply(rt, rt); + // transform.apply(rb, rb); + + // if (Math.max(lt.x, lb.x, rt.x, rb.x) <= x0 + // || Math.min(lt.x, lb.x, rt.x, rb.x) >= x1 + // || Math.max(lt.y, lb.y, rt.y, rb.y) <= y0 + // || Math.min(lt.y, lb.y, rt.y, rb.y) >= y1) { + // return false; + // } + + // const nx = s * (lb.y - lt.y); + // const ny = s * (lt.x - lb.x); + // const n00 = (nx * x0) + (ny * y0); + // const n10 = (nx * x1) + (ny * y0); + // const n01 = (nx * x0) + (ny * y1); + // const n11 = (nx * x1) + (ny * y1); + + // if (Math.max(n00, n10, n01, n11) <= (nx * lt.x) + (ny * lt.y) + // || Math.min(n00, n10, n01, n11) >= (nx * rb.x) + (ny * rb.y)) { + // return false; + // } + + // const mx = s * (lt.y - rt.y); + // const my = s * (rt.x - lt.x); + // const m00 = (mx * x0) + (my * y0); + // const m10 = (mx * x1) + (my * y0); + // const m01 = (mx * x0) + (my * y1); + // const m11 = (mx * x1) + (my * y1); + + // if (Math.max(m00, m10, m01, m11) <= (mx * lt.x) + (my * lt.y) + // || Math.min(m00, m10, m01, m11) >= (mx * rb.x) + (my * rb.y)) { + // return false; + // } + + // return true; + // } + + /** + * Pads the rectangle making it grow in all directions. + * If paddingY is omitted, both paddingX and paddingY will be set to paddingX. + * @param paddingX - The horizontal padding amount. + * @param paddingY - The vertical padding amount. + * @returns Returns itself. + */ + pad (paddingX = 0, paddingY = paddingX): this { + this.x -= paddingX; + this.y -= paddingY; + + this.width += paddingX * 2; + this.height += paddingY * 2; + + return this; + } + + /** + * Fits this rectangle around the passed one. + * @param rectangle - The rectangle to fit. + * @returns Returns itself. + */ + fit (rectangle: Rectangle): this { + const x1 = Math.max(this.x, rectangle.x); + const x2 = Math.min(this.x + this.width, rectangle.x + rectangle.width); + const y1 = Math.max(this.y, rectangle.y); + const y2 = Math.min(this.y + this.height, rectangle.y + rectangle.height); + + this.x = x1; + this.width = Math.max(x2 - x1, 0); + this.y = y1; + this.height = Math.max(y2 - y1, 0); + + return this; + } + + /** + * Enlarges rectangle that way its corners lie on grid + * @param resolution - resolution + * @param eps - precision + * @returns Returns itself. + */ + ceil (resolution = 1, eps = 0.001): this { + const x2 = Math.ceil((this.x + this.width - eps) * resolution) / resolution; + const y2 = Math.ceil((this.y + this.height - eps) * resolution) / resolution; + + this.x = Math.floor((this.x + eps) * resolution) / resolution; + this.y = Math.floor((this.y + eps) * resolution) / resolution; + + this.width = x2 - this.x; + this.height = y2 - this.y; + + return this; + } + + /** + * Enlarges this rectangle to include the passed rectangle. + * @param rectangle - The rectangle to include. + * @returns Returns itself. + */ + enlarge (rectangle: Rectangle): this { + const x1 = Math.min(this.x, rectangle.x); + const x2 = Math.max(this.x + this.width, rectangle.x + rectangle.width); + const y1 = Math.min(this.y, rectangle.y); + const y2 = Math.max(this.y + this.height, rectangle.y + rectangle.height); + + this.x = x1; + this.width = x2 - x1; + this.y = y1; + this.height = y2 - y1; + + return this; + } + + /** + * Returns the framing rectangle of the rectangle as a Rectangle object + * @param out - optional rectangle to store the result + * @returns The framing rectangle + */ + getBounds (out?: Rectangle): Rectangle { + out = out || new Rectangle(); + out.copyFrom(this); + + return out; + } + + getX (): number { + return this.x; + } + + getY (): number { + return this.y; + } + + override build (points: number[]): number[] { + const x = this.x; + const y = this.y; + const width = this.width; + const height = this.height; + + if (!(width >= 0 && height >= 0)) { + return points; + } + + points[0] = x; + points[1] = y; + points[2] = x + width; + points[3] = y; + points[4] = x + width; + points[5] = y + height; + points[6] = x; + points[7] = y + height; + + return points; + } + + override triangulate (points: number[], vertices: number[], verticesOffset: number, indices: number[], indicesOffset: number) { + let count = 0; + + const verticesStride = 2; + + verticesOffset *= verticesStride; + + vertices[verticesOffset + count] = points[0]; + vertices[verticesOffset + count + 1] = points[1]; + + count += verticesStride; + + vertices[verticesOffset + count] = points[2]; + vertices[verticesOffset + count + 1] = points[3]; + + count += verticesStride; + + vertices[verticesOffset + count] = points[6]; + vertices[verticesOffset + count + 1] = points[7]; + + count += verticesStride; + + vertices[verticesOffset + count] = points[4]; + vertices[verticesOffset + count + 1] = points[5]; + + count += verticesStride; + + const verticesIndex = verticesOffset / verticesStride; + + // triangle 1 + indices[indicesOffset++] = verticesIndex; + indices[indicesOffset++] = verticesIndex + 1; + indices[indicesOffset++] = verticesIndex + 2; + + // triangle 2 + indices[indicesOffset++] = verticesIndex + 1; + indices[indicesOffset++] = verticesIndex + 3; + indices[indicesOffset++] = verticesIndex + 2; + } +} diff --git a/packages/effects-core/src/plugins/shape/shape-path.ts b/packages/effects-core/src/plugins/shape/shape-path.ts new file mode 100644 index 000000000..f0773f3cd --- /dev/null +++ b/packages/effects-core/src/plugins/shape/shape-path.ts @@ -0,0 +1,209 @@ +/** + * Based on: + * https://github.com/pixijs/pixijs/blob/dev/src/scene/graphics/shared/path/ShapePath.ts + */ + +import type { Matrix4 } from '@galacean/effects-math/es/core/matrix4'; +import { Polygon } from './polygon'; +import { buildAdaptiveBezier } from './build-adaptive-bezier'; +import type { GraphicsPath } from './graphics-path'; +import type { ShapePrimitive } from './shape-primitive'; +import { Ellipse } from './ellipse'; +import type { StarType } from './poly-star'; +import { PolyStar } from './poly-star'; +import { Rectangle } from './rectangle'; + +export class ShapePath { + currentPoly: Polygon | null = null; + shapePrimitives: { shape: ShapePrimitive, transform?: Matrix4 }[] = []; + + constructor ( + private graphicsPath: GraphicsPath, + ) { } + + /** Builds the path. */ + buildPath () { + this.currentPoly = null; + this.shapePrimitives.length = 0; + const path = this.graphicsPath; + + for (const instruction of path.instructions) { + const action = instruction.action; + const data = instruction.data; + + switch (action) { + case 'bezierCurveTo': { + this.bezierCurveTo(data[0], data[1], data[2], data[3], data[4], data[5], data[6]); + + break; + } + case 'moveTo': { + this.moveTo(data[0], data[1]); + + break; + } + case 'ellipse': { + this.ellipse(data[0], data[1], data[2], data[3], data[4]); + + break; + } + case 'polyStar': { + this.polyStar(data[0], data[1], data[2], data[3], data[4], data[5], data[6]); + + break; + } + case 'rect': { + this.rect(data[0], data[1], data[2], data[3], data[4]); + + break; + } + } + } + this.endPoly(); + } + + /** + * Adds a cubic Bezier curve to the path. + * It requires three points: the first two are control points and the third one is the end point. + * The starting point is the last point in the current path. + * @param cp1x - The x-coordinate of the first control point. + * @param cp1y - The y-coordinate of the first control point. + * @param cp2x - The x-coordinate of the second control point. + * @param cp2y - The y-coordinate of the second control point. + * @param x - The x-coordinate of the end point. + * @param y - The y-coordinate of the end point. + * @param smoothness - Optional parameter to adjust the smoothness of the curve. + * @returns The instance of the current object for chaining. + */ + bezierCurveTo ( + cp1x: number, cp1y: number, cp2x: number, cp2y: number, + x: number, y: number, + smoothness?: number, + ): ShapePath { + this.ensurePoly(); + const currentPoly = this.currentPoly as Polygon; + + buildAdaptiveBezier( + currentPoly.points, + currentPoly.lastX, currentPoly.lastY, + cp1x, cp1y, cp2x, cp2y, x, y, + smoothness, + ); + + return this; + } + + moveTo (x: number, y: number): ShapePath { + this.startPoly(x, y); + + return this; + } + + /** + * Draws an ellipse at the specified location and with the given x and y radii. + * An optional transformation can be applied, allowing for rotation, scaling, and translation. + * @param x - The x-coordinate of the center of the ellipse. + * @param y - The y-coordinate of the center of the ellipse. + * @param radiusX - The horizontal radius of the ellipse. + * @param radiusY - The vertical radius of the ellipse. + * @param transform - An optional `Matrix` object to apply a transformation to the ellipse. This can include rotations. + * @returns The instance of the current object for chaining. + */ + ellipse (x: number, y: number, radiusX: number, radiusY: number, transform?: Matrix4): this { + // TODO apply rotation to transform... + + this.drawShape(new Ellipse(x, y, radiusX, radiusY), transform); + + return this; + } + + polyStar (pointCount: number, outerRadius: number, innerRadius: number, outerRoundness: number, innerRoundness: number, starType: StarType, transform?: Matrix4) { + this.drawShape(new PolyStar(pointCount, outerRadius, innerRadius, outerRoundness, innerRoundness, starType), transform); + + return this; + } + + /** + * Draws a rectangle shape. This method adds a new rectangle path to the current drawing. + * @param x - The x-coordinate of the upper-left corner of the rectangle. + * @param y - The y-coordinate of the upper-left corner of the rectangle. + * @param w - The width of the rectangle. + * @param h - The height of the rectangle. + * @param transform - An optional `Matrix` object to apply a transformation to the rectangle. + * @returns The instance of the current object for chaining. + */ + rect (x: number, y: number, w: number, h: number, transform?: Matrix4): this { + this.drawShape(new Rectangle(x, y, w, h), transform); + + return this; + } + + /** + * Draws a given shape on the canvas. + * This is a generic method that can draw any type of shape specified by the `ShapePrimitive` parameter. + * An optional transformation matrix can be applied to the shape, allowing for complex transformations. + * @param shape - The shape to draw, defined as a `ShapePrimitive` object. + * @param matrix - An optional `Matrix` for transforming the shape. This can include rotations, + * scaling, and translations. + * @returns The instance of the current object for chaining. + */ + drawShape (shape: ShapePrimitive, matrix?: Matrix4): this { + this.endPoly(); + + this.shapePrimitives.push({ shape, transform: matrix }); + + return this; + } + + /** + * Starts a new polygon path from the specified starting point. + * This method initializes a new polygon or ends the current one if it exists. + * @param x - The x-coordinate of the starting point of the new polygon. + * @param y - The y-coordinate of the starting point of the new polygon. + * @returns The instance of the current object for chaining. + */ + private startPoly (x: number, y: number): this { + let currentPoly = this.currentPoly; + + if (currentPoly) { + this.endPoly(); + } + + currentPoly = new Polygon(); + + currentPoly.points.push(x, y); + + this.currentPoly = currentPoly; + + return this; + } + + /** + * Ends the current polygon path. If `closePath` is set to true, + * the path is closed by connecting the last point to the first one. + * This method finalizes the current polygon and prepares it for drawing or adding to the shape primitives. + * @param closePath - A boolean indicating whether to close the polygon by connecting the last point + * back to the starting point. False by default. + * @returns The instance of the current object for chaining. + */ + private endPoly (closePath = false): this { + const shape = this.currentPoly; + + if (shape && shape.points.length > 2) { + shape.closePath = closePath; + + this.shapePrimitives.push({ shape }); + } + + this.currentPoly = null; + + return this; + } + + private ensurePoly (start = true): void { + if (this.currentPoly) { return; } + + this.currentPoly = new Polygon(); + this.currentPoly.points.push(0, 0); + } +} diff --git a/packages/effects-core/src/plugins/shape/shape-primitive.ts b/packages/effects-core/src/plugins/shape/shape-primitive.ts new file mode 100644 index 000000000..81b1e5b9b --- /dev/null +++ b/packages/effects-core/src/plugins/shape/shape-primitive.ts @@ -0,0 +1,24 @@ +export abstract class ShapePrimitive { + + /** Checks whether the x and y coordinates passed to this function are contained within this ShapePrimitive. */ + // abstract contains (x: number, y: number): boolean; + /** Checks whether the x and y coordinates passed to this function are contained within the stroke of this shape */ + // abstract strokeContains (x: number, y: number, strokeWidth: number): boolean; + /** Creates a clone of this ShapePrimitive instance. */ + abstract clone (): ShapePrimitive; + /** Copies the properties from another ShapePrimitive to this ShapePrimitive. */ + abstract copyFrom (source: ShapePrimitive): void; + /** Copies the properties from this ShapePrimitive to another ShapePrimitive. */ + abstract copyTo (destination: ShapePrimitive): void; + /** Returns the framing rectangle of the ShapePrimitive as a Rectangle object. */ + // getBounds(out?: Rectangle): Rectangle, + + /** The X coordinate of the shape */ + // abstract getX (): number; + /** The Y coordinate of the shape */ + // abstract getY (): number; + + abstract build (points: number[]): void; + + abstract triangulate (points: number[], vertices: number[], verticesOffset: number, indices: number[], indicesOffset: number): void; +} diff --git a/packages/effects-core/src/plugins/shape/triangle.ts b/packages/effects-core/src/plugins/shape/triangle.ts new file mode 100644 index 000000000..887b260ed --- /dev/null +++ b/packages/effects-core/src/plugins/shape/triangle.ts @@ -0,0 +1,206 @@ +import { Rectangle } from './rectangle'; +import { ShapePrimitive } from './shape-primitive'; + +/** + * A class to define a shape of a triangle via user defined coordinates. + * + * Create a `Triangle` object with the `x`, `y`, `x2`, `y2`, `x3`, `y3` properties. + */ +export class Triangle extends ShapePrimitive { + /** + * The X coord of the first point. + * @default 0 + */ + x: number; + /** + * The Y coord of the first point. + * @default 0 + */ + y: number; + /** + * The X coord of the second point. + * @default 0 + */ + x2: number; + /** + * The Y coord of the second point. + * @default 0 + */ + y2: number; + /** + * The X coord of the third point. + * @default 0 + */ + x3: number; + /** + * The Y coord of the third point. + * @default 0 + */ + y3: number; + + /** + * @param x - The X coord of the first point. + * @param y - The Y coord of the first point. + * @param x2 - The X coord of the second point. + * @param y2 - The Y coord of the second point. + * @param x3 - The X coord of the third point. + * @param y3 - The Y coord of the third point. + */ + constructor (x = 0, y = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0) { + super(); + this.x = x; + this.y = y; + this.x2 = x2; + this.y2 = y2; + this.x3 = x3; + this.y3 = y3; + } + + /** + * Checks whether the x and y coordinates given are contained within this triangle + * @param x - The X coordinate of the point to test + * @param y - The Y coordinate of the point to test + * @returns Whether the x/y coordinates are within this Triangle + */ + contains (x: number, y: number): boolean { + const s = ((this.x - this.x3) * (y - this.y3)) - ((this.y - this.y3) * (x - this.x3)); + const t = ((this.x2 - this.x) * (y - this.y)) - ((this.y2 - this.y) * (x - this.x)); + + if ((s < 0) !== (t < 0) && s !== 0 && t !== 0) { return false; } + + const d = ((this.x3 - this.x2) * (y - this.y2)) - ((this.y3 - this.y2) * (x - this.x2)); + + return d === 0 || (d < 0) === (s + t <= 0); + } + + /** + * Checks whether the x and y coordinates given are contained within this triangle including the stroke. + * @param pointX - The X coordinate of the point to test + * @param pointY - The Y coordinate of the point to test + * @param strokeWidth - The width of the line to check + * @returns Whether the x/y coordinates are within this triangle + */ + // strokeContains (pointX: number, pointY: number, strokeWidth: number): boolean { + // const halfStrokeWidth = strokeWidth / 2; + // const halfStrokeWidthSquared = halfStrokeWidth * halfStrokeWidth; + + // const { x, x2, x3, y, y2, y3 } = this; + + // if (squaredDistanceToLineSegment(pointX, pointY, x, y, x2, y3) <= halfStrokeWidthSquared + // || squaredDistanceToLineSegment(pointX, pointY, x2, y2, x3, y3) <= halfStrokeWidthSquared + // || squaredDistanceToLineSegment(pointX, pointY, x3, y3, x, y) <= halfStrokeWidthSquared) { + // return true; + // } + + // return false; + // } + + /** + * Creates a clone of this Triangle + * @returns a copy of the triangle + */ + clone (): ShapePrimitive { + const triangle = new Triangle( + this.x, + this.y, + this.x2, + this.y2, + this.x3, + this.y3 + ); + + return triangle; + } + + /** + * Copies another triangle to this one. + * @param triangle - The triangle to copy from. + * @returns Returns itself. + */ + copyFrom (triangle: Triangle): this { + this.x = triangle.x; + this.y = triangle.y; + this.x2 = triangle.x2; + this.y2 = triangle.y2; + this.x3 = triangle.x3; + this.y3 = triangle.y3; + + return this; + } + + /** + * Copies this triangle to another one. + * @param triangle - The triangle to copy to. + * @returns Returns given parameter. + */ + copyTo (triangle: Triangle): Triangle { + triangle.copyFrom(this); + + return triangle; + } + + /** + * Returns the framing rectangle of the triangle as a Rectangle object + * @param out - optional rectangle to store the result + * @returns The framing rectangle + */ + getBounds (out?: Rectangle): Rectangle { + out = out || new Rectangle(); + + const minX = Math.min(this.x, this.x2, this.x3); + const maxX = Math.max(this.x, this.x2, this.x3); + const minY = Math.min(this.y, this.y2, this.y3); + const maxY = Math.max(this.y, this.y2, this.y3); + + out.x = minX; + out.y = minY; + out.width = maxX - minX; + out.height = maxY - minY; + + return out; + } + + getX (): number { + return this.x; + } + + getY (): number { + return this.y; + } + + override build (points: number[]): void { + points[0] = this.x; + points[1] = this.y; + points[2] = this.x2; + points[3] = this.y2; + points[4] = this.x3; + points[5] = this.y3; + } + + override triangulate (points: number[], vertices: number[], verticesOffset: number, indices: number[], indicesOffset: number): void { + let count = 0; + const verticesStride = 2; + + verticesOffset *= verticesStride; + + vertices[verticesOffset + count] = points[0]; + vertices[verticesOffset + count + 1] = points[1]; + + count += verticesStride; + + vertices[verticesOffset + count] = points[2]; + vertices[verticesOffset + count + 1] = points[3]; + + count += verticesStride; + + vertices[verticesOffset + count] = points[4]; + vertices[verticesOffset + count + 1] = points[5]; + + const verticesIndex = verticesOffset / verticesStride; + + // triangle 1 + indices[indicesOffset++] = verticesIndex; + indices[indicesOffset++] = verticesIndex + 1; + indices[indicesOffset++] = verticesIndex + 2; + } +} diff --git a/packages/effects-core/src/plugins/shape/triangulate.ts b/packages/effects-core/src/plugins/shape/triangulate.ts new file mode 100644 index 000000000..82d627d49 --- /dev/null +++ b/packages/effects-core/src/plugins/shape/triangulate.ts @@ -0,0 +1,73 @@ +import * as libtess from 'libtess'; + +const tessy = (function initTesselator () { + // function called for each vertex of tesselator output + function vertexCallback ( + data: [x: number, y: number, z: number], + polyVertArray: number[], + ) { + polyVertArray[polyVertArray.length] = data[0]; + polyVertArray[polyVertArray.length] = data[1]; + } + function begincallback (type: number) { + if (type !== libtess.primitiveType.GL_TRIANGLES) { + console.info('expected TRIANGLES but got type: ' + type); + } + } + function errorcallback (errno: number) { + console.error('error callback, error number: ' + errno); + } + // callback for when segments intersect and must be split + function combinecallback ( + coords: [number, number, number], + data: number[][], + weight: number[], + ) { + // console.log('combine callback'); + return [coords[0], coords[1], coords[2]]; + } + function edgeCallback (flag: boolean) { + // don't really care about the flag, but need no-strip/no-fan behavior + // console.log('edge flag: ' + flag); + } + + const tessy = new libtess.GluTesselator(); + + // tessy.gluTessProperty(libtess.gluEnum.GLU_TESS_WINDING_RULE, libtess.windingRule.GLU_TESS_WINDING_POSITIVE); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); + + return tessy; +})(); + +export function triangulate (contours: number[][]) { + // libtess will take 3d verts and flatten to a plane for tesselation + // since only doing 2d tesselation here, provide z=1 normal to skip + // iterating over verts only to get the same answer. + // comment out to test normal-generation code + tessy.gluTessNormal(0, 0, 1); + + const triangleVerts: number[] = []; + + tessy.gluTessBeginPolygon(triangleVerts); + + for (let i = 0; i < contours.length; i++) { + tessy.gluTessBeginContour(); + const contour = contours[i]; + + for (let j = 0; j < contour.length; j += 2) { + const coords = [contour[j], contour[j + 1], 0]; + + tessy.gluTessVertex(coords, coords); + } + tessy.gluTessEndContour(); + } + + // finish polygon + tessy.gluTessEndPolygon(); + + return triangleVerts; +} diff --git a/packages/effects-core/src/plugins/sprite/sprite-item.ts b/packages/effects-core/src/plugins/sprite/sprite-item.ts index bb63b734c..c118e77d6 100644 --- a/packages/effects-core/src/plugins/sprite/sprite-item.ts +++ b/packages/effects-core/src/plugins/sprite/sprite-item.ts @@ -1,25 +1,17 @@ -import { Matrix4, Vector3, Vector4 } from '@galacean/effects-math/es/core/index'; -import type { vec2, vec4 } from '@galacean/effects-specification'; +import { Matrix4, Vector4 } from '@galacean/effects-math/es/core/index'; import * as spec from '@galacean/effects-specification'; -import { RendererComponent } from '../../components/renderer-component'; import { effectsClass } from '../../decorators'; import type { Engine } from '../../engine'; import { glContext } from '../../gl'; -import type { MaterialProps } from '../../material'; -import { Material, getPreMultiAlpha, setBlendMode, setMaskMode, setSideMode } from '../../material'; -import type { ValueGetter } from '../../math'; -import { createValueGetter, trianglesFromRect, vecFill, vecMulCombine } from '../../math'; -import type { GeometryDrawMode, Renderer } from '../../render'; +import type { GeometryDrawMode } from '../../render'; import { Geometry } from '../../render'; import type { GeometryFromShape } from '../../shape'; -import type { Texture } from '../../texture'; -import { addItem, colorStopsFromGradient, getColorFromGradientStops } from '../../utils'; -import type { FrameContext, PlayableGraph } from '../cal/playable-graph'; -import { Playable, PlayableAsset } from '../cal/playable-graph'; -import type { BoundingBoxTriangle, HitTestTriangleParams } from '../interact/click-handler'; -import { HitTestType } from '../interact/click-handler'; -import { getImageItemRenderInfo, maxSpriteMeshItemCount, spriteMeshShaderFromRenderInfo } from './sprite-mesh'; -import { VFXItem } from '../../vfx-item'; +import { TextureSourceType, type Texture, type Texture2DSourceOptionsVideo } from '../../texture'; +import type { PlayableGraph, Playable } from '../cal/playable-graph'; +import { PlayableAsset } from '../cal/playable-graph'; +import type { ColorPlayableAssetData } from '../../animation'; +import { ColorPlayable } from '../../animation'; +import { BaseRenderComponent, getImageItemRenderInfo } from '../../components/base-render-component'; /** * 用于创建 spriteItem 的数据类型, 经过处理后的 spec.SpriteContent @@ -37,253 +29,55 @@ export interface SpriteItemProps extends Omit { * 图层元素基础属性, 经过处理后的 spec.SpriteContent.options */ export type SpriteItemOptions = { - startColor: vec4, + startColor: spec.vec4, renderLevel?: spec.RenderLevel, }; -/** - * 图层元素渲染属性, 经过处理后的 spec.SpriteContent.renderer - */ -export interface SpriteItemRenderer extends Required> { - order: number, - mask: number, - texture: Texture, - shape?: GeometryFromShape, - anchor?: vec2, - particleOrigin?: spec.ParticleOrigin, -} - -/** - * 图层的渲染属性,用于 Mesh 的合并判断 - */ -export interface SpriteItemRenderInfo { - side: number, - occlusion: boolean, - blending: number, - cachePrefix: string, - mask: number, - maskMode: number, - cacheId: string, - wireframe?: boolean, -} - export type splitsDataType = [r: number, x: number, y: number, w: number, h: number | undefined][]; const singleSplits: splitsDataType = [[0, 0, 1, 1, undefined]]; -const tempColor: vec4 = [1, 1, 1, 1]; let seed = 0; -export class SpriteColorPlayable extends Playable { - clipData: { colorOverLifetime?: spec.ColorOverLifetime, startColor?: spec.RGBAColorValue }; - colorOverLifetime: { stop: number, color: any }[]; - opacityOverLifetime: ValueGetter; - startColor: spec.RGBAColorValue; - renderColor: vec4 = [1, 1, 1, 1]; - spriteMaterial: Material; - spriteComponent: SpriteComponent; - - override processFrame (context: FrameContext): void { - const boundObject = context.output.getUserData(); - - if (!(boundObject instanceof VFXItem)) { - return; - } - if (!this.spriteComponent) { - this.spriteComponent = boundObject.getComponent(SpriteComponent); - } - if (!this.spriteMaterial) { - this.spriteMaterial = this.spriteComponent.material; - const startColor = this.spriteMaterial.getVector4('_Color'); - - if (startColor) { - this.startColor = startColor.toArray(); - } - } - - this.spriteComponent.setAnimationTime(this.time); - let colorInc = vecFill(tempColor, 1); - let colorChanged; - const life = this.time / boundObject.duration; - - const opacityOverLifetime = this.opacityOverLifetime; - const colorOverLifetime = this.colorOverLifetime; - - if (colorOverLifetime) { - colorInc = getColorFromGradientStops(colorOverLifetime, life, true) as vec4; - colorChanged = true; - } - if (opacityOverLifetime) { - colorInc[3] *= opacityOverLifetime.getValue(life); - colorChanged = true; - } - - if (colorChanged) { - vecMulCombine(this.renderColor, colorInc, this.startColor); - this.spriteMaterial.getVector4('_Color')?.setFromArray(this.renderColor); - } - } - - create (clipData: SpriteColorPlayableAssetData) { - this.clipData = clipData; - const colorOverLifetime = clipData.colorOverLifetime; - - if (colorOverLifetime) { - this.opacityOverLifetime = createValueGetter(colorOverLifetime.opacity ?? 1); - if (colorOverLifetime.color && colorOverLifetime.color[0] === spec.ValueType.GRADIENT_COLOR) { - this.colorOverLifetime = colorStopsFromGradient(colorOverLifetime.color[1]); - } - } - - return this; - } -} - -@effectsClass('SpriteColorPlayableAsset') +@effectsClass(spec.DataType.SpriteColorPlayableAsset) export class SpriteColorPlayableAsset extends PlayableAsset { - data: SpriteColorPlayableAssetData; + data: ColorPlayableAssetData; override createPlayable (graph: PlayableGraph): Playable { - const spriteColorPlayable = new SpriteColorPlayable(graph); + const spriteColorPlayable = new ColorPlayable(graph); spriteColorPlayable.create(this.data); return spriteColorPlayable; } - override fromData (data: SpriteColorPlayableAssetData): void { + override fromData (data: ColorPlayableAssetData): void { this.data = data; } } -export interface SpriteColorPlayableAssetData extends spec.EffectsObjectData { - colorOverLifetime?: spec.ColorOverLifetime, -} - @effectsClass(spec.DataType.SpriteComponent) -export class SpriteComponent extends RendererComponent { - renderer: SpriteItemRenderer; - interaction?: { behavior: spec.InteractBehavior }; - cachePrefix = '-'; - geoData: { atlasOffset: number[] | spec.TypedArray, index: number[] | spec.TypedArray }; - anchor?: vec2; - +export class SpriteComponent extends BaseRenderComponent { textureSheetAnimation?: spec.TextureSheetAnimation; + splits: splitsDataType = singleSplits; frameAnimationLoop = false; - splits: splitsDataType; - emptyTexture: Texture; - color: vec4 = [1, 1, 1, 1]; - worldMatrix: Matrix4; - geometry: Geometry; /* 要过包含父节点颜色/透明度变化的动画的帧对比 打开这段兼容代码 */ // override colorOverLifetime: { stop: number, color: any }[]; // override opacityOverLifetime: ValueGetter; - /***********************/ - private renderInfo: SpriteItemRenderInfo; - // readonly mesh: Mesh; - private readonly wireframe?: boolean; - private preMultiAlpha: number; - private visible = true; - private isManualTimeSet = false; - private frameAnimationTime = 0; constructor (engine: Engine, props?: SpriteItemProps) { super(engine); this.name = 'MSprite' + seed++; - this.renderer = { - renderMode: spec.RenderMode.BILLBOARD, - blending: spec.BlendingMode.ALPHA, - texture: this.engine.emptyTexture, - occlusion: false, - transparentOcclusion: false, - side: spec.SideMode.DOUBLE, - mask: 0, - maskMode: spec.MaskMode.NONE, - order: 0, - }; - this.emptyTexture = this.engine.emptyTexture; - this.splits = singleSplits; - this.renderInfo = getImageItemRenderInfo(this); - - const geometry = this.createGeometry(glContext.TRIANGLES); - const material = this.createMaterial(this.renderInfo, 2); - - this.worldMatrix = Matrix4.fromIdentity(); - this.material = material; - this.geometry = geometry; - this.material.setVector4('_Color', new Vector4().setFromArray([1, 1, 1, 1])); - this.material.setVector4('_TexOffset', new Vector4().setFromArray([0, 0, 1, 1])); + this.geometry = this.createGeometry(glContext.TRIANGLES); this.setItem(); - if (props) { this.fromData(props); } } - /** - * 设置当前 Mesh 的可见性。 - * @param visible - true:可见,false:不可见 - */ - setVisible (visible: boolean) { - this.visible = visible; - } - /** - * 获取当前 Mesh 的可见性。 - */ - getVisible (): boolean { - return this.visible; - } - - /** - * 设置当前图层的颜色 - * > Tips: 透明度也属于颜色的一部分,当有透明度/颜色 K 帧变化时,该 API 会失效 - * @since 2.0.0 - * @param color - 颜色值 - */ - setColor (color: vec4) { - this.color = color; - this.material.setVector4('_Color', new Vector4().setFromArray(color)); - } - - /** - * 设置当前 Mesh 的纹理 - * @since 2.0.0 - * @param texture - 纹理对象 - */ - setTexture (texture: Texture) { - this.renderer.texture = texture; - this.material.setTexture('uSampler0', texture); - } - - /** - * @internal - */ - setAnimationTime (time: number) { - this.frameAnimationTime = time; - this.isManualTimeSet = true; - } - - override render (renderer: Renderer) { - if (!this.getVisible()) { - return; - } - const material = this.material; - const geo = this.geometry; - - if (renderer.renderingData.currentFrame.globalUniforms) { - renderer.setGlobalMatrix('effects_ObjectToWorld', this.transform.getWorldMatrix()); - } - this.material.setVector2('_Size', this.transform.size); - renderer.drawGeometry(geo, material); - } - - override start (): void { - this.item.getHitTestParams = this.getHitTestParams; - } - - override update (dt: number): void { + override onUpdate (dt: number): void { if (!this.isManualTimeSet) { this.frameAnimationTime += dt / 1000; this.isManualTimeSet = false; @@ -296,7 +90,16 @@ export class SpriteComponent extends RendererComponent { } const life = Math.min(Math.max(time / duration, 0.0), 1.0); const ta = this.textureSheetAnimation; + const { video } = this.renderer.texture.source as Texture2DSourceOptionsVideo; + + if (video) { + if (time === 0) { + video.pause(); + } else { + video.play().catch(e => { this.engine.renderErrors.add(e); }); + } + } if (ta) { const total = ta.total || (ta.row * ta.col); let texRectX = 0; @@ -345,80 +148,31 @@ export class SpriteComponent extends RendererComponent { dx, dy, ]); } + } override onDestroy (): void { + const textures = this.getTextures(); + if (this.item && this.item.composition) { - this.item.composition.destroyTextures(this.getTextures()); + this.item.composition.destroyTextures(textures); } - } - private getItemInitData () { - this.geoData = this.getItemGeometryData(); + textures.forEach(texture => { + const source = texture.source; - const { index, atlasOffset } = this.geoData; - const idxCount = index.length; - // @ts-expect-error - const indexData: number[] = this.wireframe ? new Uint8Array([0, 1, 1, 3, 2, 3, 2, 0]) : new index.constructor(idxCount); - - if (!this.wireframe) { - for (let i = 0; i < idxCount; i++) { - indexData[i] = 0 + index[i]; + if ( + source.sourceType === TextureSourceType.video && + source?.video + ) { + source.video.pause(); + source.video.src = ''; + source.video.load(); } - } - - return { - atlasOffset, - index: indexData, - }; - } - - private setItem () { - const textures: Texture[] = []; - let texture = this.renderer.texture; - - if (texture) { - addItem(textures, texture); - } - texture = this.renderer.texture; - const data = this.getItemInitData(); - - const renderer = this.renderer; - const texParams = this.material.getVector4('_TexParams'); - - if (texParams) { - texParams.x = renderer.occlusion ? +(renderer.transparentOcclusion) : 1; - texParams.y = +this.preMultiAlpha; - texParams.z = renderer.renderMode; - } - - const attributes = { - atlasOffset: new Float32Array(data.atlasOffset.length), - index: new Uint16Array(data.index.length), - }; - - attributes.atlasOffset.set(data.atlasOffset); - attributes.index.set(data.index); - const { material, geometry } = this; - const indexData = attributes.index; - - geometry.setIndexData(indexData); - geometry.setAttributeData('atlasOffset', attributes.atlasOffset); - geometry.setDrawCount(data.index.length); - for (let i = 0; i < textures.length; i++) { - const texture = textures[i]; - - material.setTexture('uSampler' + i, texture); - } - // FIXME: 内存泄漏的临时方案,后面再调整 - const emptyTexture = this.emptyTexture; - - for (let k = textures.length; k < maxSpriteMeshItemCount; k++) { - material.setTexture('uSampler' + k, emptyTexture); - } + }); } - private createGeometry (mode: GeometryDrawMode) { + override createGeometry (mode: GeometryDrawMode) { const maxVertex = 12 * this.splits.length; return Geometry.create(this.engine, { @@ -448,51 +202,10 @@ export class SpriteComponent extends RendererComponent { } - private createMaterial (renderInfo: SpriteItemRenderInfo, count: number): Material { - const { side, occlusion, blending, maskMode, mask } = renderInfo; - const materialProps: MaterialProps = { - shader: spriteMeshShaderFromRenderInfo(renderInfo, count, 1), - }; - - this.preMultiAlpha = getPreMultiAlpha(blending); - - const material = Material.create(this.engine, materialProps); - - const states = { - side, - blending: true, - blendMode: blending, - mask, - maskMode, - depthTest: true, - depthMask: occlusion, - }; - - material.blending = states.blending; - material.stencilRef = states.mask !== undefined ? [states.mask, states.mask] : undefined; - material.depthTest = states.depthTest; - material.depthMask = states.depthMask; - states.blending && setBlendMode(material, states.blendMode); - setMaskMode(material, states.maskMode); - setSideMode(material, states.side); - - material.shader.shaderData.properties = 'uSampler0("uSampler0",2D) = "white" {}'; - if (!material.hasUniform('_Color')) { - material.setVector4('_Color', new Vector4(0, 0, 0, 1)); - } - if (!material.hasUniform('_TexOffset')) { - material.setVector4('_TexOffset', new Vector4()); - } - if (!material.hasUniform('_TexParams')) { - material.setVector4('_TexParams', new Vector4()); - } - - return material; - } - - private getItemGeometryData () { - const { splits, renderer, textureSheetAnimation } = this; + override getItemGeometryData () { + const { splits, textureSheetAnimation } = this; const sx = 1, sy = 1; + const renderer = this.renderer; if (renderer.shape) { const { index = [], aPoint = [] } = renderer.shape; @@ -510,7 +223,7 @@ export class SpriteComponent extends RendererComponent { this.geometry.setAttributeData('aPos', new Float32Array(position)); return { - index, + index: index as number[], atlasOffset, }; } @@ -563,64 +276,11 @@ export class SpriteComponent extends RendererComponent { index.push(base, 1 + base, 2 + base, 2 + base, 1 + base, 3 + base); } } - this.geometry.setAttributeData('aPos', new Float32Array(position)); return { index, atlasOffset }; } - getTextures (): Texture[] { - const ret = []; - const tex = this.renderer.texture; - - if (tex) { - ret.push(tex); - } - - return ret; - } - - /** - * 获取图层包围盒的类型和世界坐标 - * @returns - */ - getBoundingBox (): BoundingBoxTriangle | void { - if (!this.item) { - return; - } - const worldMatrix = this.transform.getWorldMatrix(); - const triangles = trianglesFromRect(Vector3.ZERO, 0.5 * this.transform.size.x, 0.5 * this.transform.size.y); - - triangles.forEach(triangle => { - worldMatrix.transformPoint(triangle.p0 as Vector3); - worldMatrix.transformPoint(triangle.p1 as Vector3); - worldMatrix.transformPoint(triangle.p2 as Vector3); - }); - - return { - type: HitTestType.triangle, - area: triangles, - }; - } - - getHitTestParams = (force?: boolean): HitTestTriangleParams | void => { - const ui = this.interaction; - - if ((force || ui)) { - const area = this.getBoundingBox(); - - if (area) { - return { - behavior: this.interaction?.behavior || 0, - type: area.type, - triangles: area.area, - backfaceCulling: this.renderer.side === spec.SideMode.FRONT, - }; - } - } - }; - - // TODO: [1.31] @十弦 https://github.com/galacean/effects-runtime/commit/fe8736540b9a461d8e96658f4d755ff8089a263b#diff-a3618f4527c5fe6e842f20d67d5c82984568502c6bf6fdfcbd24f69e2894ca90 override fromData (data: SpriteItemProps): void { super.fromData(data); @@ -628,8 +288,7 @@ export class SpriteComponent extends RendererComponent { let renderer = data.renderer; if (!renderer) { - //@ts-expect-error - renderer = {}; + renderer = {} as SpriteItemProps['renderer']; } this.interaction = interaction; @@ -637,8 +296,8 @@ export class SpriteComponent extends RendererComponent { renderMode: renderer.renderMode ?? spec.RenderMode.BILLBOARD, blending: renderer.blending ?? spec.BlendingMode.ALPHA, texture: renderer.texture ?? this.engine.emptyTexture, - occlusion: !!(renderer.occlusion), - transparentOcclusion: !!(renderer.transparentOcclusion) || (renderer.maskMode === spec.MaskMode.MASK), + occlusion: !!renderer.occlusion, + transparentOcclusion: !!renderer.transparentOcclusion || (renderer.maskMode === spec.MaskMode.MASK), side: renderer.side ?? spec.SideMode.DOUBLE, shape: renderer.shape, mask: renderer.mask ?? 0, @@ -664,8 +323,4 @@ export class SpriteComponent extends RendererComponent { this.material.setVector4('_TexOffset', new Vector4().setFromArray([0, 0, 1, 1])); this.setItem(); } - - override toData (): void { - super.toData(); - } } diff --git a/packages/effects-core/src/plugins/sprite/sprite-loader.ts b/packages/effects-core/src/plugins/sprite/sprite-loader.ts index 28899af68..d0144b841 100644 --- a/packages/effects-core/src/plugins/sprite/sprite-loader.ts +++ b/packages/effects-core/src/plugins/sprite/sprite-loader.ts @@ -2,7 +2,7 @@ import type * as spec from '@galacean/effects-specification'; import type { PrecompileOptions } from '../../plugin-system'; import type { Renderer } from '../../render'; import { createCopyShader } from '../../render'; -import { AbstractPlugin } from '../index'; +import { AbstractPlugin } from '../plugin'; import { maxSpriteMeshItemCount, spriteMeshShaderFromRenderInfo, spriteMeshShaderIdFromRenderInfo } from './sprite-mesh'; const defRenderInfo = { diff --git a/packages/effects-core/src/plugins/sprite/sprite-mesh.ts b/packages/effects-core/src/plugins/sprite/sprite-mesh.ts index 7a279e89d..512ba78fc 100644 --- a/packages/effects-core/src/plugins/sprite/sprite-mesh.ts +++ b/packages/effects-core/src/plugins/sprite/sprite-mesh.ts @@ -5,7 +5,7 @@ import type { GPUCapabilityDetail, ShaderMacros, SharedShaderWithSource } from ' import { GLSLVersion } from '../../render'; import { itemFrag, itemFrameFrag, itemVert } from '../../shader'; import type { Transform } from '../../transform'; -import type { SpriteComponent, SpriteItemRenderInfo } from './sprite-item'; +import type { ItemRenderInfo } from '../../components'; export type SpriteRenderData = { life: number, @@ -35,23 +35,6 @@ export function setSpriteMeshMaxItemCountByGPU (gpuCapability: GPUCapabilityDeta } } -export function getImageItemRenderInfo (item: SpriteComponent): SpriteItemRenderInfo { - const { renderer } = item; - const { blending, side, occlusion, mask, maskMode, order } = renderer; - const blendingCache = +blending; - const cachePrefix = item.cachePrefix || '-'; - - return { - side, - occlusion, - blending, - mask, - maskMode, - cachePrefix, - cacheId: `${cachePrefix}.${+side}+${+occlusion}+${blendingCache}+${order}+${maskMode}.${mask}`, - }; -} - export function spriteMeshShaderFromFilter ( level: number, options?: { wireframe?: boolean, env?: string }, @@ -72,11 +55,11 @@ export function spriteMeshShaderFromFilter ( }; } -export function spriteMeshShaderIdFromRenderInfo (renderInfo: SpriteItemRenderInfo, count: number): string { +export function spriteMeshShaderIdFromRenderInfo (renderInfo: ItemRenderInfo, count: number): string { return `${renderInfo.cachePrefix}_effects_sprite_${count}`; } -export function spriteMeshShaderFromRenderInfo (renderInfo: SpriteItemRenderInfo, count: number, level: number, env?: string): SharedShaderWithSource { +export function spriteMeshShaderFromRenderInfo (renderInfo: ItemRenderInfo, count: number, level: number, env?: string): SharedShaderWithSource { const { wireframe } = renderInfo; const shader = spriteMeshShaderFromFilter(level, { wireframe, diff --git a/packages/effects-core/src/plugins/text/text-item.ts b/packages/effects-core/src/plugins/text/text-item.ts index e7f6d5c42..dafb4e2d6 100644 --- a/packages/effects-core/src/plugins/text/text-item.ts +++ b/packages/effects-core/src/plugins/text/text-item.ts @@ -2,8 +2,6 @@ import * as spec from '@galacean/effects-specification'; import type { Engine } from '../../engine'; import { Texture } from '../../texture'; -import type { SpriteItemProps } from '../sprite/sprite-item'; -import { SpriteComponent } from '../sprite/sprite-item'; import { TextLayout } from './text-layout'; import { TextStyle } from './text-style'; import { glContext } from '../../gl'; @@ -12,6 +10,18 @@ import { canvasPool } from '../../canvas-pool'; import { applyMixins, isValidFontFamily } from '../../utils'; import type { Material } from '../../material'; import type { VFXItem } from '../../vfx-item'; +import { BaseRenderComponent } from '../../components'; + +/** + * 用于创建 textItem 的数据类型, 经过处理后的 spec.TextContentOptions + */ +export interface TextItemProps extends Omit { + listIndex?: number, + renderer: { + mask: number, + texture: Texture, + } & Omit, +} export const DEFAULT_FONTS = [ 'serif', @@ -22,7 +32,7 @@ export const DEFAULT_FONTS = [ interface CharInfo { /** - * 段落y值 + * 段落 y 值 */ y: number, /** @@ -38,24 +48,36 @@ interface CharInfo { export interface TextComponent extends TextComponentBase { } +let seed = 0; + /** * @since 2.0.0 */ @effectsClass(spec.DataType.TextComponent) -export class TextComponent extends SpriteComponent { +export class TextComponent extends BaseRenderComponent { isDirty = true; /** * 文本行数 */ lineCount = 0; + protected readonly SCALE_FACTOR = 0.1; + protected readonly ALPHA_FIX_VALUE = 1 / 255; - constructor (engine: Engine, props?: spec.TextContent) { - super(engine, props as unknown as SpriteItemProps); + constructor (engine: Engine, props?: TextItemProps) { + super(engine); + + this.name = 'MText' + seed++; + this.geometry = this.createGeometry(glContext.TRIANGLES); + + if (props) { + this.fromData(props); + } this.canvas = canvasPool.getCanvas(); canvasPool.saveCanvas(this.canvas); this.context = this.canvas.getContext('2d', { willReadFrequently: true }); + this.setItem(); if (!props) { return; @@ -67,15 +89,35 @@ export class TextComponent extends SpriteComponent { this.updateTexture(); } - override update (dt: number): void { - super.update(dt); + override onUpdate (dt: number): void { + super.onUpdate(dt); this.updateTexture(); } - override fromData (data: SpriteItemProps): void { + override fromData (data: TextItemProps): void { super.fromData(data); - const options = data.options as spec.TextContentOptions; + const { interaction, options, listIndex = 0 } = data; + let renderer = data.renderer; + + if (!renderer) { + renderer = {} as TextItemProps['renderer']; + } + this.interaction = interaction; + + this.renderer = { + renderMode: renderer.renderMode ?? spec.RenderMode.BILLBOARD, + blending: renderer.blending ?? spec.BlendingMode.ALPHA, + texture: renderer.texture ?? this.engine.emptyTexture, + occlusion: !!renderer.occlusion, + transparentOcclusion: !!renderer.transparentOcclusion || (renderer.maskMode === spec.MaskMode.MASK), + side: renderer.side ?? spec.SideMode.DOUBLE, + mask: renderer.mask ?? 0, + maskMode: renderer.maskMode ?? spec.MaskMode.NONE, + order: listIndex, + }; + + this.interaction = interaction; this.updateWithOptions(options); // Text this.updateTexture(); @@ -474,7 +516,7 @@ export class TextComponentBase { //与 toDataURL() 两种方式都需要像素读取操作 const imageData = context.getImageData(0, 0, this.canvas.width, this.canvas.height); - this.material.setTexture('uSampler0', + this.material.setTexture('_MainTex', Texture.createWithData( this.engine, { diff --git a/packages/effects-core/src/plugins/text/text-layout.ts b/packages/effects-core/src/plugins/text/text-layout.ts index 6f5080215..0acb03d76 100644 --- a/packages/effects-core/src/plugins/text/text-layout.ts +++ b/packages/effects-core/src/plugins/text/text-layout.ts @@ -23,7 +23,7 @@ export class TextLayout { lineHeight: number; constructor (options: spec.TextContentOptions) { - const { textHeight = 100, textWidth = 100, textOverflow = spec.TextOverflow.display, textBaseline = spec.TextBaseline.top, textAlign = spec.TextAlignment.left, text, letterSpace = 0, autoWidth = false, fontSize, lineHeight = fontSize } = options; + const { textHeight = 100, textWidth = 100, textOverflow = spec.TextOverflow.display, textBaseline = spec.TextBaseline.top, textAlign = spec.TextAlignment.left, text = ' ', letterSpace = 0, autoWidth = false, fontSize, lineHeight = fontSize } = options; const tempWidth = fontSize + letterSpace; diff --git a/packages/effects-core/src/plugins/timeline/index.ts b/packages/effects-core/src/plugins/timeline/index.ts new file mode 100644 index 000000000..21c26c968 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/index.ts @@ -0,0 +1,3 @@ +export * from './track'; +export * from './tracks'; +export * from './playable-assets'; diff --git a/packages/effects-core/src/plugins/timeline/playable-assets/color-property-playable-asset.ts b/packages/effects-core/src/plugins/timeline/playable-assets/color-property-playable-asset.ts new file mode 100644 index 000000000..11bd603be --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/playable-assets/color-property-playable-asset.ts @@ -0,0 +1,22 @@ +import { effectsClass, serialize } from '../../../decorators'; +import type { Playable, PlayableGraph } from '../../cal/playable-graph'; +import { PlayableAsset } from '../../cal/playable-graph'; +import { PropertyClipPlayable } from '../playables'; +import { createValueGetter } from '../../../math'; +import type { Color } from '@galacean/effects-math/es/core'; +import * as spec from '@galacean/effects-specification'; + +@effectsClass(spec.DataType.ColorPropertyPlayableAsset) +export class ColorPropertyPlayableAsset extends PlayableAsset { + @serialize() + curveData: spec.ColorCurveValue; + + override createPlayable (graph: PlayableGraph): Playable { + const clipPlayable = new PropertyClipPlayable(graph); + + clipPlayable.curve = createValueGetter(this.curveData); + clipPlayable.value = clipPlayable.curve.getValue(0); + + return clipPlayable; + } +} diff --git a/packages/effects-core/src/plugins/timeline/playable-assets/float-property-playable-asset.ts b/packages/effects-core/src/plugins/timeline/playable-assets/float-property-playable-asset.ts new file mode 100644 index 000000000..409642d3d --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/playable-assets/float-property-playable-asset.ts @@ -0,0 +1,22 @@ +import type { FixedNumberExpression } from '@galacean/effects-specification'; +import { effectsClass, serialize } from '../../../decorators'; +import type { Playable, PlayableGraph } from '../../cal/playable-graph'; +import { PlayableAsset } from '../../cal/playable-graph'; +import { PropertyClipPlayable } from '../playables'; +import { createValueGetter } from '../../../math'; +import * as spec from '@galacean/effects-specification'; + +@effectsClass(spec.DataType.FloatPropertyPlayableAsset) +export class FloatPropertyPlayableAsset extends PlayableAsset { + @serialize() + curveData: FixedNumberExpression; + + override createPlayable (graph: PlayableGraph): Playable { + const clipPlayable = new PropertyClipPlayable(graph); + + clipPlayable.curve = createValueGetter(this.curveData); + clipPlayable.value = clipPlayable.curve.getValue(0); + + return clipPlayable; + } +} diff --git a/packages/effects-core/src/plugins/timeline/playable-assets/index.ts b/packages/effects-core/src/plugins/timeline/playable-assets/index.ts new file mode 100644 index 000000000..1c61f96a0 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/playable-assets/index.ts @@ -0,0 +1,5 @@ +export * from './color-property-playable-asset'; +export * from './float-property-playable-asset'; +export * from './sub-composition-playable-asset'; +export * from './timeline-asset'; +export * from './vector4-property-playable-asset'; diff --git a/packages/effects-core/src/plugins/timeline/playables/sub-composition-playable-asset.ts b/packages/effects-core/src/plugins/timeline/playable-assets/sub-composition-playable-asset.ts similarity index 67% rename from packages/effects-core/src/plugins/timeline/playables/sub-composition-playable-asset.ts rename to packages/effects-core/src/plugins/timeline/playable-assets/sub-composition-playable-asset.ts index 6e5a5b179..3ef2494de 100644 --- a/packages/effects-core/src/plugins/timeline/playables/sub-composition-playable-asset.ts +++ b/packages/effects-core/src/plugins/timeline/playable-assets/sub-composition-playable-asset.ts @@ -1,11 +1,12 @@ +import * as spec from '@galacean/effects-specification'; import { effectsClass } from '../../../decorators'; import type { Playable, PlayableGraph } from '../../cal/playable-graph'; import { PlayableAsset } from '../../cal/playable-graph'; -import { SubCompositionClipPlayable } from './sub-composition-clip-playable'; +import { SubCompositionClipPlayable } from '../playables'; -@effectsClass('SubCompositionPlayableAsset') +@effectsClass(spec.DataType.SubCompositionPlayableAsset) export class SubCompositionPlayableAsset extends PlayableAsset { override createPlayable (graph: PlayableGraph): Playable { return new SubCompositionClipPlayable(graph); } -} \ No newline at end of file +} diff --git a/packages/effects-core/src/plugins/timeline/playable-assets/timeline-asset.ts b/packages/effects-core/src/plugins/timeline/playable-assets/timeline-asset.ts new file mode 100644 index 000000000..8701001a3 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/playable-assets/timeline-asset.ts @@ -0,0 +1,192 @@ +import * as spec from '@galacean/effects-specification'; +import { effectsClass, serialize } from '../../../decorators'; +import { VFXItem } from '../../../vfx-item'; +import type { RuntimeClip, TrackAsset } from '../track'; +import { ObjectBindingTrack } from '../../cal/calculate-item'; +import type { FrameContext, PlayableGraph } from '../../cal/playable-graph'; +import { Playable, PlayableAsset, PlayableTraversalMode } from '../../cal/playable-graph'; +import type { Constructor } from '../../../utils'; +import { TrackInstance } from '../track-instance'; + +@effectsClass(spec.DataType.TimelineAsset) +export class TimelineAsset extends PlayableAsset { + @serialize() + tracks: TrackAsset[] = []; + + private cacheFlattenedTracks: TrackAsset[] | null = null; + + get flattenedTracks () { + if (!this.cacheFlattenedTracks) { + this.cacheFlattenedTracks = []; + // flatten track tree + for (const masterTrack of this.tracks) { + this.cacheFlattenedTracks.push(masterTrack); + this.addSubTracksRecursive(masterTrack, this.cacheFlattenedTracks); + } + } + + return this.cacheFlattenedTracks; + } + + override createPlayable (graph: PlayableGraph): Playable { + const timelinePlayable = new TimelinePlayable(graph); + + timelinePlayable.setTraversalMode(PlayableTraversalMode.Passthrough); + + for (const track of this.tracks) { + if (track instanceof ObjectBindingTrack) { + track.create(this); + } + } + + this.sortTracks(this.tracks); + timelinePlayable.compileTracks(graph, this.flattenedTracks); + + return timelinePlayable; + } + + createTrack(classConstructor: Constructor, parent: TrackAsset, name?: string): T { + const newTrack = new classConstructor(this.engine); + + newTrack.name = name ? name : classConstructor.name; + parent.addChild(newTrack); + + this.invalidate(); + + return newTrack; + } + + /** + * Invalidates the asset, called when tracks data changed + */ + private invalidate () { + this.cacheFlattenedTracks = null; + } + + private addSubTracksRecursive (track: TrackAsset, allTracks: TrackAsset[]) { + for (const subTrack of track.getChildTracks()) { + allTracks.push(subTrack); + } + for (const subTrack of track.getChildTracks()) { + this.addSubTracksRecursive(subTrack, allTracks); + } + } + + private sortTracks (tracks: TrackAsset[]) { + const sortedTracks = []; + + for (let i = 0; i < tracks.length; i++) { + sortedTracks.push(new TrackSortWrapper(tracks[i], i)); + } + sortedTracks.sort(compareTracks); + tracks.length = 0; + for (const trackWrapper of sortedTracks) { + tracks.push(trackWrapper.track); + } + } + + override fromData (data: spec.TimelineAssetData): void { + } +} + +export class TimelinePlayable extends Playable { + clips: RuntimeClip[] = []; + masterTrackInstances: TrackInstance[] = []; + + override prepareFrame (context: FrameContext): void { + this.evaluate(); + } + + evaluate () { + const time = this.getTime(); + + // update all tracks binding + this.updateTrackAnimatedObject(this.masterTrackInstances); + + // TODO search active clips + + for (const clip of this.clips) { + clip.evaluateAt(time); + } + } + + compileTracks (graph: PlayableGraph, tracks: TrackAsset[]) { + const outputTrack: TrackAsset[] = tracks; + + // map for searching track instance with track asset guid + const trackInstanceMap: Record = {}; + + for (const track of outputTrack) { + // create track mixer and track output + const trackMixPlayable = track.createPlayableGraph(graph, this.clips); + + this.addInput(trackMixPlayable, 0); + const trackOutput = track.createOutput(); + + trackOutput.setUserData(track.boundObject); + + graph.addOutput(trackOutput); + trackOutput.setSourcePlayable(this, this.getInputCount() - 1); + + // create track instance + const trackInstance = new TrackInstance(track, trackMixPlayable, trackOutput); + + trackInstanceMap[track.getInstanceId()] = trackInstance; + + if (!track.parent) { + this.masterTrackInstances.push(trackInstance); + } + } + + // build trackInstance tree + for (const track of outputTrack) { + const trackInstance = trackInstanceMap[track.getInstanceId()]; + + for (const child of track.getChildTracks()) { + const childTrackInstance = trackInstanceMap[child.getInstanceId()]; + + trackInstance.addChild(childTrackInstance); + } + } + } + + private updateTrackAnimatedObject (trackInstances: TrackInstance[]) { + for (const trackInstance of trackInstances) { + const trackAsset = trackInstance.trackAsset; + + // update track binding use custom method + trackAsset.updateAnimatedObject(); + trackInstance.output.setUserData(trackAsset.boundObject); + + // update children tracks + this.updateTrackAnimatedObject(trackInstance.children); + } + } +} + +export class TrackSortWrapper { + track: TrackAsset; + originalIndex: number; + + constructor (track: TrackAsset, originalIndex: number) { + this.track = track; + this.originalIndex = originalIndex; + } +} + +function compareTracks (a: TrackSortWrapper, b: TrackSortWrapper): number { + const bindingA = a.track.boundObject; + const bindingB = b.track.boundObject; + + if (!(bindingA instanceof VFXItem) || !(bindingB instanceof VFXItem)) { + return a.originalIndex - b.originalIndex; + } + + if (VFXItem.isAncestor(bindingA, bindingB)) { + return -1; + } else if (VFXItem.isAncestor(bindingB, bindingA)) { + return 1; + } else { + return a.originalIndex - b.originalIndex; // 非父子关系的元素保持原始顺序 + } +} diff --git a/packages/effects-core/src/plugins/timeline/playable-assets/vector4-property-playable-asset.ts b/packages/effects-core/src/plugins/timeline/playable-assets/vector4-property-playable-asset.ts new file mode 100644 index 000000000..03ac3b46e --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/playable-assets/vector4-property-playable-asset.ts @@ -0,0 +1,22 @@ +import { effectsClass, serialize } from '../../../decorators'; +import type { Playable, PlayableGraph } from '../../cal/playable-graph'; +import { PlayableAsset } from '../../cal/playable-graph'; +import { PropertyClipPlayable } from '../playables'; +import { createValueGetter } from '../../../math'; +import type { Vector4 } from '@galacean/effects-math/es/core'; +import type * as spec from '@galacean/effects-specification'; + +@effectsClass('Vector4PropertyPlayableAsset') +export class Vector4PropertyPlayableAsset extends PlayableAsset { + @serialize() + curveData: spec.Vector4CurveValue; + + override createPlayable (graph: PlayableGraph): Playable { + const clipPlayable = new PropertyClipPlayable(graph); + + clipPlayable.curve = createValueGetter(this.curveData); + clipPlayable.value = clipPlayable.curve.getValue(0); + + return clipPlayable; + } +} diff --git a/packages/effects-core/src/plugins/timeline/playables/activation-mixer-playable.ts b/packages/effects-core/src/plugins/timeline/playables/activation-mixer-playable.ts index d7175b2fc..6de87478a 100644 --- a/packages/effects-core/src/plugins/timeline/playables/activation-mixer-playable.ts +++ b/packages/effects-core/src/plugins/timeline/playables/activation-mixer-playable.ts @@ -25,26 +25,10 @@ export class ActivationMixerPlayable extends Playable { if (hasInput) { boundItem.transform.setValid(true); - this.showRendererComponents(boundItem); + boundItem.setActive(true); } else { boundItem.transform.setValid(false); - this.hideRendererComponents(boundItem); - } - } - - private hideRendererComponents (item: VFXItem) { - for (const rendererComponent of item.rendererComponents) { - if (rendererComponent.enabled) { - rendererComponent.enabled = false; - } - } - } - - private showRendererComponents (item: VFXItem) { - for (const rendererComponent of item.rendererComponents) { - if (!rendererComponent.enabled) { - rendererComponent.enabled = true; - } + boundItem.setActive(false); } } } diff --git a/packages/effects-core/src/plugins/timeline/playables/color-property-mixer-playable.ts b/packages/effects-core/src/plugins/timeline/playables/color-property-mixer-playable.ts new file mode 100644 index 000000000..13634e6cf --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/playables/color-property-mixer-playable.ts @@ -0,0 +1,54 @@ +import { Color } from '@galacean/effects-math/es/core/color'; +import type { FrameContext } from '../../cal/playable-graph'; +import { Playable } from '../../cal/playable-graph'; +import { PropertyClipPlayable } from './property-clip-playable'; + +export class ColorPropertyMixerPlayable extends Playable { + propertyName = ''; + + override processFrame (context: FrameContext): void { + const boundObject = context.output.getUserData() as Record; + + if (!boundObject) { + return; + } + + let hasInput = false; + const value = boundObject[this.propertyName]; + + if (!(value instanceof Color)) { + return; + } + + value.setZero(); + + // evaluate the curve + for (let i = 0; i < this.getInputCount(); i++) { + const weight = this.getInputWeight(i); + + if (weight > 0) { + const propertyClipPlayable = this.getInput(i) as PropertyClipPlayable; + + if (!(propertyClipPlayable instanceof PropertyClipPlayable)) { + console.error('ColorPropertyMixerPlayable received incompatible input'); + continue; + } + + const curveValue = propertyClipPlayable.value; + + value.r += curveValue.r * weight; + value.g += curveValue.g * weight; + value.b += curveValue.b * weight; + value.a += curveValue.a * weight; + + hasInput = true; + } + } + + // set value + if (hasInput) { + boundObject[this.propertyName] = value; + } + } +} + diff --git a/packages/effects-core/src/plugins/timeline/playables/float-property-mixer-playable.ts b/packages/effects-core/src/plugins/timeline/playables/float-property-mixer-playable.ts new file mode 100644 index 000000000..8f5072886 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/playables/float-property-mixer-playable.ts @@ -0,0 +1,44 @@ +import type { FrameContext } from '../../cal/playable-graph'; +import { Playable } from '../../cal/playable-graph'; +import { PropertyClipPlayable } from './property-clip-playable'; + +export class FloatPropertyMixerPlayable extends Playable { + propertyName = ''; + + override processFrame (context: FrameContext): void { + const boundObject = context.output.getUserData(); + + if (!boundObject) { + return; + } + + let hasInput = false; + let value = 0; + + // evaluate the curve + for (let i = 0; i < this.getInputCount(); i++) { + const weight = this.getInputWeight(i); + + if (weight > 0) { + const propertyClipPlayable = this.getInput(i) as PropertyClipPlayable; + + if (!(propertyClipPlayable instanceof PropertyClipPlayable)) { + console.error('FloatPropertyTrack added non-FloatPropertyPlayableAsset'); + continue; + } + + const curveValue = propertyClipPlayable.value; + + value += curveValue * weight; + + hasInput = true; + } + } + + // set value + if (hasInput) { + (boundObject as Record)[this.propertyName] = value; + } + } +} + diff --git a/packages/effects-core/src/plugins/timeline/playables/index.ts b/packages/effects-core/src/plugins/timeline/playables/index.ts new file mode 100644 index 000000000..f82d4a196 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/playables/index.ts @@ -0,0 +1,7 @@ +export * from './activation-mixer-playable'; +export * from './property-clip-playable'; +export * from './float-property-mixer-playable'; +export * from './sub-composition-clip-playable'; +export * from './sub-composition-mixer-playable'; +export * from './vector4-property-mixer-playable'; +export * from './color-property-mixer-playable'; diff --git a/packages/effects-core/src/plugins/timeline/playables/property-clip-playable.ts b/packages/effects-core/src/plugins/timeline/playables/property-clip-playable.ts new file mode 100644 index 000000000..305bf6f99 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/playables/property-clip-playable.ts @@ -0,0 +1,12 @@ +import type { ValueGetter } from '../../../math'; +import type { FrameContext } from '../../cal/playable-graph'; +import { Playable } from '../../cal/playable-graph'; + +export class PropertyClipPlayable extends Playable { + value: T; + curve: ValueGetter; + + override processFrame (context: FrameContext): void { + this.value = this.curve.getValue(this.time / this.getDuration()); + } +} \ No newline at end of file diff --git a/packages/effects-core/src/plugins/timeline/playables/sub-composition-mixer-playable.ts b/packages/effects-core/src/plugins/timeline/playables/sub-composition-mixer-playable.ts index 222f77efc..1cd8dd225 100644 --- a/packages/effects-core/src/plugins/timeline/playables/sub-composition-mixer-playable.ts +++ b/packages/effects-core/src/plugins/timeline/playables/sub-composition-mixer-playable.ts @@ -23,9 +23,9 @@ export class SubCompositionMixerPlayable extends Playable { } if (hasInput) { - compositionComponent.showItems(); + compositionComponent.item.setActive(true); } else { - compositionComponent.hideItems(); + compositionComponent.item.setActive(false); } } } \ No newline at end of file diff --git a/packages/effects-core/src/plugins/timeline/playables/vector4-property-mixer-playable.ts b/packages/effects-core/src/plugins/timeline/playables/vector4-property-mixer-playable.ts new file mode 100644 index 000000000..076c05b56 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/playables/vector4-property-mixer-playable.ts @@ -0,0 +1,54 @@ +import { Vector4 } from '@galacean/effects-math/es/core/vector4'; +import type { FrameContext } from '../../cal/playable-graph'; +import { Playable } from '../../cal/playable-graph'; +import { PropertyClipPlayable } from './property-clip-playable'; + +export class Vector4PropertyMixerPlayable extends Playable { + propertyName = ''; + + override processFrame (context: FrameContext): void { + const boundObject = context.output.getUserData() as Record; + + if (!boundObject) { + return; + } + + let hasInput = false; + const value = boundObject[this.propertyName]; + + if (!(value instanceof Vector4)) { + return; + } + + value.setZero(); + + // evaluate the curve + for (let i = 0; i < this.getInputCount(); i++) { + const weight = this.getInputWeight(i); + + if (weight > 0) { + const propertyClipPlayable = this.getInput(i) as PropertyClipPlayable; + + if (!(propertyClipPlayable instanceof PropertyClipPlayable)) { + console.error('Vector4PropertyTrack added non-Vector4PropertyPlayableAsset'); + continue; + } + + const curveValue = propertyClipPlayable.value; + + value.x += curveValue.x * weight; + value.y += curveValue.y * weight; + value.z += curveValue.z * weight; + value.w += curveValue.w * weight; + + hasInput = true; + } + } + + // set value + if (hasInput) { + boundObject[this.propertyName] = value; + } + } +} + diff --git a/packages/effects-core/src/plugins/timeline/track-instance.ts b/packages/effects-core/src/plugins/timeline/track-instance.ts new file mode 100644 index 000000000..94de3c8c5 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/track-instance.ts @@ -0,0 +1,24 @@ +import type { Playable, PlayableOutput } from '../cal/playable-graph'; +import type { TrackAsset } from './track'; + +/** + * A class that stores track assets and the generated mixer playables and playable outputs. + * It is used to query the corresponding playable object based on the track asset. + */ +export class TrackInstance { + trackAsset: TrackAsset; + mixer: Playable; + output: PlayableOutput; + + children: TrackInstance[] = []; + + constructor (trackAsset: TrackAsset, mixer: Playable, output: PlayableOutput) { + this.trackAsset = trackAsset; + this.mixer = mixer; + this.output = output; + } + + addChild (trackInstance: TrackInstance) { + this.children.push(trackInstance); + } +} \ No newline at end of file diff --git a/packages/effects-core/src/plugins/timeline/track.ts b/packages/effects-core/src/plugins/timeline/track.ts index 10637579b..a4ffdad09 100644 --- a/packages/effects-core/src/plugins/timeline/track.ts +++ b/packages/effects-core/src/plugins/timeline/track.ts @@ -1,4 +1,4 @@ -import { EndBehavior } from '@galacean/effects-specification'; +import * as spec from '@galacean/effects-specification'; import { effectsClass, serialize } from '../../decorators'; import { VFXItem } from '../../vfx-item'; import type { PlayableGraph } from '../cal/playable-graph'; @@ -15,7 +15,7 @@ export class TimelineClip { start = 0; duration = 0; asset: PlayableAsset; - endBehavior: EndBehavior; + endBehavior: spec.EndBehavior; constructor () { } @@ -25,9 +25,9 @@ export class TimelineClip { const duration = this.duration; if (localTime - duration > 0) { - if (this.endBehavior === EndBehavior.restart) { + if (this.endBehavior === spec.EndBehavior.restart) { localTime = localTime % duration; - } else if (this.endBehavior === EndBehavior.freeze) { + } else if (this.endBehavior === spec.EndBehavior.freeze) { localTime = Math.min(duration, localTime); } } @@ -39,12 +39,13 @@ export class TimelineClip { /** * @since 2.0.0 */ -@effectsClass('TrackAsset') +@effectsClass(spec.DataType.TrackAsset) export class TrackAsset extends PlayableAsset { name: string; - binding: object; - + boundObject: object; + parent: TrackAsset; trackType = TrackType.MasterTrack; + private clipSeed = 0; @serialize(TimelineClip) @@ -56,8 +57,10 @@ export class TrackAsset extends PlayableAsset { /** * 重写该方法以获取自定义对象绑定 */ - resolveBinding (parentBinding: object): object { - return parentBinding; + updateAnimatedObject () { + if (this.parent) { + this.boundObject = this.parent.boundObject; + } } /** @@ -96,6 +99,8 @@ export class TrackAsset extends PlayableAsset { for (const timelineClip of timelineClips) { const clipPlayable = this.createClipPlayable(graph, timelineClip); + clipPlayable.setDuration(timelineClip.duration); + const clip = new RuntimeClip(timelineClip, clipPlayable, mixer, this); runtimeClips.push(clip); @@ -117,6 +122,7 @@ export class TrackAsset extends PlayableAsset { addChild (child: TrackAsset) { this.children.push(child); + child.parent = this; } createClip ( @@ -152,6 +158,13 @@ export class TrackAsset extends PlayableAsset { private createClipPlayable (graph: PlayableGraph, clip: TimelineClip) { return clip.asset.createPlayable(graph); } + + override fromData (data: spec.EffectsObjectData): void { + super.fromData(data); + for (const child of this.children) { + child.parent = this; + } + } } export enum TrackType { @@ -174,8 +187,8 @@ export class RuntimeClip { this.parentMixer = parentMixer; this.track = track; - if (this.track.binding instanceof VFXItem) { - this.particleSystem = this.track.binding.getComponent(ParticleSystem); + if (this.track.boundObject instanceof VFXItem) { + this.particleSystem = this.track.boundObject.getComponent(ParticleSystem); } } @@ -194,9 +207,9 @@ export class RuntimeClip { let weight = 1.0; let ended = false; let started = false; - const boundObject = this.track.binding; + const boundObject = this.track.boundObject; - if (localTime >= clip.start + clip.duration && clip.endBehavior === EndBehavior.destroy) { + if (localTime >= clip.start + clip.duration && clip.endBehavior === spec.EndBehavior.destroy) { if (boundObject instanceof VFXItem && VFXItem.isParticle(boundObject) && this.particleSystem && !this.particleSystem.destroyed) { weight = 1.0; } else { @@ -215,23 +228,16 @@ export class RuntimeClip { } this.parentMixer.setInputWeight(this.playable, weight); + const clipTime = clip.toLocalTime(localTime); + + this.playable.setTime(clipTime); + // 判断动画是否结束 if (ended) { - if (boundObject instanceof VFXItem && !boundObject.ended) { - boundObject.ended = true; - boundObject.onEnd(); - if (!boundObject.compositionReusable && !boundObject.reusable) { - boundObject.dispose(); - this.playable.dispose(); - } - } if (this.playable.getPlayState() === PlayState.Playing) { this.playable.pause(); } } - const clipTime = clip.toLocalTime(localTime); - - this.playable.setTime(clipTime); } } diff --git a/packages/effects-core/src/plugins/timeline/tracks/activation-track.ts b/packages/effects-core/src/plugins/timeline/tracks/activation-track.ts index cbc9524bc..ecd19d945 100644 --- a/packages/effects-core/src/plugins/timeline/tracks/activation-track.ts +++ b/packages/effects-core/src/plugins/timeline/tracks/activation-track.ts @@ -1,11 +1,12 @@ +import * as spec from '@galacean/effects-specification'; import { effectsClass } from '../../../decorators'; import type { PlayableGraph, Playable } from '../../cal/playable-graph'; import { ActivationMixerPlayable } from '../playables/activation-mixer-playable'; import { TrackAsset } from '../track'; -@effectsClass('ActivationTrack') +@effectsClass(spec.DataType.ActivationTrack) export class ActivationTrack extends TrackAsset { override createTrackMixer (graph: PlayableGraph): Playable { return new ActivationMixerPlayable(graph); } -} \ No newline at end of file +} diff --git a/packages/effects-core/src/plugins/timeline/tracks/color-property-track.ts b/packages/effects-core/src/plugins/timeline/tracks/color-property-track.ts new file mode 100644 index 000000000..9bd52de10 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/tracks/color-property-track.ts @@ -0,0 +1,22 @@ +import * as spec from '@galacean/effects-specification'; +import { effectsClass } from '../../../decorators'; +import type { PlayableGraph, Playable } from '../../cal/playable-graph'; +import { ColorPropertyMixerPlayable } from '../playables'; +import { PropertyTrack } from './property-track'; + +@effectsClass(spec.DataType.ColorPropertyTrack) +export class ColorPropertyTrack extends PropertyTrack { + override createTrackMixer (graph: PlayableGraph): Playable { + const mixer = new ColorPropertyMixerPlayable(graph); + + const propertyNames = this.propertyNames; + + if (propertyNames.length > 0) { + const propertyName = propertyNames[propertyNames.length - 1]; + + mixer.propertyName = propertyName; + } + + return mixer; + } +} \ No newline at end of file diff --git a/packages/effects-core/src/plugins/timeline/tracks/float-property-track.ts b/packages/effects-core/src/plugins/timeline/tracks/float-property-track.ts new file mode 100644 index 000000000..03e0360e8 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/tracks/float-property-track.ts @@ -0,0 +1,39 @@ +import * as spec from '@galacean/effects-specification'; +import { effectsClass } from '../../../decorators'; +import type { PlayableGraph, Playable } from '../../cal/playable-graph'; +import { FloatPropertyMixerPlayable } from '../playables'; +import { PropertyTrack } from './property-track'; + +@effectsClass(spec.DataType.FloatPropertyTrack) +export class FloatPropertyTrack extends PropertyTrack { + + override createTrackMixer (graph: PlayableGraph): Playable { + const mixer = new FloatPropertyMixerPlayable(graph); + + const propertyNames = this.propertyNames; + + if (propertyNames.length > 0) { + const propertyName = propertyNames[propertyNames.length - 1]; + + mixer.propertyName = propertyName; + } + + return mixer; + } + + override updateAnimatedObject () { + const propertyNames = this.propertyNames; + let target: Record = this.parent.boundObject; + + for (let i = 0; i < propertyNames.length - 1; i++) { + const property = target[propertyNames[i]]; + + if (property === undefined) { + console.error('The ' + propertyNames[i] + ' property of ' + target + ' was not found'); + } + target = property; + } + + this.boundObject = target; + } +} diff --git a/packages/effects-core/src/plugins/timeline/tracks/index.ts b/packages/effects-core/src/plugins/timeline/tracks/index.ts new file mode 100644 index 000000000..c479fe406 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/tracks/index.ts @@ -0,0 +1,9 @@ +export * from './activation-track'; +export * from './float-property-track'; +export * from './sprite-color-track'; +export * from './sub-composition-track'; +export * from './transform-track'; +export * from './material-track'; +export * from './property-track'; +export * from './vector4-property-track'; +export * from './color-property-track'; diff --git a/packages/effects-core/src/plugins/timeline/tracks/material-track.ts b/packages/effects-core/src/plugins/timeline/tracks/material-track.ts new file mode 100644 index 000000000..0e3566bc8 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/tracks/material-track.ts @@ -0,0 +1,18 @@ +import { RendererComponent } from '../../../components'; +import { effectsClass, serialize } from '../../../decorators'; +import { TrackAsset } from '../track'; + +@effectsClass('MaterialTrack') +export class MaterialTrack extends TrackAsset { + + @serialize() + index: number; + + override updateAnimatedObject () { + if (!(this.parent.boundObject instanceof RendererComponent)) { + return; + } + this.parent.boundObject; + this.boundObject = this.parent.boundObject.materials[this.index]; + } +} diff --git a/packages/effects-core/src/plugins/timeline/tracks/property-track.ts b/packages/effects-core/src/plugins/timeline/tracks/property-track.ts new file mode 100644 index 000000000..fd7c59d75 --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/tracks/property-track.ts @@ -0,0 +1,34 @@ +import type { EffectsObjectData } from '@galacean/effects-specification'; +import { serialize } from '../../../decorators'; +import { TrackAsset } from '../track'; + +export class PropertyTrack extends TrackAsset { + + protected propertyNames: string[] = []; + + @serialize() + private path = ''; + + override updateAnimatedObject () { + const propertyNames = this.propertyNames; + let target: Record = this.parent.boundObject; + + for (let i = 0; i < propertyNames.length - 1; i++) { + const property = target[propertyNames[i]]; + + if (property === undefined) { + console.error('The ' + propertyNames[i] + ' property of ' + target + ' was not found'); + } + target = property; + } + + this.boundObject = target; + } + + override fromData (data: EffectsObjectData): void { + super.fromData(data); + const propertyNames = this.path.split('.'); + + this.propertyNames = propertyNames; + } +} \ No newline at end of file diff --git a/packages/effects-core/src/plugins/timeline/tracks/sprite-color-track.ts b/packages/effects-core/src/plugins/timeline/tracks/sprite-color-track.ts index cb6381951..86e64c5d5 100644 --- a/packages/effects-core/src/plugins/timeline/tracks/sprite-color-track.ts +++ b/packages/effects-core/src/plugins/timeline/tracks/sprite-color-track.ts @@ -1,7 +1,8 @@ +import * as spec from '@galacean/effects-specification'; import { effectsClass } from '../../../decorators'; import { TrackAsset } from '../track'; -@effectsClass('SpriteColorTrack') +@effectsClass(spec.DataType.SpriteColorTrack) export class SpriteColorTrack extends TrackAsset { -} \ No newline at end of file +} diff --git a/packages/effects-core/src/plugins/timeline/tracks/sub-composition-track.ts b/packages/effects-core/src/plugins/timeline/tracks/sub-composition-track.ts index 966d11316..0ff980cf0 100644 --- a/packages/effects-core/src/plugins/timeline/tracks/sub-composition-track.ts +++ b/packages/effects-core/src/plugins/timeline/tracks/sub-composition-track.ts @@ -1,19 +1,19 @@ +import * as spec from '@galacean/effects-specification'; import { VFXItem } from '../../../vfx-item'; import { CompositionComponent } from '../../../comp-vfx-item'; import { TrackAsset } from '../track'; import { effectsClass } from '../../../decorators'; import type { PlayableGraph, Playable } from '../../cal/playable-graph'; -import { SubCompositionMixerPlayable } from '../playables/sub-composition-mixer-playable'; +import { SubCompositionMixerPlayable } from '../playables'; -@effectsClass('SubCompositionTrack') +@effectsClass(spec.DataType.SubCompositionTrack) export class SubCompositionTrack extends TrackAsset { - override resolveBinding (parentBinding: object): object { - if (!(parentBinding instanceof VFXItem)) { + override updateAnimatedObject () { + if (!this.parent || !(this.parent.boundObject instanceof VFXItem)) { throw new Error('SubCompositionTrack needs to be set under the VFXItem track.'); } - - return parentBinding.getComponent(CompositionComponent); + this.boundObject = this.parent.boundObject.getComponent(CompositionComponent); } override createTrackMixer (graph: PlayableGraph): Playable { diff --git a/packages/effects-core/src/plugins/timeline/tracks/transform-track.ts b/packages/effects-core/src/plugins/timeline/tracks/transform-track.ts index 2186a6c7c..38651f4a9 100644 --- a/packages/effects-core/src/plugins/timeline/tracks/transform-track.ts +++ b/packages/effects-core/src/plugins/timeline/tracks/transform-track.ts @@ -1,7 +1,8 @@ +import * as spec from '@galacean/effects-specification'; import { effectsClass } from '../../../decorators'; import { TrackAsset } from '../track'; -@effectsClass('TransformTrack') +@effectsClass(spec.DataType.TransformTrack) export class TransformTrack extends TrackAsset { -} \ No newline at end of file +} diff --git a/packages/effects-core/src/plugins/timeline/tracks/vector4-property-track.ts b/packages/effects-core/src/plugins/timeline/tracks/vector4-property-track.ts new file mode 100644 index 000000000..ee7f3e45f --- /dev/null +++ b/packages/effects-core/src/plugins/timeline/tracks/vector4-property-track.ts @@ -0,0 +1,22 @@ +import * as spec from '@galacean/effects-specification'; +import { effectsClass } from '../../../decorators'; +import type { PlayableGraph, Playable } from '../../cal/playable-graph'; +import { Vector4PropertyMixerPlayable } from '../playables'; +import { PropertyTrack } from './property-track'; + +@effectsClass(spec.DataType.Vector4PropertyTrack) +export class Vector4PropertyTrack extends PropertyTrack { + override createTrackMixer (graph: PlayableGraph): Playable { + const mixer = new Vector4PropertyMixerPlayable(graph); + + const propertyNames = this.propertyNames; + + if (propertyNames.length > 0) { + const propertyName = propertyNames[propertyNames.length - 1]; + + mixer.propertyName = propertyName; + } + + return mixer; + } +} \ No newline at end of file diff --git a/packages/effects-core/src/render/global-volume.ts b/packages/effects-core/src/render/global-volume.ts deleted file mode 100644 index a1c23f81c..000000000 --- a/packages/effects-core/src/render/global-volume.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 后处理配置 - */ -export interface PostProcessVolumeData { - useHDR: boolean, - // Bloom - useBloom: boolean, - threshold: number, - bloomIntensity: number, - // ColorAdjustments - brightness: number, - saturation: number, - contrast: number, - // Vignette - // vignetteColor: Color, - // vignetteCenter: Vector2, - vignetteIntensity: number, - vignetteSmoothness: number, - vignetteRoundness: number, - // ToneMapping - useToneMapping: boolean, // 1: true, 0: false -} - -export const defaultGlobalVolume: PostProcessVolumeData = { - useHDR: false, - /***** Material Uniform *****/ - // Bloom - useBloom: true, - threshold: 1.0, - bloomIntensity: 1.0, - // ColorAdjustments - brightness: 1.0, - saturation: 1.0, - contrast: 1.0, - // Vignette - // vignetteColor: new math.Color(0, 0, 0, 1), - // vignetteCenter: new math.Vector2(0.5, 0.5), - vignetteIntensity: 0.2, - vignetteSmoothness: 0.4, - vignetteRoundness: 1.0, - // ToneMapping - useToneMapping: true, // 1: true, 0: false -}; diff --git a/packages/effects-core/src/render/gpu-capability.ts b/packages/effects-core/src/render/gpu-capability.ts index 2b2fab77f..533e7df98 100644 --- a/packages/effects-core/src/render/gpu-capability.ts +++ b/packages/effects-core/src/render/gpu-capability.ts @@ -19,7 +19,7 @@ export interface GPUCapabilityDetail { shaderTextureLod: boolean, instanceDraw?: boolean, drawBuffers?: boolean, - asyncShaderCompile?: boolean, + asyncShaderCompile: boolean, //draw elements use uint32 Array intIndexElementBuffer?: boolean, //render pass depth and stencil texture readable @@ -100,7 +100,7 @@ export class GPUCapability { shaderTextureLod: level2 || !!gl.getExtension('EXT_shader_texture_lod'), instanceDraw: level2 || !!gl.getExtension('ANGLE_instanced_arrays'), drawBuffers: level2 || !!this.drawBufferExtension, - asyncShaderCompile: !!gl.getExtension('KHR_parallel_shader_compile'), + asyncShaderCompile: !!this.glAsyncCompileExt, intIndexElementBuffer: !!gl.getExtension('OES_element_index_uint'), standardDerivatives: level2 || !!gl.getExtension('OES_standard_derivatives'), readableDepthStencilTextures: level2 || !!depthTextureExtension, diff --git a/packages/effects-core/src/render/index.ts b/packages/effects-core/src/render/index.ts index eae570a5f..4fe803fc1 100644 --- a/packages/effects-core/src/render/index.ts +++ b/packages/effects-core/src/render/index.ts @@ -9,5 +9,4 @@ export * from './types'; export * from './geometry'; export * from './framebuffer'; export * from './renderer'; -export * from './global-volume'; export * from './semantic-map'; diff --git a/packages/effects-core/src/render/mesh.ts b/packages/effects-core/src/render/mesh.ts index c1f89d9a2..6e40214ce 100644 --- a/packages/effects-core/src/render/mesh.ts +++ b/packages/effects-core/src/render/mesh.ts @@ -4,7 +4,7 @@ import type { Material, MaterialDestroyOptions } from '../material'; import type { Geometry, Renderer } from '../render'; import type { Disposable } from '../utils'; import { DestroyOptions } from '../utils'; -import { RendererComponent } from '../components/renderer-component'; +import { RendererComponent } from '../components'; export interface MeshOptionsBase { material: Material, diff --git a/packages/effects-core/src/render/post-process-pass.ts b/packages/effects-core/src/render/post-process-pass.ts index f83fc9ee9..41063c620 100644 --- a/packages/effects-core/src/render/post-process-pass.ts +++ b/packages/effects-core/src/render/post-process-pass.ts @@ -13,6 +13,7 @@ import type { ShaderWithSource } from './shader'; import { colorGradingFrag, gaussianDownHFrag, gaussianDownVFrag, gaussianUpFrag, screenMeshVert, thresholdFrag } from '../shader'; import { Vector2 } from '@galacean/effects-math/es/core/vector2'; import { Vector3 } from '@galacean/effects-math/es/core/vector3'; +import type * as spec from '@galacean/effects-specification'; // Bloom 阈值 Pass export class BloomThresholdPass extends RenderPass { @@ -68,7 +69,7 @@ export class BloomThresholdPass extends RenderPass { stencilAction: TextureStoreAction.clear, }); this.screenMesh.material.setTexture('_MainTex', this.mainTexture); - const threshold = renderer.renderingData.currentFrame.globalVolume.threshold; + const threshold = renderer.renderingData.currentFrame.globalVolume?.bloom?.threshold ?? 1.0; this.screenMesh.material.setFloat('_Threshold', threshold); renderer.renderMeshes([this.screenMesh]); @@ -263,31 +264,55 @@ export class ToneMappingPass extends RenderPass { depthAction: TextureStoreAction.clear, stencilAction: TextureStoreAction.clear, }); - const { - useBloom, bloomIntensity, - brightness, saturation, contrast, - useToneMapping, - vignetteIntensity, vignetteSmoothness, vignetteRoundness, - } = renderer.renderingData.currentFrame.globalVolume; + const globalVolume = renderer.renderingData.currentFrame.globalVolume; + + const bloom: spec.Bloom = { + threshold: 0, + intensity: 0, + active: false, + ...globalVolume?.bloom, + }; + + const vignette: spec.Vignette = { + intensity: 0, + smoothness: 0, + roundness: 0, + active: false, + ...globalVolume?.vignette, + }; + + const colorAdjustments: spec.ColorAdjustments = { + brightness: 0, + saturation: 0, + contrast: 0, + active: false, + ...globalVolume?.colorAdjustments, + }; + + const tonemapping: spec.Tonemapping = { + active: false, + ...globalVolume?.tonemapping, + }; this.screenMesh.material.setTexture('_SceneTex', this.sceneTextureHandle.texture); - this.screenMesh.material.setFloat('_Brightness', brightness); - this.screenMesh.material.setFloat('_Saturation', saturation); - this.screenMesh.material.setFloat('_Contrast', contrast); - this.screenMesh.material.setInt('_UseBloom', Number(useBloom)); - if (useBloom) { + this.screenMesh.material.setFloat('_Brightness', Math.pow(2, colorAdjustments.brightness)); + this.screenMesh.material.setFloat('_Saturation', (colorAdjustments.saturation * 0.01) + 1); + this.screenMesh.material.setFloat('_Contrast', (colorAdjustments.contrast * 0.01) + 1); + + this.screenMesh.material.setInt('_UseBloom', Number(bloom.active)); + if (bloom.active) { this.screenMesh.material.setTexture('_GaussianTex', this.mainTexture); - this.screenMesh.material.setFloat('_BloomIntensity', bloomIntensity); + this.screenMesh.material.setFloat('_BloomIntensity', bloom.intensity); } - if (vignetteIntensity > 0) { - this.screenMesh.material.setFloat('_VignetteIntensity', vignetteIntensity); - this.screenMesh.material.setFloat('_VignetteSmoothness', vignetteSmoothness); - this.screenMesh.material.setFloat('_VignetteRoundness', vignetteRoundness); + if (vignette.intensity > 0) { + this.screenMesh.material.setFloat('_VignetteIntensity', vignette.intensity); + this.screenMesh.material.setFloat('_VignetteSmoothness', vignette.smoothness); + this.screenMesh.material.setFloat('_VignetteRoundness', vignette.roundness); this.screenMesh.material.setVector2('_VignetteCenter', new Vector2(0.5, 0.5)); this.screenMesh.material.setVector3('_VignetteColor', new Vector3(0.0, 0.0, 0.0)); } - this.screenMesh.material.setInt('_UseToneMapping', Number(useToneMapping)); + this.screenMesh.material.setInt('_UseToneMapping', Number(tonemapping.active)); renderer.renderMeshes([this.screenMesh]); } } diff --git a/packages/effects-core/src/render/render-frame.ts b/packages/effects-core/src/render/render-frame.ts index d8fdf7674..77afcb2c8 100644 --- a/packages/effects-core/src/render/render-frame.ts +++ b/packages/effects-core/src/render/render-frame.ts @@ -29,6 +29,7 @@ import { BloomThresholdPass, HQGaussianDownSamplePass, HQGaussianUpSamplePass, ToneMappingPass, } from './post-process-pass'; import type { PostProcessVolume, RendererComponent } from '../components'; +import type { Vector3 } from '@galacean/effects-math/es/core/vector3'; /** * 渲染数据,保存了当前渲染使用到的数据。 @@ -145,6 +146,10 @@ export interface RenderFrameOptions { * 后处理渲染配置 */ globalVolume?: PostProcessVolume, + /** + * 后处理是否开启 + */ + postProcessingEnabled?: boolean, /** * 名称 */ @@ -186,7 +191,7 @@ export class RenderFrame implements Disposable { /** * 存放后处理的属性设置 */ - globalVolume: PostProcessVolume; + globalVolume?: PostProcessVolume; renderer: Renderer; resource: RenderFrameResource; keepColorBuffer?: boolean; @@ -216,11 +221,14 @@ export class RenderFrame implements Disposable { protected destroyed = false; protected renderPassInfoMap: WeakMap = new WeakMap(); + private drawObjectPass: RenderPass; + constructor (options: RenderFrameOptions) { const { camera, keepColorBuffer, renderer, editorTransform = [1, 1, 0, 0], globalVolume, + postProcessingEnabled = false, clearAction = { colorAction: TextureLoadAction.whatever, stencilAction: TextureLoadAction.clear, @@ -238,10 +246,15 @@ export class RenderFrame implements Disposable { let drawObjectPassClearAction = {}; this.renderer = renderer; - if (this.globalVolume) { - const { useHDR } = this.globalVolume; + if (postProcessingEnabled) { + const enableHDR = true; + + if (!this.renderer.engine.gpuCapability.detail.halfFloatTexture) { + throw new Error('Half float texture is not supported.'); + } + // 使用HDR浮点纹理,FLOAT在IOS上报错,使用HALF_FLOAT - const textureType = useHDR ? glContext.HALF_FLOAT : glContext.UNSIGNED_BYTE; + const textureType = enableHDR ? glContext.HALF_FLOAT : glContext.UNSIGNED_BYTE; attachments = [{ texture: { format: glContext.RGBA, type: textureType, magFilter: glContext.LINEAR, minFilter: glContext.LINEAR } }]; depthStencilAttachment = { storageType: RenderPassAttachmentStorageType.depth_stencil_opaque }; @@ -252,28 +265,28 @@ export class RenderFrame implements Disposable { }; } - // 创建 drawObjectPass - const renderPasses = [ - new RenderPass(renderer, { - name: RENDER_PASS_NAME_PREFIX, - priority: RenderPassPriorityNormal, - meshOrder: OrderType.ascending, - depthStencilAttachment, - attachments, - clearAction: drawObjectPassClearAction, - }), - ]; + this.drawObjectPass = new RenderPass(renderer, { + name: RENDER_PASS_NAME_PREFIX, + priority: RenderPassPriorityNormal, + meshOrder: OrderType.ascending, + depthStencilAttachment, + attachments, + clearAction: drawObjectPassClearAction, + }); + + const renderPasses = [this.drawObjectPass]; this.setRenderPasses(renderPasses); - if (this.globalVolume) { + if (postProcessingEnabled) { const sceneTextureHandle = new RenderTargetHandle(engine); //保存后处理前的屏幕图像 const gaussianStep = 7; // 高斯模糊的迭代次数,次数越高模糊范围越大 const viewport: vec4 = [0, 0, this.renderer.getWidth() / 2, this.renderer.getHeight() / 2]; const gaussianDownResults = new Array(gaussianStep); //存放多个高斯Pass的模糊结果,用于Bloom - const textureType = this.globalVolume.useHDR ? glContext.HALF_FLOAT : glContext.UNSIGNED_BYTE; + const enableHDR = true; + const textureType = enableHDR ? glContext.HALF_FLOAT : glContext.UNSIGNED_BYTE; const bloomThresholdPass = new BloomThresholdPass(renderer, { name: 'BloomThresholdPass', attachments: [{ @@ -387,48 +400,8 @@ export class RenderFrame implements Disposable { * 根据 Mesh 优先级添加到 RenderPass * @param mesh - 要添加的 Mesh 对象 */ - addMeshToDefaultRenderPass (mesh?: RendererComponent) { - if (!mesh) { - return; - } - - this.renderPasses[0].addMesh(mesh); - - // const renderPasses = this.renderPasses; - // const infoMap = this.renderPassInfoMap; - // const { priority } = mesh; - - // for (let i = 1; i < renderPasses.length; i++) { - // const renderPass = renderPasses[i - 1]; - // const info = infoMap.get(renderPasses[i])!; - - // if (info && info.listStart > priority && (priority > infoMap.get(renderPass)!.listEnd || i === 1)) { - // return this.addToRenderPass(renderPass, mesh); - // } - // } - // // TODO: diff逻辑待优化,有时会添加进找不到的元素 - // let last = renderPasses[renderPasses.length - 1]; - - // // TODO: 是否添加mesh到pass的判断方式需要优化,先通过长度判断是否有postprocess - // for (const pass of renderPasses) { - // if (!(pass instanceof HQGaussianDownSamplePass - // || pass instanceof BloomThresholdPass - // || pass instanceof ToneMappingPass - // || pass instanceof HQGaussianUpSamplePass - // || pass.name === 'mars-final-copy')) { - // last = pass; - // } - // } - - // // if (priority > infoMap.get(last)!.listStart || renderPasses.length === 1) { - // // return this.addToRenderPass(last, mesh); - // // } - - // return this.addToRenderPass(last, mesh); - - // if (__DEBUG__) { - // throw Error('render pass not found'); - // } + addMeshToDefaultRenderPass (mesh: RendererComponent) { + this.drawObjectPass.addMesh(mesh); } /** @@ -436,206 +409,10 @@ export class RenderFrame implements Disposable { * 如果 renderPass 中没有 mesh,此 renderPass 会被删除 * @param mesh - 要删除的 Mesh 对象 */ - removeMeshFromDefaultRenderPass (mesh: Mesh) { - // const renderPasses = this.renderPasses; - // const infoMap = this.renderPassInfoMap; - - // for (let i = renderPasses.length - 1; i >= 0; i--) { - // const renderPass = renderPasses[i]; - // const info = infoMap.get(renderPass)!; - - // // 只有渲染场景物体的pass才有 info - // if (!info) { - // continue; - // } - - // if (info.listStart <= mesh.priority && info.listEnd >= mesh.priority) { - // const idx = renderPass.meshes.indexOf(mesh); - - // if (idx === -1) { - // return; - // } - - // // TODO hack: 现在的除了rp1和finalcopy pass,所有renderpass的meshes是一个copy加上一个filter mesh,这里的判断当filter mesh被删除后当前pass需不需要删除, - // // 判断需要更鲁棒。 - // const shouldRestoreRenderPass = idx === 1 && renderPass.meshes[0].name === MARS_COPY_MESH_NAME; - - // renderPass.removeMesh(mesh); - // if (shouldRestoreRenderPass) { - // const nextRenderPass = renderPasses[i + 1]; - // const meshes = renderPass.meshes; - - // if (!info.intermedia) { - // info.preRenderPass?.resetColorAttachments([]); - // //this.renderer.extension.resetColorAttachments?.(info.preRenderPass, []); - // } - // for (let j = 1; j < meshes.length; j++) { - // info.preRenderPass?.addMesh(meshes[j]); - // } - // const cp = renderPass.attachments[0]?.texture; - // const keepColor = cp === this.resource.color_a || cp === this.resource.color_b; - - // renderPass.dispose({ - // meshes: DestroyOptions.keep, - // colorAttachment: keepColor ? RenderPassDestroyAttachmentType.keep : RenderPassDestroyAttachmentType.destroy, - // depthStencilAttachment: RenderPassDestroyAttachmentType.keep, - // }); - // removeItem(renderPasses, renderPass); - // this.removeRenderPass(renderPass); - // infoMap.delete(renderPass); - // if (nextRenderPass) { - // this.updateRenderInfo(nextRenderPass); - // } - // if (info.preRenderPass) { - // this.updateRenderInfo(info.preRenderPass); - // } - // if (info.prePasses) { - // info.prePasses.forEach(rp => { - // this.removeRenderPass(rp.pass); - // if (rp?.destroyOptions !== false) { - // rp.pass.attachments.forEach(c => { - // if (c.texture !== this.resource.color_b || c.texture !== this.resource.color_a) { - // c.texture.dispose(); - // } - // }); - // const options: RenderPassDestroyOptions = { - // ...(rp?.destroyOptions ? rp.destroyOptions as RenderPassDestroyOptions : {}), - // depthStencilAttachment: RenderPassDestroyAttachmentType.keep, - // }; - - // rp.pass.dispose(options); - // } - // }); - // } - // this.resetRenderPassDefaultAttachment(renderPasses, Math.max(i - 1, 0)); - // if (renderPasses.length === 1) { - // renderPasses[0].resetColorAttachments([]); - // //this.renderer.extension.resetColorAttachments?.(renderPasses[0], []); - // this.removeRenderPass(this.resource.finalCopyRP); - // } - // } - - // return this.resetClearActions(); - // } - // } + removeMeshFromDefaultRenderPass (mesh: RendererComponent) { + this.drawObjectPass.removeMesh(mesh); } - // /** - // * 将 Mesh 所有在 RenderPass 进行切分 - // * @param mesh - 目标 Mesh 对象 - // * @param options - 切分选项,包含 RenderPass 相关的 Attachment 等数据 - // */ - // splitDefaultRenderPassByMesh (mesh: Mesh, options: RenderPassSplitOptions): RenderPass { - // const index = this.findMeshRenderPassIndex(mesh); - // const renderPass = this.renderPasses[index]; - - // if (__DEBUG__) { - // if (!renderPass) { - // throw Error('RenderPassNotFound'); - // } - // } - // this.createResource(); - // const meshIndex = renderPass.meshes.indexOf(mesh); - // const ms0 = renderPass.meshes.slice(0, meshIndex); - // const ms1 = renderPass.meshes.slice(meshIndex); - // const infoMap = this.renderPassInfoMap; - - // // TODO 为什么要加这个判断? - // // if (renderPass.attachments[0] && this.renderPasses[index + 1] !== this.resource.finalCopyRP) { - // // throw Error('not implement'); - // // } else { - // if (!options.attachments?.length) { - // throw Error('should include at least one color attachment'); - // } - // const defRPS = this.renderPasses; - // const defIndex = defRPS.indexOf(renderPass); - // const lastDefRP = defRPS[defIndex - 1]; - - // removeItem(defRPS, renderPass); - // const lastInfo = infoMap.get(renderPass); - - // infoMap.delete(renderPass); - // const filter = GPUCapability.getInstance().level === 2 ? glContext.LINEAR : glContext.NEAREST; - // const rp0 = new RenderPass({ - // name: RENDER_PASS_NAME_PREFIX + defIndex, - // priority: renderPass.priority, - // attachments: [{ - // texture: { - // sourceType: TextureSourceType.framebuffer, - // format: glContext.RGBA, - // name: 'frame_a', - // minFilter: filter, - // magFilter: filter, - // }, - // }], - // clearAction: renderPass.clearAction || { colorAction: TextureLoadAction.clear }, - // storeAction: renderPass.storeAction, - // depthStencilAttachment: this.resource.depthStencil, - // meshes: ms0, - // meshOrder: OrderType.ascending, - // }); - - // ms1.unshift(this.createCopyMesh()); - - // const renderPasses = this.renderPasses; - - // renderPasses[index] = rp0; - // const prePasses: RenderPass[] = []; - - // const restMeshes = ms1.slice(); - - // if (options.prePasses) { - // options.prePasses.forEach((pass, i) => { - // pass.priority = renderPass.priority + 1 + i; - // pass.setMeshes(ms1); - // prePasses.push(pass); - // }); - // renderPasses.splice(index + 1, 0, ...prePasses); - // restMeshes.splice(0, 2); - // } - // const copyRP = this.resource.finalCopyRP; - - // if (!renderPasses.includes(copyRP)) { - // renderPasses.push(copyRP); - // } - // // let sourcePass = (prePasses.length && !options.useLastDefaultPassColor) ? prePasses[prePasses.length - 1] : rp0; - - // const finalFilterPass = prePasses[prePasses.length - 1]; - - // finalFilterPass.initialize(this.renderer); - - // // 不切RT,接着上一个pass的渲染结果渲染 - // const rp1 = new RenderPass({ - // name: RENDER_PASS_NAME_PREFIX + (defIndex + 1), - // priority: renderPass.priority + 1 + (options.prePasses?.length || 0), - // meshes: restMeshes, - // meshOrder: OrderType.ascending, - // depthStencilAttachment: this.resource.depthStencil, - // storeAction: options.storeAction, - // clearAction: { - // depthAction: TextureLoadAction.whatever, - // stencilAction: TextureLoadAction.whatever, - // colorAction: TextureLoadAction.whatever, - // }, - // }); - - // renderPasses.splice(index + 1 + (options.prePasses?.length || 0), 0, rp1); - // this.setRenderPasses(renderPasses); - // this.updateRenderInfo(finalFilterPass); - // this.updateRenderInfo(rp0); - // this.updateRenderInfo(rp1); - - // // 目的是删除滤镜元素后,把之前滤镜用到的prePass给删除,逻辑有些复杂,考虑优化 - // infoMap.get(rp0)!.prePasses = lastInfo!.prePasses; - // prePasses.pop(); - // infoMap.get(finalFilterPass)!.prePasses = prePasses.map((pass, i) => { - // return { pass, destroyOptions: false }; - // }); - // this.resetClearActions(); - - // return finalFilterPass; - // } - /** * 销毁 RenderFrame * @param options - 可以有选择销毁一些对象 @@ -769,50 +546,6 @@ export class RenderFrame implements Disposable { } } - // protected updateRenderInfo (renderPass: RenderPass): RenderPassInfo { - // const map = this.renderPassInfoMap; - // const passes = this.renderPasses; - // let info: RenderPassInfo; - - // if (!map.has(renderPass)) { - // info = { - // intermedia: false, - // renderPass: renderPass, - // listStart: 0, - // listEnd: 0, - // }; - // map.set(renderPass, info); - // } else { - // info = map.get(renderPass)!; - // } - // info.intermedia = renderPass.attachments.length > 0; - // const meshes = renderPass.meshes; - - // if (meshes[0]) { - // info.listStart = (meshes[0].name === MARS_COPY_MESH_NAME ? meshes[1] : meshes[0]).priority; - // info.listEnd = meshes[meshes.length - 1].priority; - // } else { - // info.listStart = 0; - // info.listEnd = 0; - // } - // const index = passes.indexOf(renderPass); - // const depthStencilActon = index === 0 ? TextureLoadAction.clear : TextureLoadAction.whatever; - - // if (index === 0) { - // renderPass.clearAction.colorAction = TextureLoadAction.clear; - // } - // renderPass.clearAction.depthAction = depthStencilActon; - // renderPass.clearAction.stencilAction = depthStencilActon; - // if (index > -1) { - // renderPass.semantics.setSemantic('EDITOR_TRANSFORM', () => this.editorTransform); - // } else { - // renderPass.semantics.setSemantic('EDITOR_TRANSFORM', undefined); - // } - // info.preRenderPass = passes[index - 1]; - - // return info; - // } - /** * 设置 RenderPass 数组,直接修改内部的 RenderPass 数组 * @param passes - RenderPass 数组 @@ -1001,7 +734,7 @@ class FinalCopyRP extends RenderPass { export class GlobalUniforms { floats: Record = {}; ints: Record = {}; - // vector3s: Record = {}; + vector3s: Record = {}; vector4s: Record = {}; matrices: Record = {}; //... diff --git a/packages/effects-core/src/render/renderer.ts b/packages/effects-core/src/render/renderer.ts index 6164f8a9a..6c2b95230 100644 --- a/packages/effects-core/src/render/renderer.ts +++ b/packages/effects-core/src/render/renderer.ts @@ -1,5 +1,5 @@ -import type { Matrix4, Vector4 } from '@galacean/effects-math/es/core/index'; -import type { RendererComponent } from '../components/renderer-component'; +import type { Matrix4, Vector3, Vector4 } from '@galacean/effects-math/es/core/index'; +import type { RendererComponent } from '../components'; import type { Engine } from '../engine'; import type { Material } from '../material'; import type { LostHandler, RestoreHandler } from '../utils'; @@ -41,6 +41,10 @@ export class Renderer implements LostHandler, RestoreHandler { // OVERRIDE } + setGlobalVector3 (name: string, value: Vector3) { + // OVERRIDE + } + setGlobalMatrix (name: string, value: Matrix4) { // OVERRIDE } diff --git a/packages/effects-core/src/render/semantic-map.ts b/packages/effects-core/src/render/semantic-map.ts index 73a577ddd..9acb92fe8 100644 --- a/packages/effects-core/src/render/semantic-map.ts +++ b/packages/effects-core/src/render/semantic-map.ts @@ -29,7 +29,7 @@ export class SemanticMap implements Disposable { const ret = this.semantics[name]; if (isFunction(ret)) { - return (ret as SemanticFunc)(state); + return ret(state); } return ret; diff --git a/packages/effects-core/src/render/shader.ts b/packages/effects-core/src/render/shader.ts index f1d80cf86..af7d65857 100644 --- a/packages/effects-core/src/render/shader.ts +++ b/packages/effects-core/src/render/shader.ts @@ -1,4 +1,4 @@ -import type * as spec from '@galacean/effects-specification'; +import * as spec from '@galacean/effects-specification'; import { effectsClass } from '../decorators'; import { EffectsObject } from '../effects-object'; import type { Engine } from '../engine'; @@ -97,7 +97,7 @@ export abstract class ShaderVariant extends EffectsObject { } } -@effectsClass('Shader') +@effectsClass(spec.DataType.Shader) export class Shader extends EffectsObject { shaderData: spec.ShaderData; diff --git a/packages/effects-core/src/scene.ts b/packages/effects-core/src/scene.ts index 7874f3688..ff0c033f5 100644 --- a/packages/effects-core/src/scene.ts +++ b/packages/effects-core/src/scene.ts @@ -4,7 +4,7 @@ import type { PluginSystem } from './plugin-system'; import type { PickEnum } from './utils'; import { isObject } from './utils'; -export type ImageSource = spec.TemplateImage | spec.Image | spec.CompressedImage; +export type ImageLike = spec.HTMLImageLike | ArrayBuffer | Texture; export type SceneRenderLevel = PickEnum; /** @@ -18,7 +18,7 @@ export interface Scene { readonly storage: Record, textureOptions: Record[], - images: ImageSource[], + images: ImageLike[], consumed?: boolean, textures?: Texture[], /** @@ -33,8 +33,29 @@ export interface Scene { * 加载分段时长 */ timeInfos: Record, - url: SceneType, - usedImages: Record, + url: Scene.LoadType, +} + +export namespace Scene { + type URLType = { url: string, options?: SceneLoadOptions }; + + /** + * 接受用于加载的数据类型 + */ + export type LoadType = string | Scene | URLType | spec.JSONScene | Record; + + // JSON 对象 + export function isJSONObject (scene: any): scene is Scene { + return isObject(scene) && 'jsonScene' in scene; + } + + export function isURL (scene: any): scene is URLType { + return isObject(scene) && 'url' in scene; + } + + export function isWithOptions (scene: any): scene is URLType { + return isObject(scene) && 'options' in scene; + } } /** @@ -107,23 +128,3 @@ export interface SceneLoadOptions { */ speed?: number, } - -/** - * 接受用于加载的数据类型 - */ -export type SceneURLType = { url: string }; -export type SceneType = string | Scene | SceneURLType | Record; -export type SceneWithOptionsType = { options: SceneLoadOptions }; -export type SceneLoadType = SceneType | SceneWithOptionsType; - -export function isSceneJSON (scene: any): scene is Scene { - return isObject(scene) && 'jsonScene' in scene; -} - -export function isSceneURL (scene: any): scene is Scene { - return isObject(scene) && 'url' in scene; -} - -export function isSceneWithOptions (scene: any): scene is SceneWithOptionsType { - return isObject(scene) && 'options' in scene; -} diff --git a/packages/effects-core/src/serialization-helper.ts b/packages/effects-core/src/serialization-helper.ts index 8f8977b01..7495473fd 100644 --- a/packages/effects-core/src/serialization-helper.ts +++ b/packages/effects-core/src/serialization-helper.ts @@ -70,13 +70,13 @@ export class SerializationHelper { if (!serializedDatas[serializeObject.getInstanceId()]) { serializedDatas[serializeObject.getInstanceId()] = {}; } - SerializationHelper.serializeTaggedProperties(serializeObject, serializedDatas[serializeObject.getInstanceId()]); + SerializationHelper.serialize(serializeObject, serializedDatas[serializeObject.getInstanceId()]); } return serializedDatas; } - static serializeTaggedProperties ( + static serialize ( effectsObject: EffectsObject, serializedData?: Record, ) { @@ -148,7 +148,7 @@ export class SerializationHelper { return serializedData; } - static deserializeTaggedProperties ( + static deserialize ( serializedData: spec.EffectsObjectData, effectsObject: EffectsObject, ) { @@ -183,7 +183,7 @@ export class SerializationHelper { effectsObject.fromData(taggedProperties as spec.EffectsObjectData); } - static async deserializeTaggedPropertiesAsync ( + static async deserializeAsync ( serializedData: spec.EffectsObjectData, effectsObject: EffectsObject, ) { diff --git a/packages/effects-core/src/shader/item.frag.glsl b/packages/effects-core/src/shader/item.frag.glsl index 1f6c35f58..ce672f476 100644 --- a/packages/effects-core/src/shader/item.frag.glsl +++ b/packages/effects-core/src/shader/item.frag.glsl @@ -4,7 +4,7 @@ varying vec4 vColor; varying vec2 vTexCoord;//x y varying vec3 vParams;//texIndex mulAplha transparentOcclusion -uniform sampler2D uSampler0; +uniform sampler2D _MainTex; vec4 blendColor(vec4 color, vec4 vc, float mode) { vec4 ret = color * vc; @@ -24,7 +24,7 @@ vec4 blendColor(vec4 color, vec4 vc, float mode) { void main() { vec4 color = vec4(0.); - vec4 texColor = texture2D(uSampler0, vTexCoord.xy); + vec4 texColor = texture2D(_MainTex, vTexCoord.xy); color = blendColor(texColor, vColor, floor(0.5 + vParams.y)); if(vParams.z == 0. && color.a < 0.04) { // 1/256 = 0.04 discard; diff --git a/packages/effects-core/src/shader/particle.vert.glsl b/packages/effects-core/src/shader/particle.vert.glsl index a6551f3ef..023ffbd1b 100644 --- a/packages/effects-core/src/shader/particle.vert.glsl +++ b/packages/effects-core/src/shader/particle.vert.glsl @@ -10,12 +10,18 @@ const float d2r = 3.141592653589793 / 180.; attribute vec3 aPos; attribute vec4 aOffset;//texcoord.xy time:start duration -attribute vec3 aVel; -attribute vec3 aRot; +// attribute vec3 aVel; +// attribute vec3 aRot; attribute vec4 aColor; attribute vec3 aDirX; attribute vec3 aDirY; +attribute vec3 aTranslation; +attribute vec3 aRotation0; +attribute vec3 aRotation1; +attribute vec3 aRotation2; +attribute vec3 aLinearMove; + #ifdef USE_SPRITE attribute vec3 aSprite;//start duration cycles uniform vec4 uSprite;//col row totalFrame blend @@ -37,53 +43,37 @@ uniform mat4 effects_MatrixV; uniform mat4 effects_MatrixVP; uniform vec4 uParams;//time duration endBehavior -uniform vec4 uAcceleration; -uniform vec4 uGravityModifierValue; +// uniform vec4 uAcceleration; +// uniform vec4 uGravityModifierValue; uniform vec4 uOpacityOverLifetimeValue; -#ifdef ROT_X_LIFETIME -uniform vec4 uRXByLifeTimeValue; -#endif +// #ifdef ROT_X_LIFETIME +// uniform vec4 uRXByLifeTimeValue; +// #endif -#ifdef ROT_Y_LIFETIME -uniform vec4 uRYByLifeTimeValue; -#endif +// #ifdef ROT_Y_LIFETIME +// uniform vec4 uRYByLifeTimeValue; +// #endif -#ifdef ROT_Z_LIFETIME -uniform vec4 uRZByLifeTimeValue; -#endif +// #ifdef ROT_Z_LIFETIME +// uniform vec4 uRZByLifeTimeValue; +// #endif #ifdef COLOR_OVER_LIFETIME uniform sampler2D uColorOverLifetime; #endif -#if LINEAR_VEL_X + LINEAR_VEL_Y + LINEAR_VEL_Z -#if LINEAR_VEL_X -uniform vec4 uLinearXByLifetimeValue; -#endif -#if LINEAR_VEL_Y -uniform vec4 uLinearYByLifetimeValue; -#endif -#if LINEAR_VEL_Z -uniform vec4 uLinearZByLifetimeValue; -#endif -#endif +// uniform vec4 uLinearXByLifetimeValue; +// uniform vec4 uLinearYByLifetimeValue; +// uniform vec4 uLinearZByLifetimeValue; -#ifdef SPEED_OVER_LIFETIME -uniform vec4 uSpeedLifetimeValue; -#endif +// #ifdef SPEED_OVER_LIFETIME +// uniform vec4 uSpeedLifetimeValue; +// #endif -#if ORB_VEL_X + ORB_VEL_Y + ORB_VEL_Z -#if ORB_VEL_X uniform vec4 uOrbXByLifetimeValue; -#endif -#if ORB_VEL_Y uniform vec4 uOrbYByLifetimeValue; -#endif -#if ORB_VEL_Z uniform vec4 uOrbZByLifetimeValue; -#endif uniform vec3 uOrbCenter; -#endif uniform vec4 uSizeByLifetimeValue; @@ -121,28 +111,28 @@ vec3 calOrbitalMov(float _life, float _dur) { return orb; } -vec3 calLinearMov(float _life, float _dur) { - vec3 mov = vec3(0.0); - #ifdef AS_LINEAR_MOVEMENT - #define FUNC(a) getValueFromTime(_life,a) - #else - #define FUNC(a) getIntegrateFromTime0(_life,a) * _dur - #endif - - #if LINEAR_VEL_X - mov.x = FUNC(uLinearXByLifetimeValue); - #endif - - #if LINEAR_VEL_Y - mov.y = FUNC(uLinearYByLifetimeValue); - #endif - - #if LINEAR_VEL_Z - mov.z = FUNC(uLinearZByLifetimeValue); - #endif - #undef FUNC - return mov; -} +// vec3 calLinearMov(float _life, float _dur) { +// vec3 mov = vec3(0.0); +// #ifdef AS_LINEAR_MOVEMENT +// #define FUNC(a) getValueFromTime(_life,a) +// #else +// #define FUNC(a) getIntegrateFromTime0(_life,a) * _dur +// #endif + +// #if LINEAR_VEL_X +// mov.x = FUNC(uLinearXByLifetimeValue); +// #endif + +// #if LINEAR_VEL_Y +// mov.y = FUNC(uLinearYByLifetimeValue); +// #endif + +// #if LINEAR_VEL_Z +// mov.z = FUNC(uLinearZByLifetimeValue); +// #endif +// #undef FUNC +// return mov; +// } mat3 mat3FromRotation(vec3 rotation) { vec3 sinR = sin(rotation * d2r); @@ -176,43 +166,43 @@ UVDetail getSpriteUV(vec2 uv, float lifeTime) { } #endif -vec3 calculateTranslation(vec3 vel, float t0, float t1, float dur) { - float dt = t1 - t0; // 相对delay的时间 - float d = getIntegrateByTimeFromTime(0., dt, uGravityModifierValue); - vec3 acc = uAcceleration.xyz * d; - #ifdef SPEED_OVER_LIFETIME - // dt / dur 归一化 - return vel * getIntegrateFromTime0(dt / dur, uSpeedLifetimeValue) * dur + acc; - #endif - return vel * dt + acc; -} - -mat3 transformFromRotation(vec3 rot, float _life, float _dur) { - vec3 rotation = rot; - #ifdef ROT_LIFETIME_AS_MOVEMENT - #define FUNC1(a) getValueFromTime(_life,a) - #else - #define FUNC1(a) getIntegrateFromTime0(_life,a) * _dur - #endif - - #ifdef ROT_X_LIFETIME - rotation.x += FUNC1(uRXByLifeTimeValue); - #endif - - #ifdef ROT_Y_LIFETIME - rotation.y += FUNC1(uRYByLifeTimeValue); - #endif - - #ifdef ROT_Z_LIFETIME - rotation.z += FUNC1(uRZByLifeTimeValue); - #endif - - if(dot(rotation, rotation) == 0.0) { - return mat3(1.0); - } - #undef FUNC1 - return mat3FromRotation(rotation); -} +// vec3 calculateTranslation(vec3 vel, float t0, float t1, float dur) { +// float dt = t1 - t0; // 相对delay的时间 +// float d = getIntegrateByTimeFromTime(0., dt, uGravityModifierValue); +// vec3 acc = uAcceleration.xyz * d; +// #ifdef SPEED_OVER_LIFETIME +// // dt / dur 归一化 +// return vel * getIntegrateFromTime0(dt / dur, uSpeedLifetimeValue) * dur + acc; +// #endif +// return vel * dt + acc; +// } + +// mat3 transformFromRotation(vec3 rot, float _life, float _dur) { +// vec3 rotation = rot; +// #ifdef ROT_LIFETIME_AS_MOVEMENT +// #define FUNC1(a) getValueFromTime(_life,a) +// #else +// #define FUNC1(a) getIntegrateFromTime0(_life,a) * _dur +// #endif + +// #ifdef ROT_X_LIFETIME +// rotation.x += FUNC1(uRXByLifeTimeValue); +// #endif + +// #ifdef ROT_Y_LIFETIME +// rotation.y += FUNC1(uRYByLifeTimeValue); +// #endif + +// #ifdef ROT_Z_LIFETIME +// rotation.z += FUNC1(uRZByLifeTimeValue); +// #endif + +// if(dot(rotation, rotation) == 0.0) { +// return mat3(1.0); +// } +// #undef FUNC1 +// return mat3FromRotation(rotation); +// } void main() { float time = uParams.x - aOffset.z; @@ -246,18 +236,18 @@ void main() { #ifdef SIZE_Y_BY_LIFE size.y = getValueFromTime(life, uSizeYByLifetimeValue); #endif - vec3 point = transformFromRotation(aRot, life, dur) * (aDirX * size.x + aDirY * size.y); - vec3 pt = calculateTranslation(aVel, aOffset.z, uParams.x, dur); - vec3 _pos = aPos + pt; + mat3 aRotation = mat3(aRotation0, aRotation1, aRotation2); + vec3 point = aRotation * (aDirX * size.x + aDirY * size.y); + // vec3 point = aRotation * (aDirX * size.x + aDirY * size.y); + // vec3 pt = calculateTranslation(aVel, aOffset.z, uParams.x, dur); + vec3 _pos = aPos + aTranslation; #if ORB_VEL_X + ORB_VEL_Y + ORB_VEL_Z _pos = mat3FromRotation(calOrbitalMov(life, dur)) * (_pos - uOrbCenter); _pos += uOrbCenter; #endif - #if LINEAR_VEL_X + LINEAR_VEL_Y + LINEAR_VEL_Z - _pos.xyz += calLinearMov(life, dur); - #endif + _pos.xyz += aLinearMove; #ifdef FINAL_TARGET float force = getValueFromTime(life, uForceCurve); diff --git a/packages/effects-core/src/shader/post-processing/color-grading.frag.glsl b/packages/effects-core/src/shader/post-processing/color-grading.frag.glsl index f6ecd8b57..2c516a97e 100644 --- a/packages/effects-core/src/shader/post-processing/color-grading.frag.glsl +++ b/packages/effects-core/src/shader/post-processing/color-grading.frag.glsl @@ -128,7 +128,6 @@ vec3 ApplyVignette(vec3 inputColor, vec2 uv, vec2 center, float intensity, float void main() { vec4 hdrColor = texture2D(_SceneTex, uv); - hdrColor *= hdrColor.a; hdrColor.rgb = pow(hdrColor.rgb, vec3(2.2)); // srgb 转 linear @@ -161,5 +160,6 @@ void main() { if(_UseToneMapping) { finalColor = max(vec3(0.0), ACESToneMapping(finalColor)); } - gl_FragColor = vec4(clamp(GammaCorrection(finalColor), 0.0, 1.0), 1.0); + float alpha = min(hdrColor.a, 1.0); + gl_FragColor = vec4(clamp(GammaCorrection(finalColor), 0.0, 1.0), alpha); } \ No newline at end of file diff --git a/packages/effects-core/src/shape/geometry.ts b/packages/effects-core/src/shape/geometry.ts index 73441f9b9..d45264efc 100644 --- a/packages/effects-core/src/shape/geometry.ts +++ b/packages/effects-core/src/shape/geometry.ts @@ -23,7 +23,7 @@ export type GeometryFromShape = { }; type ShapeGeometryPre = { p: spec.ShapePoints[1], s: spec.ShapeSplits[1] }; // FIXME: 考虑合并 Shape2D -export type ShapeData = { gs: ShapeGeometryPre[] } & { g: ShapeGeometryPre } & spec.ShapeGeometry; +export type ShapeGeometryData = { gs: ShapeGeometryPre[] } | { g: ShapeGeometryPre } | spec.ShapeGeometry; const POINT_INDEX = 2; @@ -95,18 +95,18 @@ export function getGeometryTriangles (geometry: spec.ShapeGeometry, options: { i * 根据新老版形状数据获取形状几何数据 * @param shape 新老版形状数据 */ -function getGeometriesByShapeData (shape: ShapeData) { +function getGeometriesByShapeData (shape: ShapeGeometryData) { const geometries: spec.ShapeGeometry[] = []; // 该版本的单个形状数据可以包含多个形状,可以加个埋点,五福之后没有就可以下掉 - if (shape.gs) { + if ('gs' in shape) { shape.gs.forEach(gs => { geometries.push({ p: [spec.ValueType.SHAPE_POINTS, gs.p], s: [spec.ValueType.SHAPE_SPLITS, gs.s], }); }); - } else if (shape.g) { + } else if ('g' in shape) { geometries.push({ p: [spec.ValueType.SHAPE_POINTS, shape.g.p], s: [spec.ValueType.SHAPE_SPLITS, shape.g.s], @@ -118,7 +118,7 @@ function getGeometriesByShapeData (shape: ShapeData) { return geometries; } -export function getGeometryByShape (shape: ShapeData, uvTransform: number[]): GeometryFromShape { +export function getGeometryByShape (shape: ShapeGeometryData, uvTransform?: number[]): GeometryFromShape { const datas = []; // 老数据兼容处理 const geometries = getGeometriesByShapeData(shape); diff --git a/packages/effects-core/src/shape/shape.ts b/packages/effects-core/src/shape/shape.ts index 17e289ee3..c7c252796 100644 --- a/packages/effects-core/src/shape/shape.ts +++ b/packages/effects-core/src/shape/shape.ts @@ -36,16 +36,16 @@ class ShapeNone implements Shape { } const map: Record): ShapeGenerator }> = { - [spec.ShapeType.NONE]: ShapeNone, - [spec.ShapeType.CONE]: Cone, - [spec.ShapeType.SPHERE]: Sphere, - [spec.ShapeType.HEMISPHERE]: Hemisphere, - [spec.ShapeType.CIRCLE]: Circle, - [spec.ShapeType.DONUT]: Donut, - [spec.ShapeType.RECTANGLE]: Rectangle, - [spec.ShapeType.EDGE]: Edge, - [spec.ShapeType.RECTANGLE_EDGE]: RectangleEdge, - [spec.ShapeType.TEXTURE]: TextureShape, + [spec.ParticleEmitterShapeType.NONE]: ShapeNone, + [spec.ParticleEmitterShapeType.CONE]: Cone, + [spec.ParticleEmitterShapeType.SPHERE]: Sphere, + [spec.ParticleEmitterShapeType.HEMISPHERE]: Hemisphere, + [spec.ParticleEmitterShapeType.CIRCLE]: Circle, + [spec.ParticleEmitterShapeType.DONUT]: Donut, + [spec.ParticleEmitterShapeType.RECTANGLE]: Rectangle, + [spec.ParticleEmitterShapeType.EDGE]: Edge, + [spec.ParticleEmitterShapeType.RECTANGLE_EDGE]: RectangleEdge, + [spec.ParticleEmitterShapeType.TEXTURE]: TextureShape, }; export function createShape (shapeOptions?: spec.ParticleShape): Shape { @@ -67,7 +67,7 @@ export function createShape (shapeOptions?: spec.ParticleShape): Shape { } const ctrl = new Ctrl(options); - if (type !== spec.ShapeType.NONE) { + if (type !== spec.ParticleEmitterShapeType.NONE) { const { alignSpeedDirection, upDirection = [0, 0, 1] } = shapeOptions as spec.ParticleShapeBase; ctrl.alignSpeedDirection = alignSpeedDirection; diff --git a/packages/effects-core/src/texture/texture.ts b/packages/effects-core/src/texture/texture.ts index 7ce28a26a..cf9155ad6 100644 --- a/packages/effects-core/src/texture/texture.ts +++ b/packages/effects-core/src/texture/texture.ts @@ -1,9 +1,10 @@ +import * as spec from '@galacean/effects-specification'; import { TextureSourceType } from './types'; -import type { TextureFactorySourceFrom, TextureSourceOptions, TextureDataType } from './types'; +import type { TextureFactorySourceFrom, TextureSourceOptions, TextureDataType, TextureOptionsBase } from './types'; import { glContext } from '../gl'; import type { Engine } from '../engine'; import { EffectsObject } from '../effects-object'; -import { loadImage } from '../downloader'; +import { loadImage, loadVideo } from '../downloader'; import { generateGUID } from '../utils'; let seed = 1; @@ -46,20 +47,54 @@ export abstract class Texture extends EffectsObject { * @param url - 要创建的 Texture URL * @since 2.0.0 */ - static async fromImage (url: string, engine: Engine): Promise { + static async fromImage ( + url: string, + engine: Engine, + options?: TextureOptionsBase, + ): Promise { const image = await loadImage(url); const texture = Texture.create(engine, { sourceType: TextureSourceType.image, image, + target: glContext.TEXTURE_2D, id: generateGUID(), flipY: true, + ...options, }); texture.initialize(); return texture; } + + /** + * 通过视频 URL 创建 Texture 对象。 + * @param url - 要创建的 Texture URL + * @param engine - 引擎对象 + * @param options - 可选的 Texture 选项 + * @since 2.1.0 + * @returns + */ + static async fromVideo ( + url: string, + engine: Engine, + options?: TextureOptionsBase, + ): Promise { + const video = await loadVideo(url); + const texture = Texture.create(engine, { + sourceType: TextureSourceType.video, + video, + id: generateGUID(), + flipY: true, + ...options, + }); + + texture.initialize(); + + return texture; + } + /** * 通过数据创建 Texture 对象。 * @param data - 要创建的 Texture 数据 @@ -206,7 +241,7 @@ export function generateWhiteTexture (engine: Engine) { return Texture.create( engine, { - id: 'whitetexture00000000000000000000', + id: spec.BuiltinObjectGUID.WhiteTexture, data: { width: 1, height: 1, @@ -222,7 +257,7 @@ export function generateTransparentTexture (engine: Engine) { return Texture.create( engine, { - id: 'transparenttexture00000000000000000000', + id: spec.BuiltinObjectGUID.TransparentTexture, data: { width: 1, height: 1, diff --git a/packages/effects-core/src/texture/utils.ts b/packages/effects-core/src/texture/utils.ts index 554af0e62..7fecf37ae 100644 --- a/packages/effects-core/src/texture/utils.ts +++ b/packages/effects-core/src/texture/utils.ts @@ -2,14 +2,13 @@ import type * as spec from '@galacean/effects-specification'; import type { Texture2DSourceOptions, TextureCubeSourceOptions } from './types'; import { TextureSourceType } from './types'; import { loadImage } from '../downloader'; -import type { Engine } from '../engine'; type TextureJSONOptions = spec.SerializedTextureSource & spec.TextureConfigOptionsBase & spec.TextureFormatOptions; export async function deserializeMipmapTexture ( textureOptions: TextureJSONOptions, bins: ArrayBuffer[], - engine: Engine, + assets: Record, files: spec.BinaryFile[] = [], ): Promise { if (textureOptions.target === 34067) { @@ -18,10 +17,9 @@ export async function deserializeMipmapTexture ( // @ts-expect-error if (pointer.id) { // @ts-expect-error - const loadedImageAsset = engine.assetLoader.loadGUID(pointer.id); + const loadedImage = assets[pointer.id]; - // @ts-expect-error - return loadedImageAsset.data; + return loadedImage; } else { return loadMipmapImage(pointer, bins); } diff --git a/packages/effects-core/src/transform.ts b/packages/effects-core/src/transform.ts index e2350163e..153e4cb10 100644 --- a/packages/effects-core/src/transform.ts +++ b/packages/effects-core/src/transform.ts @@ -19,6 +19,9 @@ const tempQuat = new Quaternion(); let seed = 1; // TODO 继承 Component +/** + * + */ export class Transform implements Disposable { /** * 转换右手坐标系左手螺旋对应的四元数到对应的旋转角 @@ -104,6 +107,11 @@ export class Transform implements Disposable { */ private readonly worldTRSCache = { position: new Vector3(0, 0, 0), quat: new Quaternion(0, 0, 0, 1), scale: new Vector3(1, 1, 1) }; + /** + * + * @param props + * @param parent + */ constructor (props: TransformProps = {}, parent?: Transform) { this.name = `transform_${seed++}`; if (props) { diff --git a/packages/effects-core/src/utils/color.ts b/packages/effects-core/src/utils/color.ts index 16bf55598..6ba1056fd 100644 --- a/packages/effects-core/src/utils/color.ts +++ b/packages/effects-core/src/utils/color.ts @@ -1,14 +1,12 @@ import { isString } from './index'; -export type color = [r: number, g: number, b: number, a: number]; - export interface ColorStop { stop: number, - color: color | number[], + color: number[], } -export function colorToArr (hex: string | number[], normalized?: boolean): color { - let ret: color = [0, 0, 0, 0]; +export function colorToArr (hex: string | number[], normalized?: boolean): number[] { + let ret: number[] = [0, 0, 0, 0]; if (isString(hex)) { hex = hex.replace(/[\s\t\r\n]/g, ''); @@ -37,9 +35,9 @@ export function colorToArr (hex: string | number[], normalized?: boolean): color return ret; } -export function getColorFromGradientStops (stops: ColorStop[], key: number, normalize?: boolean): color | number[] { +export function getColorFromGradientStops (stops: ColorStop[], key: number, normalize?: boolean): number[] { if (stops.length) { - let color: number[] | color | undefined; + let color: number[] | undefined; for (let j = 1; j <= stops.length - 1; j++) { const s0 = stops[j - 1]; @@ -101,8 +99,8 @@ export function colorStopsFromGradient (gradient: number[][] | Record = new EventEmitter(); + /** + * + * @param item + * @returns + */ static isComposition (item: VFXItem) { return item.type === spec.ItemType.composition; } + /** + * + * @param item + * @returns + */ static isSprite (item: VFXItem) { return item.type === spec.ItemType.sprite; } + /** + * + * @param item + * @returns + */ static isParticle (item: VFXItem) { return item.type === spec.ItemType.particle; } + /** + * + * @param item + * @returns + */ static isNull (item: VFXItem) { return item.type === spec.ItemType.null; } + /** + * + * @param item + * @returns + */ static isTree (item: VFXItem) { return item.type === spec.ItemType.tree; } + /** + * + * @param item + * @returns + */ static isCamera (item: VFXItem) { return item.type === spec.ItemType.camera; } - static isExtraCamera (item: VFXItem) { - return item.id === 'extra-camera' && item.name === 'extra-camera'; + /** + * + * @param ancestorCandidate + * @param descendantCandidate + * @returns + */ + static isAncestor ( + ancestorCandidate: VFXItem, + descendantCandidate: VFXItem, + ) { + let current = descendantCandidate.parent; + + while (current) { + if (current === ancestorCandidate) { + return true; + } + current = current.parent; + } + + return false; } + /** + * + * @param engine + * @param props + */ constructor ( engine: Engine, props?: VFXItemProps, @@ -267,8 +321,7 @@ export class VFXItem extends EffectsObject implements Disposable { const newComponent = new classConstructor(this.engine); this.components.push(newComponent); - newComponent.item = this; - newComponent.onAttached(); + newComponent.setVFXItem(this); return newComponent; } @@ -310,30 +363,23 @@ export class VFXItem extends EffectsObject implements Disposable { } setParent (vfxItem: VFXItem) { - if (vfxItem === this) { + if (vfxItem === this && !vfxItem) { return; } if (this.parent) { removeItem(this.parent.children, this); } this.parent = vfxItem; - if (vfxItem) { - if (!VFXItem.isCamera(this)) { - this.transform.parentTransform = vfxItem.transform; - } - vfxItem.children.push(this); - if (!this.composition) { - this.composition = vfxItem.composition; - } + if (!VFXItem.isCamera(this)) { + this.transform.parentTransform = vfxItem.transform; + } + vfxItem.children.push(this); + if (!this.composition) { + this.composition = vfxItem.composition; + } + if (!this.isDuringPlay && vfxItem.isDuringPlay) { + this.beginPlay(); } - } - - /** - * 元素动画结束播放时回调函数 - * @override - */ - onEnd () { - // OVERRIDE } /** @@ -358,19 +404,45 @@ export class VFXItem extends EffectsObject implements Disposable { } /** - * 获取元素显隐属性 + * 激活或停用 VFXItem */ - getVisible () { - return this.visible; + setActive (value: boolean) { + if (this.active !== value) { + this.active = !!value; + this.onActiveChanged(); + } } /** - * 设置元素显隐属性 会触发 `handleVisibleChanged` 回调 + * 当前 VFXItem 是否激活 + */ + get isActive () { + return this.active; + } + + /** + * 设置元素的显隐,该设置会批量开关元素组件 */ setVisible (visible: boolean) { - if (this.visible !== visible) { - this.visible = !!visible; + for (const component of this.components) { + component.enabled = visible; } + this.visible = visible; + } + + /** + * 元素组件显隐状态 + */ + get isVisible () { + return this.visible; + } + + /** + * 元素组件显隐状态 + * @deprecated use isVisible instead + */ + getVisible () { + return this.visible; } /** @@ -388,25 +460,6 @@ export class VFXItem extends EffectsObject implements Disposable { return tf; } - /** - * 获取元素内部节点的变换,目前只有场景树元素在使用 - * @param itemId 元素id信息,如果带^就返回内部节点变换,否则返回自己的变换 - * @returns 元素变换或内部节点变换 - */ - getNodeTransform (itemId: string): Transform { - for (let i = 0; i < this.components.length; i++) { - const comp = this.components[1]; - - // @ts-expect-error - if (comp.getNodeTransform) { - // @ts-expect-error - return comp.getNodeTransform(itemId); - } - } - - return this.transform; - } - /** * 设置元素在 3D 坐标轴上相对移动 */ @@ -495,34 +548,86 @@ export class VFXItem extends EffectsObject implements Disposable { return pos; } - /** - * 是否到达元素的结束时间 - * @param now - * @returns - */ - isEnded (now: number) { - // at least 1 ms - return now - this.duration > 0.001; - } - find (name: string): VFXItem | undefined { if (this.name === name) { return this; } - for (const child of this.children) { - if (child.name === name) { - return child; + + const queue: VFXItem[] = []; + + queue.push(...this.children); + let index = 0; + + while (index < queue.length) { + const item = queue[index]; + + index++; + if (item.name === name) { + return item; } + queue.push(...item.children); } + + return undefined; + } + + /** + * @internal + */ + beginPlay () { + this.isDuringPlay = true; + + if (this.composition && this.active && !this.isEnabled) { + this.onEnable(); + } + for (const child of this.children) { - const res = child.find(name); + if (!child.isDuringPlay) { + child.beginPlay(); + } + } + + } - if (res) { - return res; + /** + * @internal + */ + onActiveChanged () { + if (!this.isEnabled) { + this.onEnable(); + } else { + this.onDisable(); + } + } + + /** + * @internal + */ + onEnable () { + this.isEnabled = true; + for (const component of this.components) { + if (component.enabled && !component.isStartCalled) { + component.onStart(); + component.isStartCalled = true; } } + for (const component of this.components) { + if (component.enabled && !component.isEnableCalled) { + component.enable(); + } + } + } - return undefined; + /** + * @internal + */ + onDisable () { + this.isEnabled = false; + for (const component of this.components) { + if (component.enabled && component.isEnableCalled) { + component.disable(); + } + } } override fromData (data: VFXItemData): void { @@ -577,17 +682,13 @@ export class VFXItem extends EffectsObject implements Disposable { data.content = { options: {} }; } - if (duration <= 0) { + if (duration < 0) { throw new Error(`Item duration can't be less than 0, see ${HELP_LINK['Item duration can\'t be less than 0']}.`); } - this.itemBehaviours.length = 0; this.rendererComponents.length = 0; for (const component of this.components) { component.item = this; - if (component instanceof Behaviour) { - this.itemBehaviours.push(component); - } if (component instanceof RendererComponent) { this.rendererComponents.push(component); } diff --git a/packages/effects-threejs/src/material/three-material-util.ts b/packages/effects-threejs/src/material/three-material-util.ts index 2b495fd7b..46e2fff9f 100644 --- a/packages/effects-threejs/src/material/three-material-util.ts +++ b/packages/effects-threejs/src/material/three-material-util.ts @@ -82,22 +82,7 @@ export function setUniformValue (uniforms: Record, name: string, va */ export const TEXTURE_UNIFORM_MAP = [ 'uMaskTex', - 'uSampler0', - 'uSampler1', - 'uSampler2', - 'uSampler3', - 'uSampler4', - 'uSampler5', - 'uSampler6', - 'uSampler7', - 'uSampler8', - 'uSampler9', - 'uSampler10', - 'uSampler11', - 'uSampler12', - 'uSampler13', - 'uSampler14', - 'uSampler15', + '_MainTex', 'uColorOverLifetime', 'uColorOverTrail', ]; diff --git a/packages/effects-threejs/src/material/three-material.ts b/packages/effects-threejs/src/material/three-material.ts index c50a8a15e..9b01bc09d 100644 --- a/packages/effects-threejs/src/material/three-material.ts +++ b/packages/effects-threejs/src/material/three-material.ts @@ -2,7 +2,7 @@ import type { MaterialProps, Texture, UniformValue, MaterialDestroyOptions, UndefinedAble, Engine, math, GlobalUniforms, Renderer, } from '@galacean/effects-core'; -import { Material, Shader, ShaderType, ShaderFactory, generateGUID, maxSpriteMeshItemCount, spec } from '@galacean/effects-core'; +import { Material, Shader, ShaderType, ShaderFactory, generateGUID, spec } from '@galacean/effects-core'; import * as THREE from 'three'; import type { ThreeTexture } from '../three-texture'; import { @@ -56,10 +56,7 @@ export class ThreeMaterial extends Material { fragment: shader?.fragment ?? '', }; - for (let i = 0; i < maxSpriteMeshItemCount; i++) { - this.uniforms[`uSampler${i}`] = new THREE.Uniform(null); - } - + this.uniforms['_MainTex'] = new THREE.Uniform(null); this.uniforms['uEditorTransform'] = new THREE.Uniform([1, 1, 0, 0]); this.uniforms['effects_ObjectToWorld'] = new THREE.Uniform(new THREE.Matrix4().identity()); this.uniforms['effects_MatrixInvV'] = new THREE.Uniform([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 8, 1]); @@ -204,7 +201,7 @@ export class ThreeMaterial extends Material { return this.material.depthTest; } override set depthTest (value: UndefinedAble) { - this.material.depthTest = !!(value); + this.material.depthTest = !!value; } /** @@ -214,7 +211,7 @@ export class ThreeMaterial extends Material { return this.material.depthWrite; } override set depthMask (value: UndefinedAble) { - this.material.depthWrite = !!(value); + this.material.depthWrite = !!value; } /** @@ -236,7 +233,7 @@ export class ThreeMaterial extends Material { return this.material.polygonOffset; } override set polygonOffsetFill (value: UndefinedAble) { - this.material.polygonOffset = !!(value); + this.material.polygonOffset = !!value; } /** @@ -261,7 +258,7 @@ export class ThreeMaterial extends Material { return this.material.alphaToCoverage; } override set sampleAlphaToCoverage (value: UndefinedAble) { - this.material.alphaToCoverage = !!(value); + this.material.alphaToCoverage = !!value; } /** @@ -271,7 +268,7 @@ export class ThreeMaterial extends Material { return this.material.stencilWrite; } override set stencilTest (value: UndefinedAble) { - this.material.stencilWrite = !!(value); + this.material.stencilWrite = !!value; } /** diff --git a/packages/effects-threejs/src/three-composition.ts b/packages/effects-threejs/src/three-composition.ts index abb42403b..13e8b4e6c 100644 --- a/packages/effects-threejs/src/three-composition.ts +++ b/packages/effects-threejs/src/three-composition.ts @@ -61,13 +61,6 @@ export class ThreeComposition extends Composition { super(props, scene); } - /** - * 更新 video texture 数据 - */ - override updateVideo () { - void this.textures.map(tex => (tex as ThreeTexture).startVideo()); - } - override prepareRender (): void { const render = this.renderer; const frame = this.renderFrame; diff --git a/packages/effects-threejs/src/three-display-object.ts b/packages/effects-threejs/src/three-display-object.ts index acd5c9ceb..72b1b3ccb 100644 --- a/packages/effects-threejs/src/three-display-object.ts +++ b/packages/effects-threejs/src/three-display-object.ts @@ -1,8 +1,8 @@ import type { - EventSystem, SceneLoadOptions, Renderer, Composition, SceneLoadType, SceneType, Texture, - MessageItem, + EventSystem, SceneLoadOptions, Renderer, Composition, Texture, MessageItem, } from '@galacean/effects-core'; -import { AssetManager, isArray, isSceneURL, isSceneWithOptions, logger } from '@galacean/effects-core'; +import { Scene } from '@galacean/effects-core'; +import { AssetManager, isArray, logger } from '@galacean/effects-core'; import * as THREE from 'three'; import { ThreeComposition } from './three-composition'; import { ThreeRenderer } from './three-renderer'; @@ -70,9 +70,12 @@ export class ThreeDisplayObject extends THREE.Group { * @param options - 加载可选参数 * @returns */ - async loadScene (scene: SceneLoadType, options?: SceneLoadOptions): Promise; - async loadScene (scene: SceneLoadType[], options?: SceneLoadOptions): Promise; - async loadScene (scene: SceneLoadType | SceneLoadType[], options?: SceneLoadOptions): Promise { + async loadScene (scene: Scene.LoadType, options?: SceneLoadOptions): Promise; + async loadScene (scene: Scene.LoadType[], options?: SceneLoadOptions): Promise; + async loadScene ( + scene: Scene.LoadType | Scene.LoadType[], + options?: SceneLoadOptions, + ): Promise { let composition: Composition | Composition[]; const baseOrder = this.baseCompositionIndex; @@ -91,7 +94,7 @@ export class ThreeDisplayObject extends THREE.Group { composition.setIndex(baseOrder); } - return composition; + return composition as T; } pause () { @@ -107,24 +110,24 @@ export class ThreeDisplayObject extends THREE.Group { }); } - private async createComposition (url: SceneLoadType, options: SceneLoadOptions = {}): Promise { + private async createComposition (url: Scene.LoadType, options: SceneLoadOptions = {}): Promise { const last = performance.now(); let opts = { autoplay: true, ...options, }; - let source: SceneType; + let source: Scene.LoadType = url; - if (isSceneURL(url)) { - source = url.url; - if (isSceneWithOptions(url)) { + if (Scene.isURL(url)) { + if (!Scene.isJSONObject(url)) { + source = url.url; + } + if (Scene.isWithOptions(url)) { opts = { ...opts, ...url.options || {}, }; } - } else { - source = url; } if (this.assetManager) { diff --git a/packages/effects-threejs/src/three-geometry.ts b/packages/effects-threejs/src/three-geometry.ts index 9d6a89aef..23e4dea63 100644 --- a/packages/effects-threejs/src/three-geometry.ts +++ b/packages/effects-threejs/src/three-geometry.ts @@ -172,7 +172,7 @@ export class ThreeGeometry extends Geometry { const buffer = new THREE.InterleavedBuffer(data, attributeBuffer.stride); attributeBuffer = this.attributes[name].attribute.data = buffer; - + this.attributes[name].buffer = buffer; this.geometry.setAttribute(name, this.attributes[name].attribute); } else { attributeBuffer.set(data, 0); diff --git a/packages/effects-threejs/src/three-mesh.ts b/packages/effects-threejs/src/three-mesh.ts index 95e746377..178066e35 100644 --- a/packages/effects-threejs/src/three-mesh.ts +++ b/packages/effects-threejs/src/three-mesh.ts @@ -110,8 +110,8 @@ export class ThreeMesh extends Mesh implements Sortable { this.mesh.material = (mtl as ThreeMaterial).material; } - override start (): void { - super.start(); + override onStart (): void { + super.onStart(); (this.engine as ThreeEngine).threeGroup.add(this.mesh); } diff --git a/packages/effects-threejs/src/three-sprite-component.ts b/packages/effects-threejs/src/three-sprite-component.ts index c8ce202f6..9423768cb 100644 --- a/packages/effects-threejs/src/three-sprite-component.ts +++ b/packages/effects-threejs/src/three-sprite-component.ts @@ -83,8 +83,8 @@ export class ThreeSpriteComponent extends SpriteComponent { } } - override start (): void { - super.start(); + override onStart (): void { + super.onStart(); (this.engine as ThreeEngine).threeGroup.add(this.threeMesh); } diff --git a/packages/effects-threejs/src/three-text-component.ts b/packages/effects-threejs/src/three-text-component.ts index 60798545a..0e00abddc 100644 --- a/packages/effects-threejs/src/three-text-component.ts +++ b/packages/effects-threejs/src/three-text-component.ts @@ -31,8 +31,8 @@ export class ThreeTextComponent extends ThreeSpriteComponent { this.updateTexture(false); } - override update (dt: number): void { - super.update(dt); + override onUpdate (dt: number): void { + super.onUpdate(dt); this.updateTexture(false); } diff --git a/packages/effects-threejs/src/three-texture.ts b/packages/effects-threejs/src/three-texture.ts index c868d8b6a..bfe50ad95 100644 --- a/packages/effects-threejs/src/three-texture.ts +++ b/packages/effects-threejs/src/three-texture.ts @@ -66,6 +66,7 @@ export class ThreeTexture extends Texture { this.texture = this.createTextureByType(options); } this.texture.needsUpdate = true; + this.source = {}; } /** @@ -79,20 +80,6 @@ export class ThreeTexture extends Texture { this.texture.needsUpdate = true; } - /** - * 开始更新视频数据 - * - */ - async startVideo () { - if (this.sourceType === TextureSourceType.video) { - const video = (this.texture).source.data; - - if (video.paused) { - await video.play(); - } - } - } - /** * 组装纹理选项 * @param options - 纹理选项 @@ -223,8 +210,9 @@ export class ThreeTexture extends Texture { texture.wrapT = THREE.MirroredRepeatWrapping; this.width = this.height = 1; } + this.source = options; if (texture) { - texture.flipY = !!(flipY); + texture.flipY = !!flipY; return texture; } diff --git a/packages/effects-webgl/src/gl-material.ts b/packages/effects-webgl/src/gl-material.ts index a29052dd3..f6e760631 100644 --- a/packages/effects-webgl/src/gl-material.ts +++ b/packages/effects-webgl/src/gl-material.ts @@ -41,8 +41,8 @@ export class GLMaterial extends Material { private samplers: string[] = []; // material存放的sampler名称。 private uniforms: string[] = []; // material存放的uniform名称(不包括sampler)。 - private uniformDirtyFlag = true; - private macrosDirtyFlag = true; + private uniformDirty = true; + private macrosDirty = true; private glMaterialState = new GLMaterialState(); constructor ( @@ -217,14 +217,14 @@ export class GLMaterial extends Material { override enableMacro (keyword: string, value?: boolean | number): void { if (!this.isMacroEnabled(keyword) || this.enabledMacros[keyword] !== value) { this.enabledMacros[keyword] = value ?? true; - this.macrosDirtyFlag = true; + this.macrosDirty = true; } } override disableMacro (keyword: string): void { if (this.isMacroEnabled(keyword)) { delete this.enabledMacros[keyword]; - this.macrosDirtyFlag = true; + this.macrosDirty = true; } } @@ -234,7 +234,7 @@ export class GLMaterial extends Material { // TODO 待废弃 兼容 model/spine 插件 改造后可移除 createMaterialStates (states: MaterialStates): void { - this.sampleAlphaToCoverage = !!(states.sampleAlphaToCoverage); + this.sampleAlphaToCoverage = !!states.sampleAlphaToCoverage; this.depthTest = states.depthTest; this.depthMask = states.depthMask; this.depthRange = states.depthRange; @@ -275,10 +275,11 @@ export class GLMaterial extends Material { } override createShaderVariant () { - if (!this.shaderVariant || this.shaderVariant.shader !== this.shader || this.macrosDirtyFlag) { + if (this.shaderDirty || this.macrosDirty) { this.shaderVariant = this.shader.createVariant(this.enabledMacros); - this.macrosDirtyFlag = false; - this.uniformDirtyFlag = true; + this.macrosDirty = false; + this.shaderDirty = false; + this.uniformDirty = true; } } @@ -308,15 +309,15 @@ export class GLMaterial extends Material { for (name of globalUniforms.samplers) { if (!this.samplers.includes(name)) { this.samplers.push(name); - this.uniformDirtyFlag = true; + this.uniformDirty = true; } } } // 更新 cached uniform location - if (this.uniformDirtyFlag) { + if (this.uniformDirty) { shaderVariant.fillShaderInformation(this.uniforms, this.samplers); - this.uniformDirtyFlag = false; + this.uniformDirty = false; } if (globalUniforms) { @@ -330,6 +331,9 @@ export class GLMaterial extends Material { for (name in globalUniforms.vector4s) { shaderVariant.setVector4(name, globalUniforms.vector4s[name]); } + for (name in globalUniforms.vector3s) { + shaderVariant.setVector3(name, globalUniforms.vector3s[name]); + } for (name in globalUniforms.matrices) { shaderVariant.setMatrix(name, globalUniforms.matrices[name]); } @@ -493,7 +497,7 @@ export class GLMaterial extends Material { setTexture (name: string, texture: Texture) { if (!this.samplers.includes(name)) { this.samplers.push(name); - this.uniformDirtyFlag = true; + this.uniformDirty = true; } this.textures[name] = texture; } @@ -525,7 +529,7 @@ export class GLMaterial extends Material { clonedMaterial.matrixArrays = this.matrixArrays; clonedMaterial.samplers = this.samplers; clonedMaterial.uniforms = this.uniforms; - clonedMaterial.uniformDirtyFlag = true; + clonedMaterial.uniformDirty = true; return clonedMaterial; } @@ -622,6 +626,7 @@ export class GLMaterial extends Material { materialData.floats = {}; materialData.ints = {}; materialData.vector4s = {}; + materialData.colors = {}; materialData.textures = {}; materialData.dataType = spec.DataType.Material; materialData.stringTags = this.stringTags; @@ -722,7 +727,7 @@ export class GLMaterial extends Material { private checkUniform (uniformName: string): void { if (!this.uniforms.includes(uniformName)) { this.uniforms.push(uniformName); - this.uniformDirtyFlag = true; + this.uniformDirty = true; } } diff --git a/packages/effects-webgl/src/gl-renderer.ts b/packages/effects-webgl/src/gl-renderer.ts index ec0e2c462..15a2174de 100644 --- a/packages/effects-webgl/src/gl-renderer.ts +++ b/packages/effects-webgl/src/gl-renderer.ts @@ -18,6 +18,7 @@ import { GLTexture } from './gl-texture'; type Matrix4 = math.Matrix4; type Vector4 = math.Vector4; +type Vector3 = math.Vector3; export class GLRenderer extends Renderer implements Disposable { glRenderer: GLRendererInternal; @@ -111,8 +112,16 @@ export class GLRenderer extends Renderer implements Disposable { this.setFramebuffer(null); this.clear(frame.clearAction); + const currentCamera = frame.camera; + this.renderingData.currentFrame = frame; - this.renderingData.currentCamera = frame.camera; + this.renderingData.currentCamera = currentCamera; + + this.setGlobalMatrix('effects_MatrixInvV', currentCamera.getInverseViewMatrix()); + this.setGlobalMatrix('effects_MatrixV', currentCamera.getViewMatrix()); + this.setGlobalMatrix('effects_MatrixVP', currentCamera.getViewProjectionMatrix()); + this.setGlobalMatrix('_MatrixP', currentCamera.getProjectionMatrix()); + this.setGlobalVector3('effects_WorldSpaceCameraPos', currentCamera.position); // 根据 priority 排序 pass sortByOrder(passes); @@ -173,6 +182,11 @@ export class GLRenderer extends Renderer implements Disposable { this.renderingData.currentFrame.globalUniforms.matrices[name] = value; } + override setGlobalVector3 (name: string, value: Vector3) { + this.checkGlobalUniform(name); + this.renderingData.currentFrame.globalUniforms.vector3s[name] = value; + } + override drawGeometry (geometry: Geometry, material: Material, subMeshIndex = 0): void { if (!geometry || !material) { return; @@ -182,27 +196,6 @@ export class GLRenderer extends Renderer implements Disposable { geometry.flush(); const renderingData = this.renderingData; - // TODO 后面移到管线相机渲染开始位置 - if (renderingData.currentFrame.globalUniforms) { - if (renderingData.currentCamera) { - this.setGlobalMatrix('effects_MatrixInvV', renderingData.currentCamera.getInverseViewMatrix()); - this.setGlobalMatrix('effects_MatrixV', renderingData.currentCamera.getViewMatrix()); - this.setGlobalMatrix('effects_MatrixVP', renderingData.currentCamera.getViewProjectionMatrix()); - this.setGlobalMatrix('_MatrixP', renderingData.currentCamera.getProjectionMatrix()); - } - - // TODO 自定义材质测试代码 - const time = Date.now() % 100000000 * 0.001 * 1; - let _Time = this.getGlobalVector4('_Time'); - - // TODO 待移除 - this.setGlobalFloat('_GlobalTime', time); - if (!_Time) { - _Time = new math.Vector4(time / 20, time, time * 2, time * 3); - } - this.setGlobalVector4('_Time', _Time.set(time / 20, time, time * 2, time * 3)); - } - if (renderingData.currentFrame.editorTransform) { material.setVector4('uEditorTransform', renderingData.currentFrame.editorTransform); } diff --git a/packages/effects-webgl/src/gl-shader-library.ts b/packages/effects-webgl/src/gl-shader-library.ts index 15bd2986c..3f1f89dac 100644 --- a/packages/effects-webgl/src/gl-shader-library.ts +++ b/packages/effects-webgl/src/gl-shader-library.ts @@ -142,6 +142,7 @@ export class GLShaderLibrary implements ShaderLibrary, Disposable, RestoreHandle console.warn(`Find duplicated shader id: ${shader.id}.`); } this.programMap[shader.id] = glProgram; + // console.log('compileShader ' + result.cacheId + ' ' + result.compileTime + ' ', shader.source); }; const checkComplete = () => { if (this.engine.isDestroyed) { diff --git a/packages/effects-webgl/src/gl-texture.ts b/packages/effects-webgl/src/gl-texture.ts index d01a337e7..75fcd39bc 100644 --- a/packages/effects-webgl/src/gl-texture.ts +++ b/packages/effects-webgl/src/gl-texture.ts @@ -12,8 +12,6 @@ import type { GLPipelineContext } from './gl-pipeline-context'; import { assignInspectorName } from './gl-renderer-internal'; import type { GLEngine } from './gl-engine'; -type HTMLImageLike = HTMLImageElement | HTMLCanvasElement | HTMLVideoElement; - const FORMAT_HALF_FLOAT: Record = { [glContext.RGBA]: 34842, //RGBA16F [glContext.RGB]: 34843, //RGB16F @@ -336,7 +334,7 @@ export class GLTexture extends Texture implements Disposable, RestoreHandler { internalformat: GLenum, format: GLenum, type: GLenum, - image: HTMLImageLike, + image: spec.HTMLImageLike, ): spec.vec2 { const { sourceType, minFilter, magFilter, wrapS, wrapT } = this.source; const maxSize = this.engine.gpuCapability.detail.maxTextureSize ?? 2048; @@ -390,7 +388,7 @@ export class GLTexture extends Texture implements Disposable, RestoreHandler { return [width, height]; } - private resizeImage (image: HTMLImageLike, targetWidth?: number, targetHeight?: number): HTMLCanvasElement | HTMLImageElement { + private resizeImage (image: spec.HTMLImageLike, targetWidth?: number, targetHeight?: number): HTMLCanvasElement | HTMLImageElement { const { detail } = this.engine.gpuCapability; const maxSize = detail.maxTextureSize ?? 2048; @@ -454,11 +452,7 @@ export class GLTexture extends Texture implements Disposable, RestoreHandler { this.source.video && this.initialized ) { - const video = this.source.video; - if (video.paused) { - await video.play(); - } this.update({ video: this.source.video }); return true; @@ -487,15 +481,6 @@ export class GLTexture extends Texture implements Disposable, RestoreHandler { if (this.pipelineContext && this.textureBuffer) { this.pipelineContext.gl.deleteTexture(this.textureBuffer); } - if ( - this.source.sourceType === TextureSourceType.video && - this.source.video && - this.initialized - ) { - this.source.video.pause(); - this.source.video.src = ''; - this.source.video.load(); - } this.width = 0; this.height = 0; this.textureBuffer = null; @@ -512,7 +497,7 @@ export class GLTexture extends Texture implements Disposable, RestoreHandler { } function resizeImageByCanvas ( - image: HTMLImageLike, + image: spec.HTMLImageLike, maxSize: number, targetWidth?: number, targetHeight?: number, diff --git a/packages/effects/src/index.ts b/packages/effects/src/index.ts index 4f23df406..b51e2f9a6 100644 --- a/packages/effects/src/index.ts +++ b/packages/effects/src/index.ts @@ -84,6 +84,9 @@ Engine.create = (gl: WebGLRenderingContext | WebGL2RenderingContext) => { return new GLEngine(gl); }; +/** + * Player 版本号 + */ export const version = __VERSION__; logger.info(`Player version: ${version}.`); diff --git a/packages/effects/src/player.ts b/packages/effects/src/player.ts index f3862df03..9b0f45ace 100644 --- a/packages/effects/src/player.ts +++ b/packages/effects/src/player.ts @@ -1,14 +1,12 @@ import type { Disposable, GLType, GPUCapability, LostHandler, RestoreHandler, SceneLoadOptions, - Texture2DSourceOptionsVideo, TouchEventType, SceneLoadType, SceneType, EffectsObject, - MessageItem, Scene, + Texture2DSourceOptionsVideo, TouchEventType, EffectsObject, MessageItem, } from '@galacean/effects-core'; import { AssetManager, Composition, EVENT_TYPE_CLICK, EventSystem, logger, Renderer, Material, TextureLoadAction, Ticker, canvasPool, getPixelRatio, gpuTimer, initErrors, isAndroid, - isArray, pluginLoaderMap, setSpriteMeshMaxItemCountByGPU, spec, isSceneURL, EventEmitter, - generateWhiteTexture, isSceneWithOptions, Texture, PLAYER_OPTIONS_ENV_EDITOR, isIOS, - DEFAULT_FPS, + isArray, pluginLoaderMap, setSpriteMeshMaxItemCountByGPU, spec, EventEmitter, + generateWhiteTexture, Texture, PLAYER_OPTIONS_ENV_EDITOR, isIOS, DEFAULT_FPS, Scene, } from '@galacean/effects-core'; import type { GLRenderer } from '@galacean/effects-webgl'; import { HELP_LINK } from './constants'; @@ -179,6 +177,21 @@ export class Player extends EventEmitter> implements Disposa return this.compositions; } + /** + * Gets the array of asset managers. + * @returns + */ + getAssetManager (): ReadonlyArray { + return this.assetManagers; + } + + /** + * 获取当前播放的合成数量 + */ + get compositionCount () { + return this.compositions.length; + } + /** * 是否有合成在播放 */ @@ -267,9 +280,12 @@ export class Player extends EventEmitter> implements Disposa * @param options - 加载可选参数 * @returns */ - async loadScene (scene: SceneLoadType, options?: SceneLoadOptions): Promise; - async loadScene (scene: SceneLoadType[], options?: SceneLoadOptions): Promise; - async loadScene (scene: SceneLoadType | SceneLoadType[], options?: SceneLoadOptions): Promise { + async loadScene (scene: Scene.LoadType, options?: SceneLoadOptions): Promise; + async loadScene (scene: Scene.LoadType[], options?: SceneLoadOptions): Promise; + async loadScene ( + scene: Scene.LoadType | Scene.LoadType[], + options?: SceneLoadOptions, + ): Promise { let composition: Composition | Composition[]; const baseOrder = this.baseCompositionIndex; @@ -290,32 +306,34 @@ export class Player extends EventEmitter> implements Disposa this.ticker?.start(); - return composition; + return composition as T; } private async createComposition ( - url: SceneLoadType, + url: Scene.LoadType, options: SceneLoadOptions = {}, ): Promise { const renderer = this.renderer; const engine = renderer.engine; + const asyncShaderCompile = engine.gpuCapability?.detail?.asyncShaderCompile; const last = performance.now(); let opts = { autoplay: true, ...options, }; - let source: SceneType; + let source: Scene.LoadType = url; - if (isSceneURL(url)) { - source = url.url; - if (isSceneWithOptions(url)) { + // 加载多个合成链接并各自设置可选参数 + if (Scene.isURL(url)) { + if (!Scene.isJSONObject(url)) { + source = url.url; + } + if (Scene.isWithOptions(url)) { opts = { ...opts, ...url.options, }; } - } else { - source = url; } const assetManager = new AssetManager(opts); @@ -361,8 +379,6 @@ export class Player extends EventEmitter> implements Disposa }, }, scene); - this.compositions.push(composition); - if (this.ticker) { // 中低端设备降帧到 30fps if (opts.renderLevel === spec.RenderLevel.B) { @@ -384,6 +400,8 @@ export class Player extends EventEmitter> implements Disposa } } + const compileStart = performance.now(); + await new Promise(resolve => { this.renderer.getShaderLibrary()?.compileAllShaders(() => { resolve(null); @@ -397,10 +415,15 @@ export class Player extends EventEmitter> implements Disposa composition.pause(); } - const firstFrameTime = performance.now() - last + composition.statistic.loadTime; + const compileTime = performance.now() - compileStart; + const firstFrameTime = performance.now() - last; + composition.statistic.compileTime = compileTime; composition.statistic.firstFrameTime = firstFrameTime; - logger.info(`First frame: [${composition.name}]${firstFrameTime.toFixed(4)}ms.`); + logger.info(`Shader ${asyncShaderCompile ? 'async' : 'sync'} compile [${composition.name}]: ${compileTime.toFixed(4)}ms.`); + logger.info(`First frame [${composition.name}]: ${firstFrameTime.toFixed(4)}ms.`); + + this.compositions.push(composition); return composition; } @@ -516,6 +539,7 @@ export class Player extends EventEmitter> implements Disposa } this.ticker?.pause(); + this.emit('pause'); this.emit('update', { player: this, playing: false, @@ -750,7 +774,7 @@ export class Player extends EventEmitter> implements Disposa await video.play(); } } - newComposition.rootItem.ended = false; + newComposition.isEnded = false; newComposition.gotoAndPlay(currentTime); return newComposition; diff --git a/packages/effects/src/types.ts b/packages/effects/src/types.ts index 9423ce28e..5ece99804 100644 --- a/packages/effects/src/types.ts +++ b/packages/effects/src/types.ts @@ -48,6 +48,9 @@ export interface PlayerConfig { * player 的 name */ name?: string, + /** + * 渲染选项,传递给 WebGLRenderingContext 实例化的 WebGLContextAttributes 参数 + */ renderOptions?: { /** * 播放器是否需要截图(对应 WebGL 的 preserveDrawingBuffer 参数) @@ -71,6 +74,9 @@ export interface PlayerConfig { reportGPUTime?: (time: number) => void, } +/** + * 播放器事件 + */ export type PlayerEvent

= { /** * 播放器点击事件 diff --git a/plugin-packages/alipay-downgrade/src/index.ts b/plugin-packages/alipay-downgrade/src/index.ts index 873a456f7..cf2de2581 100644 --- a/plugin-packages/alipay-downgrade/src/index.ts +++ b/plugin-packages/alipay-downgrade/src/index.ts @@ -6,6 +6,9 @@ export * from './utils'; export * from './native-log'; export * from './types'; +/** + * 插件版本号 + */ export const version = __VERSION__; registerPlugin('alipay-downgrade', AlipayDowngradePlugin, VFXItem, true); diff --git a/plugin-packages/downgrade/src/index.ts b/plugin-packages/downgrade/src/index.ts index cef67b433..51fe8ec73 100644 --- a/plugin-packages/downgrade/src/index.ts +++ b/plugin-packages/downgrade/src/index.ts @@ -7,6 +7,9 @@ export * from './parser'; export * from './ua-decoder'; export * from './types'; +/** + * 插件版本号 + */ export const version = __VERSION__; registerPlugin('downgrade', DowngradePlugin, VFXItem, true); diff --git a/plugin-packages/editor-gizmo/src/gizmo-component.ts b/plugin-packages/editor-gizmo/src/gizmo-component.ts index 5910c55cf..2521d829e 100644 --- a/plugin-packages/editor-gizmo/src/gizmo-component.ts +++ b/plugin-packages/editor-gizmo/src/gizmo-component.ts @@ -41,7 +41,7 @@ export class GizmoComponent extends RendererComponent { mat = Matrix4.fromIdentity(); wireframeMeshes: Mesh[] = []; - override start (): void { + override onStart (): void { this.item.getHitTestParams = this.getHitTestParams; for (const item of this.item.composition?.items ?? []) { if (item.id === this.target) { @@ -118,7 +118,7 @@ export class GizmoComponent extends RendererComponent { composition?.loaderData.gizmoItems.push(this.item); } - override update (dt: number): void { + override onUpdate (dt: number): void { this.updateRenderData(); } diff --git a/plugin-packages/editor-gizmo/src/index.ts b/plugin-packages/editor-gizmo/src/index.ts index 946f1e31a..687d15beb 100644 --- a/plugin-packages/editor-gizmo/src/index.ts +++ b/plugin-packages/editor-gizmo/src/index.ts @@ -14,6 +14,9 @@ export { }; export * from './gizmo-component'; +/** + * 插件版本号 + */ export const version = __VERSION__; logger.info(`Plugin editor gizmo version: ${version}.`); diff --git a/plugin-packages/model/demo/src/camera.ts b/plugin-packages/model/demo/src/camera.ts index 3d4442ff4..84923dcad 100644 --- a/plugin-packages/model/demo/src/camera.ts +++ b/plugin-packages/model/demo/src/camera.ts @@ -1,4 +1,4 @@ -//@ts-nocheck +import type { Player } from '@galacean/effects'; import { math, spec } from '@galacean/effects'; import { CameraGestureType, CameraGestureHandlerImp } from '@galacean/effects-plugin-model'; import { LoaderImplEx } from '../../src/helper'; @@ -9,20 +9,20 @@ let player: Player; let pending = false; let sceneAABB; -let sceneCenter; +let sceneCenter: math.Vector3; let sceneRadius = 1; let mouseDown = false; const pauseOnFirstFrame = false; let gestureType = CameraGestureType.rotate_focus; -let gestureHandler; +let gestureHandler: CameraGestureHandlerImp; let moveBegin = false; let scaleBegin = false; let rotationBegin = false; let rotationFocusBegin = false; -let playScene; +let playScene: spec.JSONScene; let url = 'https://gw.alipayobjects.com/os/bmw-prod/2b867bc4-0e13-44b8-8d92-eb2db3dfeb03.glb'; @@ -71,7 +71,7 @@ async function getCurrentScene () { return loader.getLoadResult().jsonScene; } -export async function loadScene (inPlayer) { +export async function loadScene (inPlayer: Player) { if (!player) { player = inPlayer; registerMouseEvent(); @@ -80,9 +80,10 @@ export async function loadScene (inPlayer) { if (!playScene) { playScene = await getCurrentScene(); } else { - playScene.compositions[0].items.forEach(item => { + playScene.items.forEach(item => { if (item.id === 'extra-camera') { - item.transform = player.compositions[0].camera; + // @ts-expect-error + item.transform = player.getCompositions()[0].camera; } }); } @@ -175,7 +176,7 @@ function registerMouseEvent () { refreshCamera(); if (pauseOnFirstFrame) { - player.compositions.forEach(comp => { + player.getCompositions().forEach(comp => { comp.gotoAndStop(comp.time); }); } @@ -222,7 +223,7 @@ function registerMouseEvent () { gestureHandler.onXYMoving(e.clientX, e.clientY); refreshCamera(); if (pauseOnFirstFrame) { - player.compositions.forEach(comp => { + player.getCompositions().forEach(comp => { comp.gotoAndStop(comp.time); }); } @@ -242,7 +243,7 @@ function registerMouseEvent () { gestureHandler.onZMoving(e.clientX, e.clientY); refreshCamera(); if (pauseOnFirstFrame) { - player.compositions.forEach(comp => { + player.getCompositions().forEach(comp => { comp.gotoAndStop(comp.time); }); } @@ -263,7 +264,7 @@ function registerMouseEvent () { gestureHandler.onRotating(e.clientX, e.clientY); refreshCamera(); if (pauseOnFirstFrame) { - player.compositions.forEach(comp => { + player.getCompositions().forEach(comp => { comp.gotoAndStop(comp.time); }); } @@ -285,7 +286,7 @@ function registerMouseEvent () { gestureHandler.onRotatingPoint(e.clientX, e.clientY); refreshCamera(); if (pauseOnFirstFrame) { - player.compositions.forEach(comp => { + player.getCompositions().forEach(comp => { comp.gotoAndStop(comp.time); }); } @@ -355,16 +356,17 @@ function registerMouseEvent () { } function refreshCamera () { - const freeCamera = playScene.items.find(item => item.name === 'extra-camera'); - const position = player.compositions[0].camera.position; - const rotation = player.compositions[0].camera.rotation; + const freeCamera = playScene.items.find(item => item.name === 'extra-camera') as spec.VFXItemData; + const position = player.getCompositions()[0].camera.position; + const rotation = player.getCompositions()[0].camera.rotation; - if (rotation[0] === null) { + if (rotation.x === null) { return; } - freeCamera.transform.position = position; - freeCamera.transform.rotation = rotation; + freeCamera.transform!.position = position; + // @ts-expect-error + freeCamera.transform!.rotation = rotation; } const demo_infoDom = document.getElementsByClassName('demo-info')[0]; @@ -383,9 +385,9 @@ export function createUI () { `; select.onchange = e => { - gestureType = +e.target.value; + gestureType = +(e.target as HTMLSelectElement).value; }; - select.value = gestureType; + select.value = gestureType.toString(); // add ui to parent dom demo_infoDom.appendChild(uiDom); demo_infoDom.appendChild(select); diff --git a/plugin-packages/model/demo/src/editor-mode.ts b/plugin-packages/model/demo/src/editor-mode.ts new file mode 100644 index 000000000..d100d036a --- /dev/null +++ b/plugin-packages/model/demo/src/editor-mode.ts @@ -0,0 +1,366 @@ +import type { Player } from '@galacean/effects'; +import { math, spec, generateGUID } from '@galacean/effects'; +import { CameraGestureHandlerImp, PSkyboxCreator, PSkyboxType } from '@galacean/effects-plugin-model'; +import { LoaderImplEx } from '../../src/helper'; +import { GizmoSubType } from '@galacean/effects-plugin-editor-gizmo'; + +const { Sphere, Vector3, Box3 } = math; + +let player: Player; +let sceneAABB; +let sceneCenter; +let sceneRadius = 1; + +let gestureHandler: CameraGestureHandlerImp; +let mouseDown = false; +let scaleBegin = false; +let rotationBegin = false; + +let playScene: spec.JSONScene; + +let url = 'https://gw.alipayobjects.com/os/gltf-asset/89748482160728/fish_test.glb'; + +url = 'https://gw.alipayobjects.com/os/gltf-asset/89748482160728/DamagedHelmet.glb'; + +enum EditorMode { + standard, + diffuse, + wireframe, + wireframe2, +} + +let editorMode = EditorMode.standard; + +async function getCurrentScene () { + const duration = 99999; + const loader = new LoaderImplEx(); + const loadResult = await loader.loadScene({ + gltf: { + resource: url, + }, + effects: { + duration: duration, + endBehavior: spec.EndBehavior.restart, + playAnimation: 0, + }, + }); + + const sceneMin = Vector3.fromArray(loadResult.sceneAABB.min); + const sceneMax = Vector3.fromArray(loadResult.sceneAABB.max); + + sceneAABB = new Box3(sceneMin, sceneMax); + sceneRadius = sceneAABB.getBoundingSphere(new Sphere()).radius; + sceneCenter = sceneAABB.getCenter(new Vector3()); + const position = sceneCenter.add(new Vector3(0, 0, sceneRadius * 3)); + + loader.addCamera({ + near: 0.1, + far: 5000, + fov: 18, + clipMode: 0, + // + name: 'extra-camera', + duration: duration, + endBehavior: spec.EndBehavior.restart, + position: position.toArray(), + rotation: [0, 0, 0], + }); + + loader.addLight({ + lightType: spec.LightType.directional, + color: { r: 1, g: 1, b: 1, a: 1 }, + intensity: 1, + // + name: 'main-light', + position: [0, 0, 0], + rotation: [45, 45, 0], + scale: [1, 1, 1], + duration: duration, + endBehavior: spec.EndBehavior.restart, + }); + + const skyboxParams = PSkyboxCreator.getSkyboxParams(PSkyboxType.NFT); + + const specularImageList = skyboxParams.specularImage; + const diffuseImageList = specularImageList[specularImageList.length - 1]; + + loader.addSkybox({ + type: 'url', + renderable: false, + intensity: 1, + reflectionsIntensity: 1, + diffuseImage: diffuseImageList, + specularImage: specularImageList, + specularImageSize: Math.pow(2, specularImageList.length - 1), + specularMipCount: specularImageList.length, + }); + + const { jsonScene } = loader.getLoadResult(); + + loader.dispose(); + + jsonScene.plugins.push('editor-gizmo'); + + return jsonScene; +} + +export async function loadScene (inPlayer: Player) { + if (!player) { + player = inPlayer; + registerMouseEvent(); + } + // + if (!playScene) { + playScene = await getCurrentScene(); + } else { + playScene.items.forEach(item => { + if (item.name === 'extra-camera') { + const camera = player.getCompositions()[0].camera; + + item.transform = { + position: camera.position.clone(), + eulerHint: camera.rotation.clone(), + scale: { x: 1, y: 1, z: 1 }, + }; + } + }); + } + + let currentScene = playScene; + let renderMode3D = spec.RenderMode3D.none; + + if (editorMode === EditorMode.wireframe) { + currentScene = addWireframeItems(playScene); + } else if (editorMode === EditorMode.wireframe2) { + currentScene = addWireframeItems(playScene, false); + } else if (editorMode === EditorMode.diffuse) { + renderMode3D = spec.RenderMode3D.diffuse; + } + + player.destroyCurrentCompositions(); + const loadOptions = { + pluginData: { + renderMode3D: renderMode3D, + renderMode3DUVGridSize: 1 / 12, + }, + }; + + return player.loadScene(currentScene, loadOptions).then(async comp => { + gestureHandler = new CameraGestureHandlerImp(comp); + + return true; + }); +} + +function addWireframeItems (scene: spec.JSONScene, hide3DModel = true) { + const newComponents: any = []; + + scene.components.forEach(comp => { + const newComponent: any = { ...comp }; + + if (newComponent.dataType === spec.DataType.MeshComponent) { + newComponent.hide = hide3DModel; + } + newComponents.push(newComponent); + }); + const newItems: any = [ + ...scene.items, + ]; + + scene.items.forEach(item => { + if (item.type === 'mesh') { + const { name, duration, endBehavior } = item; + const newItem: any = { + name: name + '_shadedWireframe', + id: generateGUID(), + pn: 1, + type: 'editor-gizmo', + visible: true, + duration: duration ?? 999, + dataType: spec.DataType.VFXItemData, + endBehavior, + transform: { + scale: { x: 1, y: 1, z: 1 }, + position: { x: 0, y: 0, z: 0 }, + eulerHint: { x: 0, y: 0, z: 0 }, + }, + }; + const newComponent = { + dataType: 'GizmoComponent', + id: generateGUID(), + item: { id: newItem.id }, + options: { + target: item.id, + subType: GizmoSubType.modelWireframe, + color: [0, 255, 255], + }, + }; + + newItem.components = [ + { id: newComponent.id }, + ]; + newComponents.push(newComponent); + newItems.push(newItem); + } + }); + const newComposition = { + ...scene.compositions[0], + }; + const items = []; + + for (const item of newItems) { + items.push({ + id: item.id, + }); + } + newComposition.items = items; + const newScene: spec.JSONScene = { + ...scene, + components: newComponents, + items: newItems, + compositions: [newComposition], + }; + + return newScene; +} + +function registerMouseEvent () { + player.canvas.addEventListener('mousedown', function (e) { + if (!gestureHandler) { + return; + } + + mouseDown = true; + if (e.buttons === 1) { + rotationBegin = true; + } else if (e.buttons === 4) { + scaleBegin = true; + } + }); + + player.canvas.addEventListener('mousemove', async function (e) { + if (gestureHandler && mouseDown) { + if (e.buttons === 1) { + if (rotationBegin) { + gestureHandler.onRotatePointBegin( + e.clientX, + e.clientY, + player.canvas.width / 2, + player.canvas.height / 2, + [0, 0, 0], + 'extra-camera' + ); + rotationBegin = false; + } + + gestureHandler.onRotatingPoint(e.clientX, e.clientY); + refreshCamera(); + } else if (e.buttons === 4) { + if (scaleBegin) { + gestureHandler.onZMoveBegin( + e.clientX, + e.clientY, + player.canvas.width / 2, + player.canvas.height / 2, + 'extra-camera' + ); + scaleBegin = false; + } + + gestureHandler.onZMoving(e.clientX, e.clientY); + refreshCamera(); + } + } + }); + + player.canvas.addEventListener('mouseup', async function (e) { + mouseDown = false; + if (gestureHandler) { + if (e.buttons === 1) { + gestureHandler.onRotatePointEnd(); + } else if (e.buttons === 4) { + gestureHandler.onZMoveEnd(); + } + } + }); + + player.canvas.addEventListener('mouseleave', async function (e) { + mouseDown = false; + if (gestureHandler) { + if (e.buttons === 1) { + gestureHandler.onRotatePointEnd(); + } else if (e.buttons === 4) { + gestureHandler.onZMoveEnd(); + } + } + }); + + player.canvas.addEventListener('wheel', function (e) { + if (gestureHandler) { + gestureHandler.onKeyEvent({ + cameraID: 'extra-camera', + zAxis: e.deltaY > 0 ? 1 : -1, + speed: sceneRadius * 0.15, + }); + } + }); +} + +function refreshCamera () { + const freeCamera = playScene.items.find(item => item.name === 'extra-camera') as spec.VFXItemData; + const position = player.getCompositions()[0].camera.position; + const rotation = player.getCompositions()[0].camera.rotation; + + if (rotation.x === null) { + return; + } + + freeCamera.transform!.position = position; + // @ts-expect-error + freeCamera.transform!.rotation = rotation; +} + +export function createUI () { + const container = document.getElementsByClassName('container')[0] as HTMLElement; + const uiDom = document.createElement('div'); + const select = document.createElement('select'); + + container.style.background = 'rgba(0,0,0)'; + uiDom.className = 'my_ui'; + select.innerHTML = ` + + + + + `; + for (let i = 0; i < select.options.length; i++) { + const option = select.options[i]; + + if (option.value === editorMode.toString()) { + select.selectedIndex = i; + + break; + } + } + + select.onchange = async function (e) { + const element = e.target as HTMLSelectElement; + + if (element.value === 'standard') { + editorMode = EditorMode.standard; + } else if (element.value === 'diffuse') { + editorMode = EditorMode.diffuse; + } else if (element.value === 'wireframe') { + editorMode = EditorMode.wireframe; + } else if (element.value === 'wireframe2') { + editorMode = EditorMode.wireframe2; + } + + await loadScene(player); + }; + uiDom.appendChild(select); + + const demoInfo = document.getElementsByClassName('demo-info')[0]; + + demoInfo.appendChild(uiDom); +} \ No newline at end of file diff --git a/plugin-packages/model/demo/src/hit-test.ts b/plugin-packages/model/demo/src/hit-test.ts index 27324a278..ff37c868b 100644 --- a/plugin-packages/model/demo/src/hit-test.ts +++ b/plugin-packages/model/demo/src/hit-test.ts @@ -1,4 +1,4 @@ -//@ts-nocheck +import type { Composition, Player } from '@galacean/effects'; import { Transform, spec, math } from '@galacean/effects'; import { ToggleItemBounding, CompositionHitTest } from '@galacean/effects-plugin-model'; import { LoaderImplEx, InputController } from '../../src/helper'; @@ -6,17 +6,12 @@ import { createSlider } from './utility'; const { Sphere, Vector3, Box3 } = math; -let player; - -let inputController; - +let player: Player; +let inputController: InputController; let currentTime = 0.1; - -let composition; - -let playScene; - -let url; +let composition: Composition; +let playScene: spec.JSONScene; +let url: string; url = 'https://gw.alipayobjects.com/os/bmw-prod/2b867bc4-0e13-44b8-8d92-eb2db3dfeb03.glb'; url = 'https://gw.alipayobjects.com/os/gltf-asset/89748482160728/DamagedHelmet.glb'; @@ -66,7 +61,7 @@ async function getCurrentScene () { return loader.getLoadResult().jsonScene; } -export async function loadScene (inPlayer) { +export async function loadScene (inPlayer: Player) { if (!player) { player = inPlayer; registerMouseEvent(); @@ -75,9 +70,10 @@ export async function loadScene (inPlayer) { if (!playScene) { playScene = await getCurrentScene(); } else { - playScene.compositions[0].items.forEach(item => { + playScene.items.forEach(item => { if (item.id === 'extra-camera') { - item.transform = player.compositions[0].camera; + // @ts-expect-error + item.transform = player.getCompositions()[0].camera; } }); } @@ -133,11 +129,11 @@ function registerMouseEvent () { } function refreshCamera () { - const freeCamera = playScene.items.find(item => item.name === 'extra-camera'); - const position = player.compositions[0].camera.position; - const quat = player.compositions[0].camera.getQuat(); + const freeCamera = playScene.items.find(item => item.name === 'extra-camera') as spec.VFXItemData; + const position = player.getCompositions()[0].camera.position; + const quat = player.getCompositions()[0].camera.getQuat(); - if (quat[0] === null) { + if (quat.x === null) { return; } const transfrom = new Transform({ @@ -145,12 +141,14 @@ function refreshCamera () { quat: quat, }); - freeCamera.transform.position = transfrom.position; - freeCamera.transform.rotation = transfrom.rotation; + freeCamera.transform!.position = transfrom.position; + // @ts-expect-error + freeCamera.transform!.rotation = transfrom.rotation; } -function getHitTestCoord (e) { - const bounding = e.target.getBoundingClientRect(); +function getHitTestCoord (e: MouseEvent) { + const canvas = e.target as HTMLCanvasElement; + const bounding = canvas.getBoundingClientRect(); const x = ((e.clientX - bounding.left) / bounding.width) * 2 - 1; const y = 1 - ((e.clientY - bounding.top) / bounding.height) * 2; @@ -158,14 +156,12 @@ function getHitTestCoord (e) { } export function createUI () { - document.getElementsByClassName('container')[0].style.background = 'rgba(30,32,32)'; - // + const container = document.getElementsByClassName('container')[0] as HTMLElement; const uiDom = document.createElement('div'); - - uiDom.className = 'my_ui'; - const Label = document.createElement('label'); + container.style.background = 'rgba(30,32,32)'; + uiDom.className = 'my_ui'; Label.innerHTML = '

通过鼠标中键进行点击测试

'; uiDom.appendChild(Label); @@ -179,30 +175,3 @@ export function createUI () { demoInfo.appendChild(uiDom); } -function createSlider (name, minV, maxV, stepV, defaultV, callback, style) { - const InputDom = document.createElement('input'); - - InputDom.type = 'range'; - InputDom.min = minV.toString(); - InputDom.max = maxV.toString(); - InputDom.value = defaultV.toString(); - InputDom.step = stepV.toString(); - InputDom.style = style; - InputDom.addEventListener('input', function (event) { - const dom = event.target; - - Label.innerHTML = dom.value; - callback(Number(dom.value)); - }); - const divDom = document.createElement('div'); - - divDom.innerHTML = name; - divDom.appendChild(InputDom); - const Label = document.createElement('label'); - - Label.innerHTML = defaultV.toString(); - divDom.appendChild(Label); - - return divDom; -} - diff --git a/plugin-packages/model/demo/src/index.ts b/plugin-packages/model/demo/src/index.ts index 2318cdd08..ccc6c5d52 100644 --- a/plugin-packages/model/demo/src/index.ts +++ b/plugin-packages/model/demo/src/index.ts @@ -1,17 +1,20 @@ -//@ts-nocheck import '@galacean/effects-plugin-model'; import { createPlayer } from './utility'; import * as json from './json'; import * as camera from './camera'; import * as hitTest from './hit-test'; +import * as renderMode from './render-mode'; +import * as editorMode from './editor-mode'; const demoMap = { json, camera, hitTest, + renderMode, + editorMode, }; -function getDemoIndex (idxOrModule) { +function getDemoIndex (idxOrModule: any) { for (let i = 0; i < demoArray.length; i++) { if (demoArray[i][1] === idxOrModule) { return i; @@ -25,8 +28,7 @@ function getDemoIndex (idxOrModule) { } // 1. 获取 dom 对象 -const menuEle = document.getElementById('J-menu'); -const demoInfoEle = document.getElementById('J-demoInfo'); +const menuEle = document.getElementById('J-menu') as HTMLElement; const demoArray = Object.entries(demoMap); const storageKey = 'galacean-effects-plugin-model:demo-data'; let html = ''; @@ -48,16 +50,16 @@ const menuListEle = menuEle.querySelectorAll('.am-list-item'); menuListEle.forEach((item, i) => { item.addEventListener('click', () => { - localStorage.setItem(storageKey, i); + localStorage.setItem(storageKey, i.toString()); location.replace(location.href); }); }); // 4. 执行渲染逻辑 (async function doRender () { - const idx = getDemoIndex(localStorage.getItem(storageKey)); + const idx = getDemoIndex(localStorage.getItem(storageKey) ?? ''); const [name, demoInstance] = demoArray[idx]; - const env = demoInstance.getEnv ? demoInstance.getEnv() : 'editor'; + const env = 'getEnv' in demoInstance ? demoInstance.getEnv() : 'editor'; // 添加选中样式 menuListEle.forEach((ele, i) => { diff --git a/plugin-packages/model/demo/src/json.ts b/plugin-packages/model/demo/src/json.ts index 2cb249397..356420068 100644 --- a/plugin-packages/model/demo/src/json.ts +++ b/plugin-packages/model/demo/src/json.ts @@ -1,15 +1,15 @@ -//@ts-nocheck +import type { Player } from '@galacean/effects'; import { isObject } from '@galacean/effects'; import { getPMeshList, getRendererGPUInfo } from '@galacean/effects-plugin-model'; import { createButton, createPlayer, disposePlayer, createSlider, loadJsonFromURL } from './utility'; import { JSONConverter } from '@galacean/effects-plugin-model'; -let player; +let player: Player; let pending = false; let currentTime = 0; let pauseOnFirstFrame = false; -let infoElement; +let infoElement: HTMLLabelElement; let url = 'https://mdn.alipayobjects.com/mars/afts/file/A*SERYRaes5S0AAAAAAAAAAAAADlB4AQ'; // 兔子 @@ -53,7 +53,7 @@ async function getCurrentScene () { }); } -export async function loadScene (inPlayer) { +export async function loadScene (inPlayer: Player) { if (!player) { player = inPlayer; addRealTimeTicker(); @@ -87,10 +87,10 @@ export async function loadScene (inPlayer) { } export function createUI () { - document.getElementsByClassName('container')[0].style.background = 'rgba(30,32,32)'; - + const container = document.getElementsByClassName('container')[0] as HTMLElement; const parentElement = document.createElement('div'); + container.style.background = 'rgba(30,32,32)'; parentElement.className = 'my_ui'; createButton(parentElement, '重播', async function (ev) { @@ -107,6 +107,7 @@ export function createUI () { if (player !== undefined) { disposePlayer(player); } + // @ts-expect-error player = undefined; }); @@ -157,7 +158,10 @@ function addRealTimeTicker () { skinTextureMode = true; } mesh.subMeshes.forEach(subMesh => { - if (subMesh.jointMatrixTexture?.isHalfFloat) { skinHalfFloat = true; } + // @ts-expect-error + if (subMesh.jointMatrixTexture?.isHalfFloat) { + skinHalfFloat = true; + } }); }); infoList.push(`

蒙皮信息: HalfFloat ${skinHalfFloat}, 纹理模式 ${skinTextureMode}

`); diff --git a/plugin-packages/model/demo/src/render-mode.ts b/plugin-packages/model/demo/src/render-mode.ts new file mode 100644 index 000000000..95cd6271c --- /dev/null +++ b/plugin-packages/model/demo/src/render-mode.ts @@ -0,0 +1,290 @@ +import type { Player } from '@galacean/effects'; +import { math, spec } from '@galacean/effects'; +import { CameraGestureHandlerImp } from '@galacean/effects-plugin-model'; +import { LoaderImplEx } from '../../src/helper'; + +const { Sphere, Vector3, Box3 } = math; + +let player: Player; + +let sceneAABB; +let sceneCenter; +let sceneRadius = 1; + +let gestureHandler: CameraGestureHandlerImp; +let mouseDown = false; +let scaleBegin = false; +let rotationBegin = false; + +let playScene: spec.JSONScene; + +let url = 'https://gw.alipayobjects.com/os/gltf-asset/89748482160728/fish_test.glb'; + +url = 'https://gw.alipayobjects.com/os/gltf-asset/89748482160728/DamagedHelmet.glb'; + +let renderMode3D = spec.RenderMode3D.diffuse; + +async function getCurrentScene () { + const duration = 99999; + const loader = new LoaderImplEx(); + const loadResult = await loader.loadScene({ + gltf: { + resource: url, + }, + effects: { + duration: duration, + endBehavior: spec.EndBehavior.restart, + playAnimation: 0, + }, + }); + + const sceneMin = Vector3.fromArray(loadResult.sceneAABB.min); + const sceneMax = Vector3.fromArray(loadResult.sceneAABB.max); + + sceneAABB = new Box3(sceneMin, sceneMax); + sceneRadius = sceneAABB.getBoundingSphere(new Sphere()).radius; + sceneCenter = sceneAABB.getCenter(new Vector3()); + const position = sceneCenter.add(new Vector3(0, 0, sceneRadius * 3)); + + loader.addCamera({ + near: 0.1, + far: 5000, + fov: 60, + clipMode: 0, + // + name: 'extra-camera', + duration: duration, + endBehavior: spec.EndBehavior.restart, + position: position.toArray(), + rotation: [0, 0, 0], + }); + + loader.addLight({ + lightType: spec.LightType.ambient, + color: { r: 1, g: 1, b: 1, a: 1 }, + intensity: 0.2, + // + name: 'ambient-light', + position: [0, 0, 0], + rotation: [0, 0, 0], + scale: [1, 1, 1], + duration: duration, + endBehavior: spec.EndBehavior.restart, + }); + + loader.addLight({ + lightType: spec.LightType.directional, + color: { r: 1, g: 1, b: 1, a: 1 }, + intensity: 0.9, + followCamera: true, + // + name: 'main-light', + position: [0, 0, 0], + rotation: [30, 325, 0], + scale: [1, 1, 1], + duration: duration, + endBehavior: spec.EndBehavior.restart, + }); + + const { jsonScene } = loader.getLoadResult(); + + loader.dispose(); + + return jsonScene; +} + +export async function loadScene (inPlayer: Player) { + if (!player) { + player = inPlayer; + registerMouseEvent(); + } + // + if (!playScene) { + playScene = await getCurrentScene(); + } else { + playScene.items.forEach(item => { + if (item.name === 'extra-camera') { + const camera = player.getCompositions()[0].camera; + + item.transform = { + position: camera.position.clone(), + eulerHint: camera.rotation.clone(), + scale: { x: 1, y: 1, z: 1 }, + }; + } + }); + } + + player.destroyCurrentCompositions(); + const loadOptions = { + pluginData: { + renderMode3D: renderMode3D, + renderMode3DUVGridSize: 1 / 12, + }, + }; + + return player.loadScene(playScene, loadOptions).then(async comp => { + gestureHandler = new CameraGestureHandlerImp(comp); + + return true; + }); +} + +function registerMouseEvent () { + player.canvas.addEventListener('mousedown', function (e) { + if (!gestureHandler) { + return; + } + + mouseDown = true; + if (e.buttons === 1) { + rotationBegin = true; + } else if (e.buttons === 4) { + scaleBegin = true; + } + }); + + player.canvas.addEventListener('mousemove', async function (e) { + if (gestureHandler && mouseDown) { + if (e.buttons === 1) { + if (rotationBegin) { + gestureHandler.onRotatePointBegin( + e.clientX, + e.clientY, + player.canvas.width / 2, + player.canvas.height / 2, + [0, 0, 0], + 'extra-camera' + ); + rotationBegin = false; + } + + gestureHandler.onRotatingPoint(e.clientX, e.clientY); + refreshCamera(); + } else if (e.buttons === 4) { + if (scaleBegin) { + gestureHandler.onZMoveBegin( + e.clientX, + e.clientY, + player.canvas.width / 2, + player.canvas.height / 2, + 'extra-camera' + ); + scaleBegin = false; + } + + gestureHandler.onZMoving(e.clientX, e.clientY); + refreshCamera(); + } + } + }); + + player.canvas.addEventListener('mouseup', async function (e) { + mouseDown = false; + if (gestureHandler) { + if (e.buttons === 1) { + gestureHandler.onRotatePointEnd(); + } else if (e.buttons === 4) { + gestureHandler.onZMoveEnd(); + } + } + }); + + player.canvas.addEventListener('mouseleave', async function (e) { + mouseDown = false; + if (gestureHandler) { + if (e.buttons === 1) { + gestureHandler.onRotatePointEnd(); + } else if (e.buttons === 4) { + gestureHandler.onZMoveEnd(); + } + } + }); + + player.canvas.addEventListener('wheel', function (e) { + if (gestureHandler) { + gestureHandler.onKeyEvent({ + cameraID: 'extra-camera', + zAxis: e.deltaY > 0 ? 1 : -1, + speed: sceneRadius * 0.15, + }); + } + }); +} + +function refreshCamera () { + const freeCamera = playScene.items.find(item => item.name === 'extra-camera') as spec.VFXItemData; + const position = player.getCompositions()[0].camera.position; + const rotation = player.getCompositions()[0].camera.rotation; + + if (rotation.x === null) { + return; + } + + freeCamera.transform!.position = position; + // @ts-expect-error + freeCamera.transform!.rotation = rotation; +} + +export function createUI () { + const container = document.getElementsByClassName('container')[0] as HTMLElement; + const uiDom = document.createElement('div'); + const select = document.createElement('select'); + + container.style.background = 'rgba(182,217,241)'; + uiDom.className = 'my_ui'; + select.innerHTML = ` + + + + + + + + + + + `; + for (let i = 0; i < select.options.length; i++) { + const option = select.options[i]; + + if (option.value === renderMode3D) { + select.selectedIndex = i; + + break; + } + } + + select.onchange = async function (e) { + const element = e.target as HTMLSelectElement; + + if (element.value === 'none') { + renderMode3D = spec.RenderMode3D.none; + } else if (element.value === 'diffuse') { + renderMode3D = spec.RenderMode3D.diffuse; + } else if (element.value === 'uv') { + renderMode3D = spec.RenderMode3D.uv; + } else if (element.value === 'normal') { + renderMode3D = spec.RenderMode3D.normal; + } else if (element.value === 'basecolor') { + renderMode3D = spec.RenderMode3D.basecolor; + } else if (element.value === 'alpha') { + renderMode3D = spec.RenderMode3D.alpha; + } else if (element.value === 'metallic') { + renderMode3D = spec.RenderMode3D.metallic; + } else if (element.value === 'roughness') { + renderMode3D = spec.RenderMode3D.roughness; + } else if (element.value === 'ao') { + renderMode3D = spec.RenderMode3D.ao; + } else if (element.value === 'emissive') { + renderMode3D = spec.RenderMode3D.emissive; + } + + await loadScene(player); + }; + uiDom.appendChild(select); + + const demoInfo = document.getElementsByClassName('demo-info')[0]; + + demoInfo.appendChild(uiDom); +} diff --git a/plugin-packages/model/package.json b/plugin-packages/model/package.json index b06c5a396..89ebce7eb 100644 --- a/plugin-packages/model/package.json +++ b/plugin-packages/model/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@galacean/effects": "workspace:*", "@galacean/effects-plugin-editor-gizmo": "workspace:*", - "@vvfx/resource-detection": "^0.6.2", + "@vvfx/resource-detection": "^0.7.0", "@types/hammerjs": "^2.0.45", "fpsmeter": "^0.3.1", "hammerjs": "^2.0.8" diff --git a/plugin-packages/model/src/gesture/index.ts b/plugin-packages/model/src/gesture/index.ts index 2a92b586c..064089c3f 100644 --- a/plugin-packages/model/src/gesture/index.ts +++ b/plugin-packages/model/src/gesture/index.ts @@ -29,6 +29,10 @@ export class CameraGestureHandlerImp implements CameraGestureHandler { private composition: Composition, ) { } + updateComposition (composition: Composition) { + this.composition = composition; + } + getItem () { return this.composition.items?.find(item => item.name === this.getCurrentTarget()); } diff --git a/plugin-packages/model/src/gltf/json-converter.ts b/plugin-packages/model/src/gltf/json-converter.ts index 324dde177..d14dc6912 100644 --- a/plugin-packages/model/src/gltf/json-converter.ts +++ b/plugin-packages/model/src/gltf/json-converter.ts @@ -43,6 +43,13 @@ export class JSONConverter { const oldBinUrls = oldScene.bins ?? []; const binFiles: ArrayBuffer[] = []; + //@ts-expect-error + const v = sceneJSON.version.split('.'); + + if (Number(v[0]) >= 3) { + return oldScene; + } + if (oldScene.bins) { for (const bin of oldScene.bins) { binFiles.push(await this.loadBins(bin.url)); @@ -166,7 +173,6 @@ export class JSONConverter { this.createItemsFromTreeComponent(comp, newScene, oldScene); treeComp.options.tree.animation = undefined; treeComp.options.tree.animations = undefined; - newComponents.push(comp); } else if (comp.dataType !== spec.DataType.MeshComponent) { newComponents.push(comp); } @@ -622,7 +628,7 @@ export class JSONConverter { animationComponent.animationClips.push({ id: clipData.id }); }); } - + treeItem.components = []; treeItem.components.push({ id: animationComponent.id }); newScene.components.push(animationComponent); } diff --git a/plugin-packages/model/src/gltf/loader-impl.ts b/plugin-packages/model/src/gltf/loader-impl.ts index 4dd1f9afd..877139b09 100644 --- a/plugin-packages/model/src/gltf/loader-impl.ts +++ b/plugin-packages/model/src/gltf/loader-impl.ts @@ -12,17 +12,16 @@ import type { ModelAnimTrackOptions, ModelMaterialOptions, ModelSkyboxOptions, - ModelTreeOptions, ModelTextureTransform, } from '../index'; import { - Vector3, Box3, Matrix4, Euler, PSkyboxCreator, PSkyboxType, UnlitShaderGUID, PBRShaderGUID, + Vector3, Box3, Euler, PSkyboxCreator, PSkyboxType, UnlitShaderGUID, PBRShaderGUID, } from '../runtime'; import { LoaderHelper } from './loader-helper'; import { WebGLHelper } from '../utility'; -import type { PImageBufferData, PSkyboxBufferParams } from '../runtime/skybox'; +import type { PImageBufferData, PSkyboxBufferParams, PSkyboxURLParams } from '../runtime/skybox'; import type { - GLTFSkin, GLTFMesh, GLTFImage, GLTFMaterial, GLTFTexture, GLTFScene, GLTFLight, + GLTFMesh, GLTFImage, GLTFMaterial, GLTFTexture, GLTFLight, GLTFCamera, GLTFAnimation, GLTFResources, GLTFImageBasedLight, GLTFPrimitive, GLTFBufferAttribute, GLTFBounds, GLTFTextureInfo, } from '@vvfx/resource-detection'; @@ -53,12 +52,7 @@ type Box3 = math.Box3; export class LoaderImpl implements Loader { private sceneOptions: LoadSceneOptions; private loaderOptions: LoaderOptions; - private gltfScene: GLTFScene; - private gltfSkins: GLTFSkin[] = []; private gltfMeshs: GLTFMesh[] = []; - private gltfLights: GLTFLight[] = []; - private gltfCameras: GLTFCamera[] = []; - private gltfImages: GLTFImage[] = []; private gltfTextures: GLTFTexture[] = []; private gltfMaterials: GLTFMaterial[] = []; private gltfAnimations: GLTFAnimation[] = []; @@ -87,7 +81,7 @@ export class LoaderImpl implements Loader { this.composition = { id: '1', name: 'test1', - duration: 9999, + duration: 99999, endBehavior: spec.EndBehavior.restart, camera: { fov: 45, @@ -128,12 +122,7 @@ export class LoaderImpl implements Loader { })); this.processGLTFResource(gltfResource, this.imageElements); - this.gltfScene = gltfResource.scenes[0]; - this.gltfSkins = this.gltfScene.skins; this.gltfMeshs = gltfResource.meshes; - this.gltfLights = this.gltfScene.lights; - this.gltfCameras = this.gltfScene.cameras; - this.gltfImages = gltfResource.images; this.gltfTextures = gltfResource.textures; this.gltfMaterials = gltfResource.materials; this.gltfAnimations = gltfResource.animations; @@ -607,6 +596,7 @@ export class LoaderImpl implements Loader { range: data.range, innerConeAngle: data.innerConeAngle, outerConeAngle: data.outerConeAngle, + followCamera: data.followCamera, }; const item: spec.VFXItemData = { id: itemId, @@ -692,7 +682,60 @@ export class LoaderImpl implements Loader { this.components.push(component); } - async tryAddSkybox (skybox: ModelSkybox) { + addSkybox (skybox: PSkyboxURLParams) { + const itemId = generateGUID(); + const skyboxInfo = PSkyboxCreator.createSkyboxComponentData(skybox); + const { imageList, textureOptionsList, component } = skyboxInfo; + + component.item.id = itemId; + if (skybox.intensity !== undefined) { + component.intensity = skybox.intensity; + } + if (skybox.reflectionsIntensity !== undefined) { + component.reflectionsIntensity = skybox.reflectionsIntensity; + } + component.renderable = skybox.renderable ?? false; + + const item: spec.VFXItemData = { + id: itemId, + name: 'Skybox-Customize', + duration: 999, + type: spec.ItemType.skybox, + pn: 0, + visible: true, + endBehavior: spec.EndBehavior.freeze, + transform: { + position: { + x: 0, + y: 0, + z: 0, + }, + eulerHint: { + x: 0, + y: 0, + z: 0, + }, + scale: { + x: 1, + y: 1, + z: 1, + }, + }, + components: [ + { id: component.id }, + ], + content: {}, + dataType: spec.DataType.VFXItemData, + }; + + this.images.push(...imageList); + // @ts-expect-error + this.textures.push(...textureOptionsList); + this.items.push(item); + this.components.push(component); + } + + private async tryAddSkybox (skybox: ModelSkybox) { if (this.gltfImageBasedLights.length > 0 && !this.ignoreSkybox()) { const ibl = this.gltfImageBasedLights[0]; @@ -762,14 +805,28 @@ export class LoaderImpl implements Loader { return PSkyboxCreator.createSkyboxComponentData(params); } - private clear () { + dispose () { + this.clear(); + // @ts-expect-error + this.engine = null; + } + + clear () { + this.gltfMeshs = []; + this.gltfTextures = []; + this.gltfMaterials = []; + this.gltfAnimations = []; + this.gltfImageBasedLights = []; + this.images = []; + this.imageElements = []; this.textures = []; this.items = []; this.components = []; this.materials = []; this.shaders = []; this.geometries = []; + this.animations = []; } private computeSceneAABB () { @@ -821,52 +878,6 @@ export class LoaderImpl implements Loader { return sceneAABB; } - /** - * 按照传入的动画播放参数,计算需要播放的动画索引 - * - * @param treeOptions 节点树属性,需要初始化animations列表。 - * @returns 返回计算的动画索引,-1表示没有动画需要播放,-88888888表示播放所有动画。 - */ - getPlayAnimationIndex (treeOptions: ModelTreeOptions): number { - const animations = treeOptions.animations; - - if (animations === undefined || animations.length <= 0) { - // 硬编码,内部指定的不播放动画的索引值 - return -1; - } - - if (this.isPlayAllAnimation()) { - // 硬编码,内部指定的播放全部动画的索引值 - return -88888888; - } - - const animationInfo = this.sceneOptions.effects.playAnimation; - - if (animationInfo === undefined) { - return -1; - } - - if (typeof animationInfo === 'number') { - if (animationInfo >= 0 && animationInfo < animations.length) { - return animationInfo; - } else { - return -1; - } - } else { - // typeof animationInfo === 'string' - let animationIndex = -1; - - // 通过动画名字查找动画索引 - animations.forEach((anim, index) => { - if (anim.name === animationInfo) { - animationIndex = index; - } - }); - - return animationIndex; - } - } - isPlayAnimation (): boolean { return this.sceneOptions.effects.playAnimation !== undefined; } @@ -981,62 +992,6 @@ export class LoaderImpl implements Loader { }); } - createTreeOptions (scene: GLTFScene): ModelTreeOptions { - const nodeList = scene.nodes.map((node, nodeIndex) => { - const children = node.children.map(child => { - if (child.nodeIndex === undefined) { throw new Error(`Undefined nodeIndex for child ${child}`); } - - return child.nodeIndex; - }); - let pos: spec.vec3 | undefined; - let quat: spec.vec4 | undefined; - let scale: spec.vec3 | undefined; - - if (node.matrix !== undefined) { - if (node.matrix.length !== 16) { throw new Error(`Invalid matrix length ${node.matrix.length} for node ${node}`); } - const mat = Matrix4.fromArray(node.matrix); - const transform = mat.getTransform(); - - pos = transform.translation.toArray(); - quat = transform.rotation.toArray(); - scale = transform.scale.toArray(); - } else { - if (node.translation !== undefined) { pos = node.translation as spec.vec3; } - if (node.rotation !== undefined) { quat = node.rotation as spec.vec4; } - if (node.scale !== undefined) { scale = node.scale as spec.vec3; } - } - node.nodeIndex = nodeIndex; - const treeNode: spec.TreeNodeOptions = { - name: node.name, - transform: { - position: pos, - quat: quat, - scale: scale, - }, - children: children, - id: `${node.nodeIndex}`, - // id: index, id不指定就是index,指定后就是指定的值 - }; - - return treeNode; - }); - - const rootNodes = scene.rootNodes.map(root => { - if (root.nodeIndex === undefined) { throw new Error(`Undefined nodeIndex for root ${root}`); } - - return root.nodeIndex; - }); - - const treeOptions: ModelTreeOptions = { - nodes: nodeList, - children: rootNodes, - animation: -1, - animations: [], - }; - - return treeOptions; - } - createAnimations (animations: GLTFAnimation[]): ModelAnimationOptions[] { return animations.map(anim => { const tracks = anim.channels.map(channel => { @@ -1266,7 +1221,7 @@ export function getDefaultUnlitMaterialData (): spec.MaterialData { }, 'macros': [], 'shader': { - 'id': 'unlit000000000000000000000000000', + 'id': spec.BuiltinObjectGUID.UnlitShader, }, 'ints': { diff --git a/plugin-packages/model/src/gltf/protocol.ts b/plugin-packages/model/src/gltf/protocol.ts index 758ea26f2..e2881a141 100644 --- a/plugin-packages/model/src/gltf/protocol.ts +++ b/plugin-packages/model/src/gltf/protocol.ts @@ -3,7 +3,6 @@ import type { GLTFMaterial, GLTFPrimitive, GLTFLight, - GLTFScene, GLTFImage, GLTFTexture, GLTFCamera, @@ -19,7 +18,6 @@ import type { ModelAnimationOptions, ModelMaterialOptions, ModelSkyboxOptions, - ModelTreeOptions, ModelLightComponentData, ModelCameraComponentData, ModelSkyboxComponentData, } from '../index'; @@ -108,6 +106,7 @@ export interface ModelLight { range?: number, innerConeAngle?: number, outerConeAngle?: number, + followCamera?: boolean, // name: string, position: spec.vec3, @@ -155,8 +154,6 @@ export interface Loader { processMaterial (materials: GLTFMaterial[], fromGLTF: boolean): void, - createTreeOptions (scene: GLTFScene): ModelTreeOptions, - createAnimations (animations: GLTFAnimation[]): ModelAnimationOptions[], createGeometry (primitive: GLTFPrimitive, hasSkinAnim: boolean): Geometry, diff --git a/plugin-packages/model/src/index.ts b/plugin-packages/model/src/index.ts index 1ec7bc35a..bcb6e30db 100644 --- a/plugin-packages/model/src/index.ts +++ b/plugin-packages/model/src/index.ts @@ -1,11 +1,13 @@ import * as EFFECTS from '@galacean/effects'; import type { spec } from '@galacean/effects'; import { VFXItem, logger, registerPlugin } from '@galacean/effects'; -import { ModelPlugin, ModelTreePlugin } from './plugin'; +import { ModelPlugin } from './plugin'; -registerPlugin('tree', ModelTreePlugin, VFXItem, true); registerPlugin('model', ModelPlugin, VFXItem); +/** + * 插件版本号 + */ export const version = __VERSION__; export type BaseTransform = spec.BaseItemTransform; @@ -28,7 +30,6 @@ export type ModelLightOptions = spec.ModelLightOptions; export type ModelItemMesh = spec.ModelMeshItem<'studio'>; export type ModelItemSkybox = spec.ModelSkyboxItem<'studio'>; -export type ModelItemTree = spec.ModelTreeItem<'studio'>; export type ModelMeshContent = spec.ModelMeshItemContent<'studio'>; export type ModelSkyboxContent = spec.SkyboxContent<'studio'>; export type ModelMeshOptions = spec.ModelMeshOptions<'studio'>; diff --git a/plugin-packages/model/src/plugin/index.ts b/plugin-packages/model/src/plugin/index.ts index 708c9ee4c..88fa1dc3f 100644 --- a/plugin-packages/model/src/plugin/index.ts +++ b/plugin-packages/model/src/plugin/index.ts @@ -1,5 +1,4 @@ export * from './const'; export * from './model-plugin'; export * from './model-item'; -export * from './model-tree-item'; -export * from './model-tree-plugin'; +export * from './model-tree-component'; diff --git a/plugin-packages/model/src/plugin/model-item.ts b/plugin-packages/model/src/plugin/model-item.ts index f753a0e28..d4ab674e8 100644 --- a/plugin-packages/model/src/plugin/model-item.ts +++ b/plugin-packages/model/src/plugin/model-item.ts @@ -58,7 +58,7 @@ export class ModelMeshComponent extends RendererComponent { /** * 组件开始,需要创建内部对象,更新父元素信息和添加到场景管理器中 */ - override start (): void { + override onStart (): void { this.sceneManager = getSceneManager(this); this.createContent(); this.item.type = VFX_ITEM_TYPE_3D; @@ -75,7 +75,7 @@ export class ModelMeshComponent extends RendererComponent { * 组件更新,更新内部对象状态 * @param dt - 更新间隔 */ - override update (dt: number): void { + override onUpdate (dt: number): void { if (this.sceneManager) { this.content.build(this.sceneManager); } @@ -87,7 +87,7 @@ export class ModelMeshComponent extends RendererComponent { * 组件晚更新,晚更新内部对象状态 * @param dt - 更新间隔 */ - override lateUpdate (dt: number): void { + override onLateUpdate (dt: number): void { this.content.lateUpdate(); } @@ -271,7 +271,7 @@ export class ModelSkyboxComponent extends RendererComponent { /** * 组件开始,需要创建内部对象和添加到场景管理器中 */ - override start (): void { + override onStart (): void { this.createContent(); this.item.type = VFX_ITEM_TYPE_3D; this.priority = this.item.renderOrder; @@ -368,7 +368,7 @@ export class ModelLightComponent extends Behaviour { /** * 组件开始,需要创建内部对象和添加到场景管理器中 */ - override start (): void { + override onStart (): void { this.createContent(); this.item.type = VFX_ITEM_TYPE_3D; const scene = getSceneManager(this); @@ -381,7 +381,7 @@ export class ModelLightComponent extends Behaviour { * 组件更新,更新内部对象状态 * @param dt - 更新间隔 */ - override update (dt: number): void { + override onUpdate (dt: number): void { this.content.update(); } @@ -458,7 +458,7 @@ export class ModelCameraComponent extends Behaviour { /** * 组件开始,需要创建内部对象和添加到场景管理器中 */ - override start (): void { + override onStart (): void { this.createContent(); this.item.type = VFX_ITEM_TYPE_3D; const scene = getSceneManager(this); @@ -471,7 +471,7 @@ export class ModelCameraComponent extends Behaviour { * 组件更新,更新内部对象状态 * @param dt - 更新间隔 */ - override update (dt: number): void { + override onUpdate (dt: number): void { this.content.update(); this.updateMainCamera(); } @@ -564,7 +564,7 @@ export class AnimationComponent extends Behaviour { /** * 组件开始,需要创建内部对象和添加到场景管理器中 */ - override start (): void { + override onStart (): void { this.elapsedTime = 0; this.item.type = VFX_ITEM_TYPE_3D; } @@ -573,7 +573,7 @@ export class AnimationComponent extends Behaviour { * 组件更新,更新内部对象状态 * @param dt - 更新间隔 */ - override update (dt: number): void { + override onUpdate (dt: number): void { this.elapsedTime += dt * 0.001; if (this.animation >= 0 && this.animation < this.clips.length) { this.clips[this.animation].sampleAnimation(this.item, this.elapsedTime); diff --git a/plugin-packages/model/src/plugin/model-plugin.ts b/plugin-packages/model/src/plugin/model-plugin.ts index 81c4cb32a..f94cabf4f 100644 --- a/plugin-packages/model/src/plugin/model-plugin.ts +++ b/plugin-packages/model/src/plugin/model-plugin.ts @@ -210,7 +210,7 @@ export class ModelPluginComponent extends Behaviour { * 组件后更新,合成相机和场景管理器更新 * @param dt - 更新间隔 */ - override lateUpdate (dt: number): void { + override onLateUpdate (dt: number): void { const composition = this.item.composition as Composition; if (this.autoAdjustScene && this.scene.tickCount == 1) { diff --git a/plugin-packages/model/src/plugin/model-tree-component.ts b/plugin-packages/model/src/plugin/model-tree-component.ts new file mode 100644 index 000000000..96ace358b --- /dev/null +++ b/plugin-packages/model/src/plugin/model-tree-component.ts @@ -0,0 +1,38 @@ +import type { Engine } from '@galacean/effects'; +import { Behaviour, effectsClass, spec } from '@galacean/effects'; +import type { ModelTreeContent } from '../index'; + +/** + * 插件场景树组件类,实现 3D 场景树功能 + * + * FIXME: 有些发布的新JSON包含TreeComponent,这里做兼容处理,否则会报错 + * @since 2.1.0 + */ +@effectsClass(spec.DataType.TreeComponent) +export class ModelTreeComponent extends Behaviour { + /** + * 参数 + */ + options?: ModelTreeContent; + + /** + * 构造函数,创建节点树元素 + * @param engine + * @param options + */ + constructor (engine: Engine, options?: ModelTreeContent) { + super(engine); + if (options) { + this.fromData(options); + } + } + + /** + * 反序列化,保存入参和创建节点树元素 + * @param options + */ + override fromData (options: ModelTreeContent): void { + super.fromData(options); + this.options = options; + } +} diff --git a/plugin-packages/model/src/plugin/model-tree-item.ts b/plugin-packages/model/src/plugin/model-tree-item.ts deleted file mode 100644 index b2680e1f9..000000000 --- a/plugin-packages/model/src/plugin/model-tree-item.ts +++ /dev/null @@ -1,272 +0,0 @@ -import type { Engine, VFXItem } from '@galacean/effects'; -import { Behaviour, Transform, effectsClass, spec } from '@galacean/effects'; -import type { ModelTreeContent, ModelTreeOptions } from '../index'; -import { PAnimationManager } from '../runtime'; -import { getSceneManager } from './model-plugin'; - -/** - * 场景树节点描述 - */ -export interface ModelTreeNode { - /** - * 名称 - */ - name?: string, - /** - * 变换 - */ - transform: Transform, - /** - * 子节点 - */ - children: ModelTreeNode[], - /** - * 索引 - */ - id: string, - /** - * 场景树元素 - */ - tree: ModelTreeItem, -} - -/** - * 场景树元素类,支持插件中节点树相关的动画能力 - */ -export class ModelTreeItem { - private allNodes: ModelTreeNode[]; - private nodes: ModelTreeNode[]; - private cacheMap: Record; - /** - * 基础变换 - */ - readonly baseTransform: Transform; - /** - * 动画管理器 - */ - animationManager: PAnimationManager; - - /** - * 构造函数,创建场景树结构 - * @param props - 场景树数据 - * @param owner - 场景树元素 - */ - constructor (props: ModelTreeOptions, owner: VFXItem) { - this.baseTransform = owner.transform; - this.animationManager = new PAnimationManager(props, owner); - this.build(props); - } - - /** - * 场景树更新,主要是动画更新 - * @param dt - 时间间隔 - */ - tick (dt: number) { - this.animationManager.tick(dt); - } - - /** - * 获取所有节点 - * @returns - */ - getNodes () { - return this.nodes; - } - - /** - * 根据节点编号,查询节点 - * @param nodeId - 节点编号 - * @returns - */ - getNodeById (nodeId: string | number): ModelTreeNode | undefined { - const cache = this.cacheMap; - - if (!cache[nodeId]) { - const index = `^${nodeId}`; - - // @ts-expect-error - cache[nodeId] = this.allNodes.find(node => node.id === index); - } - - return cache[nodeId]; - } - - /** - * 根据节点名称,查询节点 - * @param name - 名称 - * @returns - */ - getNodeByName (name: string): ModelTreeNode | undefined { - const cache = this.cacheMap; - - if (!cache[name]) { - // @ts-expect-error - cache[name] = this.allNodes.find(node => node.name === name); - } - - return cache[name]; - } - - /** - * 根据节点 id 查询节点变换,如果查询不到节点就直接返回基础变换 - * @param nodeId - 节点 id - * @returns - */ - getNodeTransform (nodeId: string): Transform { - const node = this.getNodeById(nodeId); - - return node ? node.transform : this.baseTransform; - } - - /** - * 销毁场景树对象 - */ - dispose () { - this.allNodes = []; - this.nodes = []; - this.cacheMap = {}; - // @ts-expect-error - this.baseTransform = null; - this.animationManager?.dispose(); - // @ts-expect-error - this.animationManager = null; - } - - private build (options: ModelTreeOptions) { - const topTransform = this.baseTransform; - const nodes: ModelTreeNode[] = options.nodes.map((node, i) => ({ - name: node.name || node.id || (i + ''), - transform: new Transform({ - ...node.transform, - valid: true, - }, topTransform), - id: `^${node.id || i}`, - children: [], - tree: this, - })); - - this.cacheMap = {}; - nodes.forEach((node, i) => { - const children = options.nodes[i].children; - - // @ts-expect-error - node.transform.name = node.name; - node.transform.setValid(true); - if (children) { - children.forEach(function (index) { - const child = nodes[index]; - - if (child && child !== node) { - if (child.transform.parentTransform !== topTransform) { - console.error('Node parent has been set.'); - } - child.transform.parentTransform = node.transform; - node.children.push(child); - } - }); - } - }); - this.allNodes = nodes; - this.nodes = options.children.map(i => nodes[i]); - } -} - -/** - * 插件场景树组件类,实现 3D 场景树功能 - * @since 2.0.0 - */ -@effectsClass(spec.DataType.TreeComponent) -export class ModelTreeComponent extends Behaviour { - /** - * 内部节点树元素 - */ - content: ModelTreeItem; - /** - * 参数 - */ - options?: ModelTreeContent; - - /** - * 构造函数,创建节点树元素 - * @param engine - * @param options - */ - constructor (engine: Engine, options?: ModelTreeContent) { - super(engine); - if (options) { - this.fromData(options); - } - } - - /** - * 反序列化,保存入参和创建节点树元素 - * @param options - */ - override fromData (options: ModelTreeContent): void { - super.fromData(options); - this.options = options; - this.createContent(); - } - - /** - * 组件开始,查询合成中场景管理器并设置到动画管理器中 - */ - override start () { - this.item.type = spec.ItemType.tree; - this.content.baseTransform.setValid(true); - const sceneManager = getSceneManager(this); - - if (sceneManager) { - this.content.animationManager.setSceneManager(sceneManager); - } - } - - /** - * 组件更新,内部对象更新 - * @param dt - */ - override update (dt: number): void { - // this.timeline?.getRenderData(time, true); - // TODO: 需要使用lifetime - this.content?.tick(dt); - } - - /** - * 组件销毁,内部对象销毁 - */ - override onDestroy (): void { - this.content?.dispose(); - } - - /** - * 创建内部场景树元素 - */ - createContent () { - if (this.options) { - const treeOptions = this.options.options.tree; - - this.content = new ModelTreeItem(treeOptions, this.item); - } - } - - /** - * 获取元素的变换 - * @param itemId - 元素索引 - * @returns - */ - getNodeTransform (itemId: string): Transform { - if (this.content === undefined) { - return this.transform; - } - - const idWithSubfix = this.item.id + '^'; - - if (itemId.indexOf(idWithSubfix) === 0) { - const nodeId = itemId.substring(idWithSubfix.length); - - return this.content.getNodeTransform(nodeId); - } else { - return this.transform; - } - } -} diff --git a/plugin-packages/model/src/plugin/model-tree-plugin.ts b/plugin-packages/model/src/plugin/model-tree-plugin.ts deleted file mode 100644 index 0c494b3a5..000000000 --- a/plugin-packages/model/src/plugin/model-tree-plugin.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AbstractPlugin } from '@galacean/effects'; - -/** - * 场景树插件类,支持 3D 相关的节点动画和骨骼动画等 - */ -export class ModelTreePlugin extends AbstractPlugin { - /** - * 插件名称 - */ - override name = 'tree'; - /** - * 高优先级更新 - */ - override order = 2; -} - diff --git a/plugin-packages/model/src/runtime/animation.ts b/plugin-packages/model/src/runtime/animation.ts index d81246b8c..8cf7e4aa0 100644 --- a/plugin-packages/model/src/runtime/animation.ts +++ b/plugin-packages/model/src/runtime/animation.ts @@ -1,18 +1,9 @@ import type { Geometry, Engine, VFXItem, SkinProps } from '@galacean/effects'; import { glContext, Texture, TextureSourceType } from '@galacean/effects'; -import type { - ModelAnimTrackOptions, - ModelAnimationOptions, - ModelTreeOptions, -} from '../index'; import { Matrix4 } from './math'; import { PObjectType } from './common'; import { PObject } from './object'; -import type { InterpolationSampler } from './anim-sampler'; -import { createAnimationSampler } from './anim-sampler'; import { Float16ArrayWrapper } from '../utility/plugin-helper'; -import type { PSceneManager } from './scene'; -import { ModelTreeComponent } from '../plugin'; const forceTextureSkinning = false; @@ -473,193 +464,6 @@ export enum PAnimPathType { weights, } -/** - * 动画轨道类 - */ -export class PAnimTrack { - /** - * 节点索引 - */ - node: number; - /** - * 时间数组 - */ - timeArray: Float32Array; - /** - * 数据数组 - */ - dataArray: Float32Array; - /** - * 路径类型 - */ - path = PAnimPathType.translation; - /** - * 插值类型 - */ - interp = PAnimInterpType.linear; - /** - * 分量 - */ - component: number; - // - private sampler?: InterpolationSampler; - - /** - * 创建动画轨道对象 - * @param options - 动画轨道参数 - */ - constructor (options: ModelAnimTrackOptions) { - const { node, input, output, path, interpolation } = options; - - this.node = node; - this.timeArray = input; - this.dataArray = output; - // - if (path === 'translation') { - this.path = PAnimPathType.translation; - this.component = 3; - } else if (path === 'rotation') { - this.path = PAnimPathType.rotation; - this.component = 4; - } else if (path === 'scale') { - this.path = PAnimPathType.scale; - this.component = 3; - } else if (path === 'weights') { - this.path = PAnimPathType.weights; - this.component = this.dataArray.length / this.timeArray.length; - // special checker for weights animation - if (this.component <= 0) { - console.error(`Invalid weights component: ${this.timeArray.length}, ${this.component}, ${this.dataArray.length}.`); - } else if (this.timeArray.length * this.component != this.dataArray.length) { - console.error(`Invalid weights array length: ${this.timeArray.length}, ${this.component}, ${this.dataArray.length}.`); - } - } else { - // should never happened - console.error(`Invalid path status: ${path}.`); - } - - if (this.timeArray.length * this.component > this.dataArray.length) { - throw new Error(`Data length mismatch: ${this.timeArray.length}, ${this.component}, ${this.dataArray.length}.`); - } - - if (interpolation === 'LINEAR') { - this.interp = PAnimInterpType.linear; - } else if (interpolation === 'STEP') { - this.interp = PAnimInterpType.step; - } else { - this.interp = PAnimInterpType.cubicSpline; - } - - this.sampler = createAnimationSampler( - this.getInterpInfo(), this.timeArray, this.dataArray, this.component, this.getPathInfo() - ); - } - - /** - * 销毁 - */ - dispose () { - // @ts-expect-error - this.timeArray = undefined; - // @ts-expect-error - this.dataArray = undefined; - this.sampler?.dispose(); - this.sampler = undefined; - } - - /** - * 更新节点动画数据 - * @param time - 当前播放时间 - * @param treeItem - 节点树元素 - * @param sceneManager - 3D 场景管理器 - */ - tick (time: number, treeItem: VFXItem, sceneManager?: PSceneManager) { - const treeComponent = treeItem.getComponent(ModelTreeComponent); - const node = treeComponent?.content?.getNodeById(this.node); - - if (this.sampler !== undefined && node !== undefined) { - const result = this.sampler.evaluate(time); - - switch (this.path) { - case PAnimPathType.translation: - node.transform.setPosition(result[0], result[1], result[2]); - - break; - case PAnimPathType.rotation: - node.transform.setQuaternion(result[0], result[1], result[2], result[3]); - - break; - case PAnimPathType.scale: - node.transform.setScale(result[0], result[1], result[2]); - - break; - case PAnimPathType.weights: - { - /** - * 先生成Mesh的父节点id,然后通过id查询Mesh对象 - * 最后更新Mesh对象权重数据 - */ - const parentId = this.genParentId(treeItem.id, this.node); - const mesh = sceneManager?.queryMesh(parentId); - - if (mesh !== undefined) { - mesh.updateMorphWeights(result); - } - - } - - break; - } - } else { - if (this.sampler !== undefined) { - console.error('AnimTrack: error', this.sampler, node); - } - } - } - - /** - * 获取动画结束时间 - * @returns - */ - getEndTime (): number { - const index = this.timeArray.length - 1; - - return this.timeArray[index]; - } - - /** - * 生成 Mesh 元素的父节点 - * - * @param parentId - 父节点 id 名称 - * @param nodeIndex - Mesh 节点索引 - * - * @returns 生成的 Mesh 节点名称 - */ - private genParentId (parentId: string, nodeIndex: number): string { - return `${parentId}^${nodeIndex}`; - } - - private getPathInfo (): string { - if (this.path === PAnimPathType.scale) { - return 'scale'; - } else if (this.path === PAnimPathType.rotation) { - return 'rotation'; - } else { - return 'translation'; - } - } - - private getInterpInfo (): string { - if (this.interp === PAnimInterpType.cubicSpline) { - return 'CUBICSPLINE'; - } else if (this.interp === PAnimInterpType.step) { - return 'STEP'; - } else { - return 'LINEAR'; - } - } -} - /** * 动画纹理类 */ @@ -760,162 +564,3 @@ export class PAnimTexture { } } - -/** - * 动画类,负责动画数据创建、更新和销毁 - */ -export class PAnimation extends PObject { - private time = 0; - private duration = 0; - private tracks: PAnimTrack[] = []; - - /** - * 创建动画对象 - * @param options - 动画参数 - */ - create (options: ModelAnimationOptions) { - this.name = this.genName(options.name ?? 'Unnamed animation'); - this.type = PObjectType.animation; - // - this.time = 0; - this.duration = 0; - // - this.tracks = []; - options.tracks.forEach(inTrack => { - const track = new PAnimTrack(inTrack); - - this.tracks.push(track); - this.duration = Math.max(this.duration, track.getEndTime()); - }); - } - - /** - * 动画更新 - * @param time - 当前时间 - * @param treeItem - 场景树元素 - * @param sceneManager - 3D 场景管理器 - */ - tick (time: number, treeItem: VFXItem, sceneManager?: PSceneManager) { - this.time = time; - // TODO: 这里时间事件定义不明确,先兼容原先实现 - const newTime = this.time % this.duration; - - this.tracks.forEach(track => { - track.tick(newTime, treeItem, sceneManager); - }); - } - - /** - * 销毁 - */ - override dispose () { - this.tracks.forEach(track => { - track.dispose(); - }); - this.tracks = []; - } -} - -/** - * 动画管理类,负责管理动画对象 - */ -export class PAnimationManager extends PObject { - private ownerItem: VFXItem; - private animation = 0; - private speed = 0; - private delay = 0; - private time = 0; - private animations: PAnimation[] = []; - private sceneManager?: PSceneManager; - - /** - * 创建动画管理器 - * @param treeOptions - 场景树参数 - * @param ownerItem - 场景树所属元素 - */ - constructor (treeOptions: ModelTreeOptions, ownerItem: VFXItem) { - super(); - this.name = this.genName(ownerItem.name ?? 'Unnamed tree'); - this.type = PObjectType.animationManager; - // - this.ownerItem = ownerItem; - this.animation = treeOptions.animation ?? -1; - this.speed = 1.0; - this.delay = ownerItem.start ?? 0; - this.animations = []; - if (treeOptions.animations !== undefined) { - treeOptions.animations.forEach(animOpts => { - const anim = this.createAnimation(animOpts); - - this.animations.push(anim); - }); - } - } - - /** - * 设置场景管理器 - * @param sceneManager - 场景管理器 - */ - setSceneManager (sceneManager: PSceneManager) { - this.sceneManager = sceneManager; - } - - /** - * 创建动画对象 - * @param animationOpts - 动画参数 - * @returns 动画对象 - */ - createAnimation (animationOpts: ModelAnimationOptions) { - const animation = new PAnimation(); - - animation.create(animationOpts); - - return animation; - } - - /** - * 动画更新 - * @param deltaSeconds - 更新间隔 - */ - tick (deltaSeconds: number) { - const newDeltaSeconds = deltaSeconds * this.speed * 0.001; - - this.time += newDeltaSeconds; - // TODO: 需要合并到TreeItem中,通过lifetime进行计算 - const itemTime = this.time - this.delay; - - if (itemTime >= 0) { - if (this.animation >= 0 && this.animation < this.animations.length) { - const anim = this.animations[this.animation]; - - anim.tick(itemTime, this.ownerItem, this.sceneManager); - } else if (this.animation == -88888888) { - this.animations.forEach(anim => { - anim.tick(itemTime, this.ownerItem, this.sceneManager); - }); - } - } - } - - /** - * 销毁 - */ - override dispose () { - // @ts-expect-error - this.ownerItem = null; - this.animations.forEach(anim => { - anim.dispose(); - }); - this.animations = []; - // @ts-expect-error - this.sceneManager = null; - } - - /** - * 获取场景树元素 - * @returns - */ - getTreeItem () { - return this.ownerItem; - } -} diff --git a/plugin-packages/model/src/runtime/common.ts b/plugin-packages/model/src/runtime/common.ts index 838d3b59c..3d6cb0fb1 100644 --- a/plugin-packages/model/src/runtime/common.ts +++ b/plugin-packages/model/src/runtime/common.ts @@ -83,8 +83,8 @@ export enum PShadowType { expVariance, } -export const PBRShaderGUID = 'pbr00000000000000000000000000000'; -export const UnlitShaderGUID = 'unlit000000000000000000000000000'; +export const PBRShaderGUID = spec.BuiltinObjectGUID.PBRShader; +export const UnlitShaderGUID = spec.BuiltinObjectGUID.UnlitShader; /** * 插件变换类 diff --git a/plugin-packages/model/src/runtime/light.ts b/plugin-packages/model/src/runtime/light.ts index 2b11e96f7..474676530 100644 --- a/plugin-packages/model/src/runtime/light.ts +++ b/plugin-packages/model/src/runtime/light.ts @@ -42,6 +42,10 @@ export class PLight extends PEntity { */ lightType = PLightType.ambient; padding: Vector2 = new Vector2(0, 0); + /** + * 是否跟随相机 + */ + followCamera = false; /** * 创建灯光对象 @@ -68,6 +72,7 @@ export class PLight extends PEntity { color.b, ); this.intensity = data.intensity; + this.followCamera = data.followCamera ?? false; if (data.lightType === spec.LightType.point) { this.lightType = PLightType.point; this.range = data.range ?? -1; diff --git a/plugin-packages/model/src/runtime/mesh.ts b/plugin-packages/model/src/runtime/mesh.ts index 3b9fc0b54..5e1548b55 100644 --- a/plugin-packages/model/src/runtime/mesh.ts +++ b/plugin-packages/model/src/runtime/mesh.ts @@ -12,8 +12,6 @@ import type { PSkybox } from './skybox'; import { GeometryBoxProxy, HitTestingProxy } from '../utility/plugin-helper'; import { BoxMesh } from '../utility/ri-helper'; import { RayBoxTesting } from '../utility/hit-test-helper'; -import type { ModelTreeNode } from '../plugin'; -import { ModelTreeComponent } from '../plugin'; import type { ModelMeshComponent } from '../plugin/model-item'; type Box3 = math.Box3; @@ -800,6 +798,8 @@ export class PSubMesh { return 'DEBUG_OCCLUSION'; case spec.RenderMode3D.emissive: return 'DEBUG_EMISSIVE'; + case spec.RenderMode3D.diffuse: + return 'DEBUG_DIFFUSE'; } } @@ -852,18 +852,26 @@ export class PSubMesh { material.setVector3('_Camera', sceneStates.cameraPosition); // if (!this.isUnlitMaterial()) { - const { maxLightCount, lightList } = sceneStates; + const { maxLightCount, lightList, inverseViewMatrix } = sceneStates; for (let i = 0; i < maxLightCount; i++) { if (i < lightList.length) { const light = lightList[i]; const intensity = light.visible ? light.intensity : 0; - material.setVector3(`_Lights[${i}].direction`, light.getWorldDirection()); + if (light.followCamera) { + const newDirection = inverseViewMatrix.transformNormal(light.getWorldDirection(), new Vector3()); + const newPosition = inverseViewMatrix.transformPoint(light.getWorldPosition(), new Vector3()); + + material.setVector3(`_Lights[${i}].direction`, newDirection); + material.setVector3(`_Lights[${i}].position`, newPosition); + } else { + material.setVector3(`_Lights[${i}].direction`, light.getWorldDirection()); + material.setVector3(`_Lights[${i}].position`, light.getWorldPosition()); + } material.setFloat(`_Lights[${i}].range`, light.range); material.setVector3(`_Lights[${i}].color`, light.color); material.setFloat(`_Lights[${i}].intensity`, intensity); - material.setVector3(`_Lights[${i}].position`, light.getWorldPosition()); material.setFloat(`_Lights[${i}].innerConeCos`, Math.cos(light.innerConeAngle)); material.setFloat(`_Lights[${i}].outerConeCos`, Math.cos(light.outerConeAngle)); material.setInt(`_Lights[${i}].type`, light.lightType); @@ -1187,17 +1195,6 @@ class EffectsMeshProxy { return this.data.hide === true; } - getParentNode (): ModelTreeNode | undefined { - const nodeIndex = this.getParentIndex(); - const parentTree = this.parentItem?.getComponent(ModelTreeComponent); - - if (parentTree !== undefined && nodeIndex >= 0) { - return parentTree.content.getNodeById(nodeIndex); - } - - return undefined; - } - getParentIndex (): number { return -1; } diff --git a/plugin-packages/model/src/runtime/scene.ts b/plugin-packages/model/src/runtime/scene.ts index f6f9e93c6..5da75f4ba 100644 --- a/plugin-packages/model/src/runtime/scene.ts +++ b/plugin-packages/model/src/runtime/scene.ts @@ -95,6 +95,10 @@ export interface PSceneStates { * 相机矩阵 */ viewMatrix: Matrix4, + /** + * 逆相机矩阵 + */ + inverseViewMatrix: Matrix4, /** * 投影矩阵 */ @@ -403,6 +407,7 @@ export class PSceneManager { camera: camera, cameraPosition: camera.getEye(), viewMatrix: viewMatrix, + inverseViewMatrix: viewMatrix.clone().invert(), projectionMatrix: projectionMatrix, viewProjectionMatrix: viewProjectionMatrix, winSize: camera.getSize(), diff --git a/plugin-packages/model/src/runtime/shader-libs/standard/metallic-roughness.frag.glsl b/plugin-packages/model/src/runtime/shader-libs/standard/metallic-roughness.frag.glsl index 57bbb6651..6761af4df 100644 --- a/plugin-packages/model/src/runtime/shader-libs/standard/metallic-roughness.frag.glsl +++ b/plugin-packages/model/src/runtime/shader-libs/standard/metallic-roughness.frag.glsl @@ -635,6 +635,44 @@ void main() gl_FragColor.rgb = vec3(baseColor.a); #endif + #ifdef DEBUG_DIFFUSE + vec3 debugDiffuse = vec3(0.0); + #ifdef USE_PUNCTUAL + MaterialInfo diffuseMaterialInfo = MaterialInfo( + 1.0, + f0, + 1.0, + vec3(0.35), + f0, + f0 + ); + for (int i = 0; i < LIGHT_COUNT; ++i) + { + Light light = _Lights[i]; + if (light.type == LightType_Directional) + { + debugDiffuse += applyDirectionalLight(light, diffuseMaterialInfo, normal, view, shadow); + } + else if (light.type == LightType_Point) + { + debugDiffuse += applyPointLight(light, diffuseMaterialInfo, normal, view); + } + else if (light.type == LightType_Spot) + { + debugDiffuse += applySpotLight(light, diffuseMaterialInfo, normal, view, shadow); + } + else if (light.type == LightType_Ambient) + { + debugDiffuse += applyAmbientLight(light, diffuseMaterialInfo); + } + } + #ifdef USE_IBL + debugDiffuse += getIBLContribution(diffuseMaterialInfo, normal, view); + #endif + #endif + gl_FragColor.rgb = toneMap(debugDiffuse); + #endif + gl_FragColor.a = 1.0; #endif // !DEBUG_OUTPUT diff --git a/plugin-packages/model/src/utility/plugin-helper.ts b/plugin-packages/model/src/utility/plugin-helper.ts index e2e5cd06d..1f3328998 100644 --- a/plugin-packages/model/src/utility/plugin-helper.ts +++ b/plugin-packages/model/src/utility/plugin-helper.ts @@ -982,34 +982,6 @@ export class PluginHelper { console.error(`setupItem3DOptions: Invalid inverseBindMatrices type, ${inverseBindMatrices}.`); } } - } else if (item.type === spec.ItemType.tree) { - const jsonItem = item as spec.ModelTreeItem<'json'>; - const studioItem = item as spec.ModelTreeItem<'studio'>; - const jsonAnimations = jsonItem.content.options.tree.animations; - const studioAnimations = studioItem.content.options.tree.animations; - - if (jsonAnimations !== undefined && studioAnimations !== undefined) { - jsonAnimations.forEach((jsonAnim, i) => { - const studioAnim = studioAnimations[i]; - - jsonAnim.tracks.forEach((jsonTrack, j) => { - const inputArray = typedArrayFromBinary(scene.bins, jsonTrack.input); - const outputArray = typedArrayFromBinary(scene.bins, jsonTrack.output); - const studioTrack = studioAnim.tracks[j]; - - if (inputArray instanceof Float32Array) { - studioTrack.input = inputArray; - } else { - console.error(`setupItem3DOptions: Type of inputArray should be float32, ${inputArray}.`); - } - if (outputArray instanceof Float32Array) { - studioTrack.output = outputArray; - } else { - console.error(`setupItem3DOptions: Type of outputArray should be float32, ${outputArray}.`); - } - }); - }); - } } else if (item.type === spec.ItemType.skybox) { const skybox = item as spec.ModelSkyboxItem<'json'>; const studioSkybox = item as spec.ModelSkyboxItem<'studio'>; diff --git a/plugin-packages/model/test/src/plugin-unit.spec.ts b/plugin-packages/model/test/src/plugin-unit.spec.ts index 4676763c4..4092dc605 100644 --- a/plugin-packages/model/test/src/plugin-unit.spec.ts +++ b/plugin-packages/model/test/src/plugin-unit.spec.ts @@ -473,7 +473,7 @@ describe('渲染插件单测', function () { id: '1', name: 'mat1', dataType: spec.DataType.Material, - shader: { id: 'pbr00000000000000000000000000000' }, + shader: { id: spec.BuiltinObjectGUID.PBRShader }, stringTags: { RenderFace: spec.RenderFace.Front, RenderType: spec.RenderType.Opaque, @@ -755,7 +755,7 @@ describe('渲染插件单测', function () { const animComp = new AnimationComponent(engine); - SerializationHelper.deserializeTaggedProperties(jsonScene.components[1], animComp); + SerializationHelper.deserialize(jsonScene.components[1], animComp); expect(animComp.clips.length).to.eql(1); const animClip = animComp.clips[0]; expect(animClip.duration).to.eql(2); @@ -977,7 +977,7 @@ describe('渲染插件单测', function () { }); expect(itemList[22].type).to.eql('mesh'); const itemMesh = new VFXItem(engine); - SerializationHelper.deserializeTaggedProperties(itemList[22], itemMesh); + SerializationHelper.deserialize(itemList[22], itemMesh); const meshComp = itemMesh.getComponent(ModelMeshComponent); const meshData = meshComp.data as spec.ModelMeshComponentData; expect(meshData.name).to.eql('Cesium_Man'); @@ -1109,7 +1109,7 @@ describe('渲染插件单测', function () { // expect(itemList[1].type).to.eql('mesh'); const itemMesh = new VFXItem(engine); - SerializationHelper.deserializeTaggedProperties(itemList[1], itemMesh); + SerializationHelper.deserialize(itemList[1], itemMesh); const meshComp = itemMesh.getComponent(ModelMeshComponent); const meshData = meshComp.data as spec.ModelMeshComponentData; expect(meshData.name).to.eql('WaterBottle'); diff --git a/plugin-packages/model/test/src/utilities.ts b/plugin-packages/model/test/src/utilities.ts index 9268a52a2..94bcb147f 100644 --- a/plugin-packages/model/test/src/utilities.ts +++ b/plugin-packages/model/test/src/utilities.ts @@ -1,8 +1,8 @@ -import type { Player, SceneLoadOptions, SceneLoadType } from '@galacean/effects'; +import type { Player, SceneLoadOptions, Scene } from '@galacean/effects'; export async function generateComposition ( player: Player, - scene: SceneLoadType, + scene: Scene.LoadType, loadOptions?: SceneLoadOptions, options: Record = {}, ) { diff --git a/plugin-packages/multimedia/LICENSE b/plugin-packages/multimedia/LICENSE new file mode 100644 index 000000000..93808239d --- /dev/null +++ b/plugin-packages/multimedia/LICENSE @@ -0,0 +1,22 @@ +MIT LICENSE + +Copyright (c) 2019-present Ant Group Co., Ltd. https://www.antgroup.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. \ No newline at end of file diff --git a/plugin-packages/multimedia/README.md b/plugin-packages/multimedia/README.md new file mode 100644 index 000000000..6ecf560a7 --- /dev/null +++ b/plugin-packages/multimedia/README.md @@ -0,0 +1,25 @@ +# Galacean Effects Multimedia Plugin + +## Usage + +### Simple Import + +``` ts +import { Player } from '@galacean/effects'; +import '@galacean/effects-plugin-multimedia'; +``` + +## Development + +### Getting Started + +``` bash +# demo +pnpm --filter @galacean/effects-plugin-multimedia dev +``` + +> [Open in browser](http://localhost:8081/demo/) + +## Frame Comparison Testing + +> [Open in browser](http://localhost:8081/test/) diff --git a/plugin-packages/multimedia/demo/audio.html b/plugin-packages/multimedia/demo/audio.html new file mode 100644 index 000000000..36f46bf70 --- /dev/null +++ b/plugin-packages/multimedia/demo/audio.html @@ -0,0 +1,32 @@ + + + + + + audio 使用 - Multimedia 插件 - demo + + + + +
+
+ +
+
+ +
+
+ + +
+
+
+ + + diff --git a/plugin-packages/multimedia/demo/index.html b/plugin-packages/multimedia/demo/index.html new file mode 100644 index 000000000..9286dc7f3 --- /dev/null +++ b/plugin-packages/multimedia/demo/index.html @@ -0,0 +1,19 @@ + + + + +Multimedia 插件 - demo + + + + + +
+
Multimedia 插件 Demo
+ +
+ + diff --git a/plugin-packages/multimedia/demo/src/audio.ts b/plugin-packages/multimedia/demo/src/audio.ts new file mode 100644 index 000000000..50df352a8 --- /dev/null +++ b/plugin-packages/multimedia/demo/src/audio.ts @@ -0,0 +1,351 @@ +import { Asset, Player, spec } from '@galacean/effects'; +import '@galacean/effects-plugin-multimedia'; +import { AudioComponent, checkAutoplayPermission, loadAudio } from '@galacean/effects-plugin-multimedia'; + +const duration = 5.0; +const endBehavior = spec.EndBehavior.destroy; +const delay = 3; +const json = { + playerVersion: { web: '2.0.4', native: '0.0.1.202311221223' }, + images: [ + { + url: 'https://mdn.alipayobjects.com/mars/afts/img/A*t0FNRqje9OcAAAAAAAAAAAAADlB4AQ/original', + webp: 'https://mdn.alipayobjects.com/mars/afts/img/A*3kxITrXVFsMAAAAAAAAAAAAADlB4AQ/original', + id: '8fe7723c56254da9b2cd57a4589d4329', + renderLevel: 'B+', + }, + ], + fonts: [], + version: '3.0', + shapes: [], + plugins: ['video', 'audio'], + type: 'ge', + compositions: [ + { + id: '5', + name: 'comp1', + duration: 10, + startTime: 0, + endBehavior: 2, + previewSize: [750, 1624], + items: [ + { id: '14b3d069cbad4cbd81d0a8731cc4aba7' }, + { id: '147e873c89b34c6f96108ccc4d6e6f83' }, + { id: '8b526e86ce154031a76f9176e7224f89' }, + ], + camera: { fov: 60, far: 40, near: 0.1, clipMode: 1, position: [0, 0, 8], rotation: [0, 0, 0] }, + sceneBindings: [ + { key: { id: 'b2bd20ced8044fa8a1fd39149a3271d5' }, value: { id: '14b3d069cbad4cbd81d0a8731cc4aba7' } }, + { key: { id: 'fbc814afc3014b9fa1ddc416c87036d8' }, value: { id: '147e873c89b34c6f96108ccc4d6e6f83' } }, + { key: { id: '54f5ac560cef4c82a40720fe588dfcfd' }, value: { id: '8b526e86ce154031a76f9176e7224f89' } }, + ], + timelineAsset: { id: '28e2d15bfce443258bad609ca56fbb68' }, + }, + ], + components: [ + { + id: '2d6ad26344fa4f58af2ca2bc6b63818d', + item: { id: '14b3d069cbad4cbd81d0a8731cc4aba7' }, + dataType: 'VideoComponent', + options: { + startColor: [1, 1, 1, 1], + video: { + id: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + }, + }, + renderer: { renderMode: 1, texture: { id: 'd546cda394bb484d9bf4af217184e94e' } }, + splits: [[0, 0, 1, 1, 0]], + }, + { + id: '61b52540d9614a8da88c7a4a439b57f3', + item: { id: '147e873c89b34c6f96108ccc4d6e6f83' }, + dataType: 'AudioComponent', + options: { + audio: { + id: 'cccccccccccccccccccccccccccccccc', + }, + }, + }, + { + id: 'fe5ad4a8a1f74530a1bfb6c914019608', + item: { id: '8b526e86ce154031a76f9176e7224f89' }, + dataType: 'ParticleSystem', + shape: { type: 1, radius: 1, arc: 360, arcMode: 0, alignSpeedDirection: false, shape: 'Sphere' }, + renderer: { renderMode: 1, anchor: [0, 0] }, + emission: { rateOverTime: [0, 5] }, + options: { + maxCount: 10, + startLifetime: [0, 1.2], + startDelay: [0, 0], + particleFollowParent: false, + start3DSize: false, + startRotationZ: [0, 0], + startColor: [8, [1, 1, 1, 1]], + startSize: [0, 0.2], + sizeAspect: [0, 1], + }, + positionOverLifetime: { startSpeed: [0, 1], gravity: [0, 0, 0], gravityOverLifetime: [0, 1] }, + }, + ], + geometries: [], + materials: [], + items: [ + { + id: '14b3d069cbad4cbd81d0a8731cc4aba7', + name: 'video', + duration: 5, + type: '1', + visible: true, + endBehavior: 5, + delay: 0, + renderLevel: 'B+', + components: [{ id: '2d6ad26344fa4f58af2ca2bc6b63818d' }], + transform: { + position: { x: 0, y: 0, z: 0 }, + eulerHint: { x: 0, y: 0, z: 0 }, + anchor: { x: 0, y: 0 }, + size: { x: 3.1475, y: 3.1475 }, + scale: { x: 1, y: 1, z: 1 }, + }, + dataType: 'VFXItemData', + }, + { + id: '147e873c89b34c6f96108ccc4d6e6f83', + name: 'audio', + duration, + type: '1', + visible: true, + endBehavior, + delay, + renderLevel: 'B+', + components: [{ id: '61b52540d9614a8da88c7a4a439b57f3' }], + transform: { + position: { x: 0, y: 4.6765, z: 0 }, + eulerHint: { x: 0, y: 0, z: 0 }, + anchor: { x: 0, y: 0 }, + size: { x: 3.1492, y: 3.1492 }, + scale: { x: 1, y: 1, z: 1 }, + }, + dataType: 'VFXItemData', + }, + { + id: '8b526e86ce154031a76f9176e7224f89', + name: 'particle_2', + duration: 5, + type: '2', + visible: true, + endBehavior: 4, + delay: 0, + renderLevel: 'B+', + content: { + dataType: 'ParticleSystem', + shape: { type: 1, radius: 1, arc: 360, arcMode: 0, alignSpeedDirection: false, shape: 'Sphere' }, + renderer: { renderMode: 1, anchor: [0, 0] }, + emission: { rateOverTime: [0, 5] }, + options: { + maxCount: 10, + startLifetime: [0, 1.2], + startDelay: [0, 0], + particleFollowParent: false, + start3DSize: false, + startRotationZ: [0, 0], + startColor: [8, [1, 1, 1, 1]], + startSize: [0, 0.2], + sizeAspect: [0, 1], + }, + positionOverLifetime: { startSpeed: [0, 1], gravity: [0, 0, 0], gravityOverLifetime: [0, 1] }, + }, + components: [{ id: 'fe5ad4a8a1f74530a1bfb6c914019608' }], + transform: { position: { x: 0, y: 0, z: 0 }, eulerHint: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } }, + dataType: 'VFXItemData', + }, + ], + videos: [ + { + url: 'https://gw.alipayobjects.com/v/huamei_s9rwo4/afts/video/A*pud9Q7-6P7QAAAAAAAAAAAAADiqKAQ', + id: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + renderLevel: 'B+', + }, + ], + audios: [ + { + url: 'https://mdn.alipayobjects.com/huamei_s9rwo4/afts/file/A*zERYT5qS-7kAAAAAAAAAAAAADiqKAQ', + id: 'cccccccccccccccccccccccccccccccc', + renderLevel: 'B+', + }, + ], + shaders: [], + bins: [], + textures: [ + { id: 'd546cda394bb484d9bf4af217184e94e', source: { id: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' }, flipY: true }, + ], + animations: [], + miscs: [ + { + id: '28e2d15bfce443258bad609ca56fbb68', + dataType: 'TimelineAsset', + tracks: [ + { id: 'b2bd20ced8044fa8a1fd39149a3271d5' }, + { id: 'fbc814afc3014b9fa1ddc416c87036d8' }, + { id: '54f5ac560cef4c82a40720fe588dfcfd' }, + ], + }, + { id: '3c2ceabd3f6a47b8bf0710d1e3906642', dataType: 'ActivationPlayableAsset' }, + { + id: '878aa596a1ca474189dc14c4e5b472e8', + dataType: 'TransformPlayableAsset', + positionOverLifetime: { + path: [ + 22, + [ + [ + [4, [0, -1]], + [4, [0.992, 0]], + ], + [ + [-3.524964052019925, 0, 0], + [0, 0, 0], + ], + [ + [-2.3499760346799503, 0, 0], + [-1.1749880173399756, 0, 0], + ], + ], + ], + }, + }, + { id: 'eb2ef6afc750499a93c11c0fb9ba04e3', dataType: 'SpriteColorPlayableAsset', startColor: [1, 1, 1, 1] }, + { + id: '951dcf1cd08d40909a2cfbb2e4860886', + dataType: 'ActivationTrack', + children: [], + clips: [{ start: 0, duration: 5, endBehavior: 5, asset: { id: '3c2ceabd3f6a47b8bf0710d1e3906642' } }], + }, + { + id: '21abdd82324d4754b8fc3737846c4c88', + dataType: 'TransformTrack', + children: [], + clips: [{ start: 0, duration: 5, endBehavior: 5, asset: { id: '878aa596a1ca474189dc14c4e5b472e8' } }], + }, + { + id: '0e6cbef7789845bcb11ca5b9ea233011', + dataType: 'SpriteColorTrack', + children: [], + clips: [{ start: 0, duration: 5, endBehavior: 5, asset: { id: 'eb2ef6afc750499a93c11c0fb9ba04e3' } }], + }, + { + id: 'b2bd20ced8044fa8a1fd39149a3271d5', + dataType: 'ObjectBindingTrack', + children: [ + { id: '951dcf1cd08d40909a2cfbb2e4860886' }, + { id: '21abdd82324d4754b8fc3737846c4c88' }, + { id: '0e6cbef7789845bcb11ca5b9ea233011' }, + ], + clips: [], + }, + { id: '0455d9dd0c08438bacae2a1f389fd1b9', dataType: 'ActivationPlayableAsset' }, + { id: '1447e12aafa04939ab9fa15b9e46362e', dataType: 'TransformPlayableAsset', positionOverLifetime: {} }, + { id: '55167a8ecdf44789bfcd8fdb42f7b8f1', dataType: 'SpriteColorPlayableAsset', startColor: [1, 1, 1, 1] }, + { + id: '1865c395b5ad42a09f4e8f8c9074da35', + dataType: 'ActivationTrack', + children: [], + clips: [{ start: delay, duration, endBehavior, asset: { id: '0455d9dd0c08438bacae2a1f389fd1b9' } }], + }, + { + id: '35f9056d99074eeaa2be2a2ab2f81237', + dataType: 'TransformTrack', + children: [], + clips: [{ start: delay, duration, endBehavior, asset: { id: '1447e12aafa04939ab9fa15b9e46362e' } }], + }, + { + id: '6d04569674bb491ab56114e9a7b88b4b', + dataType: 'SpriteColorTrack', + children: [], + clips: [{ start: delay, duration, endBehavior, asset: { id: '55167a8ecdf44789bfcd8fdb42f7b8f1' } }], + }, + { + id: 'fbc814afc3014b9fa1ddc416c87036d8', + dataType: 'ObjectBindingTrack', + children: [ + { id: '1865c395b5ad42a09f4e8f8c9074da35' }, + { id: '35f9056d99074eeaa2be2a2ab2f81237' }, + { id: '6d04569674bb491ab56114e9a7b88b4b' }, + ], + clips: [], + }, + { id: 'e1fa79bd7e2e448fb7b1902e76d1dd65', dataType: 'ActivationPlayableAsset' }, + { + id: 'ce386d52216b4d2b83fd94f770553715', + dataType: 'ActivationTrack', + children: [], + clips: [{ start: 0, duration: 5, endBehavior: 4, asset: { id: 'e1fa79bd7e2e448fb7b1902e76d1dd65' } }], + }, + { + id: '54f5ac560cef4c82a40720fe588dfcfd', + dataType: 'ObjectBindingTrack', + children: [{ id: 'ce386d52216b4d2b83fd94f770553715' }], + clips: [], + }, + ], + compositionId: '5', +}; +let player: Player; +const container = document.getElementById('J-container'); +const addButton = document.getElementById('J-add'); +const updateButton = document.getElementById('J-update'); +const inputEle = document.getElementById('J-input') as HTMLInputElement; + +(async () => { + try { + player = new Player({ + container, + }); + + await checkAutoplayPermission(); + + await player.loadScene(json); + } catch (e) { + console.error('biz', e); + } +})(); + +addButton?.addEventListener('click', async () => { + const value = inputEle.value; + + if (value) { + const item = player.getCompositionByName('comp1')?.getItemByName('video'); + const audio = await loadAudio(value); + const audioAsset = new Asset(player.renderer.engine); + + audioAsset.data = audio; + + if (!item) { + return; + } + const audioComponent = item.addComponent(AudioComponent); + + audioComponent.item = item; + audioComponent.fromData({ + options: { + //@ts-expect-error + audio: audioAsset, + }, + }); + } +}); + +updateButton?.addEventListener('click', async () => { + const value = inputEle.value; + + if (value) { + const audioComponent = player + .getCompositionByName('comp1') + ?.getItemByName('audio') + ?.getComponent(AudioComponent); + + if (audioComponent) { + audioComponent.setAudioSource(await loadAudio(value)); + } + } +}); diff --git a/plugin-packages/multimedia/demo/src/video.ts b/plugin-packages/multimedia/demo/src/video.ts new file mode 100644 index 000000000..c829761e8 --- /dev/null +++ b/plugin-packages/multimedia/demo/src/video.ts @@ -0,0 +1,465 @@ +import type { Texture2DSourceOptionsVideo } from '@galacean/effects'; +import { Player, Texture, spec } from '@galacean/effects'; +import '@galacean/effects-plugin-multimedia'; +import { checkAutoplayPermission, VideoComponent } from '@galacean/effects-plugin-multimedia'; + +const json = { + 'playerVersion': { + 'web': '2.0.4', + 'native': '0.0.1.202311221223', + }, + 'images': [ + { + 'url': 'https://mdn.alipayobjects.com/mars/afts/img/A*A-EoQ6SHJBgAAAAAAAAAAAAADlB4AQ/original', + 'webp': 'https://mdn.alipayobjects.com/mars/afts/img/A*y0ihQrDikLUAAAAAAAAAAAAADlB4AQ/original', + 'id': 'e3b1624a497b4c94bdfc9d4224434a95', + 'renderLevel': 'B+', + }, + ], + 'fonts': [], + 'version': '3.0', + 'shapes': [], + 'plugins': ['video'], + videos: [ + { + url: 'https://gw.alipayobjects.com/v/huamei_s9rwo4/afts/video/A*pud9Q7-6P7QAAAAAAAAAAAAADiqKAQ', + id: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + renderLevel: 'B+', + }, + ], + 'type': 'ge', + 'compositions': [ + { + 'id': '5', + 'name': 'comp1', + 'duration': 10, + 'startTime': 0, + 'endBehavior': 4, + 'previewSize': [750, 1624], + 'items': [ + { + 'id': '14b3d069cbad4cbd81d0a8731cc4aba7', + }, + { + 'id': '8b526e86ce154031a76f9176e7224f89', + }, + ], + 'camera': { + 'fov': 60, + 'far': 40, + 'near': 0.1, + 'clipMode': 1, + 'position': [0, 0, 8], + 'rotation': [0, 0, 0], + }, + 'sceneBindings': [ + { + 'key': { + 'id': '75f0686d9d8341bf90a1711610e1d2fd', + }, + 'value': { + 'id': '14b3d069cbad4cbd81d0a8731cc4aba7', + }, + }, + { + 'key': { + 'id': 'cb6a906e43204b198ecdd323b6a4965e', + }, + 'value': { + 'id': '8b526e86ce154031a76f9176e7224f89', + }, + }, + ], + 'timelineAsset': { + 'id': 'b2cf025ce3b44b5f97759b4679e9598e', + }, + }, + ], + 'components': [ + { + 'id': 'e45437d799364b7cad14b2222669d604', + 'item': { + 'id': '14b3d069cbad4cbd81d0a8731cc4aba7', + }, + 'dataType': 'VideoComponent', + 'options': { + 'startColor': [1, 1, 1, 1], + video: { + id: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + }, + }, + 'renderer': { + 'renderMode': 1, + 'texture': { + 'id': 'b582d21fdd524c4684f1c057b220ddd0', + }, + }, + 'splits': [ + [0, 0, 1, 1, 0], + ], + }, + { + 'id': '295331279c0f472983f949b08cf3838a', + 'item': { + 'id': '8b526e86ce154031a76f9176e7224f89', + }, + 'dataType': 'ParticleSystem', + 'shape': { + 'type': 1, + 'radius': 1, + 'arc': 360, + 'arcMode': 0, + 'alignSpeedDirection': false, + 'shape': 'Sphere', + }, + 'renderer': { + 'renderMode': 1, + 'anchor': [0, 0], + }, + 'emission': { + 'rateOverTime': [0, 5], + }, + 'options': { + 'maxCount': 10, + 'startLifetime': [0, 1.2], + 'startDelay': [0, 0], + 'particleFollowParent': false, + 'start3DSize': false, + 'startRotationZ': [0, 0], + 'startColor': [8, [1, 1, 1, 1], + ], + 'startSize': [0, 0.2], + 'sizeAspect': [0, 1], + }, + 'positionOverLifetime': { + 'startSpeed': [0, 1], + 'gravity': [0, 0, 0], + 'gravityOverLifetime': [0, 1], + }, + }, + ], + 'geometries': [], + 'materials': [], + 'items': [ + { + 'id': '14b3d069cbad4cbd81d0a8731cc4aba7', + 'name': 'video', + 'duration': 5, + 'type': '1', + 'visible': true, + 'endBehavior': 5, + 'delay': 0, + 'renderLevel': 'B+', + 'components': [ + { + 'id': 'e45437d799364b7cad14b2222669d604', + }, + ], + 'transform': { + 'position': { + 'x': 0, + 'y': 0, + 'z': 0, + }, + 'eulerHint': { + 'x': 0, + 'y': 0, + 'z': 0, + }, + 'anchor': { + 'x': 0, + 'y': 0, + }, + 'size': { + 'x': 3.1475, + 'y': 3.1475, + }, + 'scale': { + 'x': 1, + 'y': 1, + 'z': 1, + }, + }, + 'dataType': 'VFXItemData', + }, + { + 'id': '8b526e86ce154031a76f9176e7224f89', + 'name': 'particle_2', + 'duration': 5, + 'type': '2', + 'visible': true, + 'endBehavior': 4, + 'delay': 0, + 'renderLevel': 'B+', + 'content': { + 'dataType': 'ParticleSystem', + 'shape': { + 'type': 1, + 'radius': 1, + 'arc': 360, + 'arcMode': 0, + 'alignSpeedDirection': false, + 'shape': 'Sphere', + }, + 'renderer': { + 'renderMode': 1, + 'anchor': [0, 0], + }, + 'emission': { + 'rateOverTime': [0, 5], + }, + 'options': { + 'maxCount': 10, + 'startLifetime': [0, 1.2], + 'startDelay': [0, 0], + 'particleFollowParent': false, + 'start3DSize': false, + 'startRotationZ': [0, 0], + 'startColor': [8, [1, 1, 1, 1], + ], + 'startSize': [0, 0.2], + 'sizeAspect': [0, 1], + }, + 'positionOverLifetime': { + 'startSpeed': [0, 1], + 'gravity': [0, 0, 0], + 'gravityOverLifetime': [0, 1], + }, + }, + 'components': [ + { + 'id': '295331279c0f472983f949b08cf3838a', + }, + ], + 'transform': { + 'position': { + 'x': 0, + 'y': 0, + 'z': 0, + }, + 'eulerHint': { + 'x': 0, + 'y': 0, + 'z': 0, + }, + 'scale': { + 'x': 1, + 'y': 1, + 'z': 1, + }, + }, + 'dataType': 'VFXItemData', + }, + ], + 'shaders': [], + 'bins': [], + 'textures': [ + { + 'id': 'b582d21fdd524c4684f1c057b220ddd0', + 'source': { + 'id': 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + }, + 'flipY': true, + }, + ], + 'animations': [], + 'miscs': [ + { + 'id': 'b2cf025ce3b44b5f97759b4679e9598e', + 'dataType': 'TimelineAsset', + 'tracks': [ + { + 'id': '75f0686d9d8341bf90a1711610e1d2fd', + }, + { + 'id': 'cb6a906e43204b198ecdd323b6a4965e', + }, + ], + }, + { + 'id': 'f1c1e1d9460848fdb035116d63bc2f3f', + 'dataType': 'ActivationPlayableAsset', + }, + { + 'id': 'c94c61ae3c384ba396261f4f93c5b4fb', + 'dataType': 'TransformPlayableAsset', + 'positionOverLifetime': { + 'path': [22, [ + [ + [4, [0, -1], + ], + [4, [0.992, 0], + ], + ], + [ + [-3.52496405201993, 0, 0], + [0, 0, 0], + ], + [ + [-2.34997603467995, 0, 0], + [-1.17498801733998, 0, 0], + ], + ], + ], + }, + }, + { + 'id': '75ae320c918345e994898d378cbc4b5a', + 'dataType': 'SpriteColorPlayableAsset', + 'startColor': [1, 1, 1, 1], + }, + { + 'id': '11111878de5e49e198c062f29f3c6c38', + 'dataType': 'ActivationTrack', + 'children': [], + 'clips': [ + { + 'start': 0, + 'duration': 5, + 'endBehavior': 5, + 'asset': { + 'id': 'f1c1e1d9460848fdb035116d63bc2f3f', + }, + }, + ], + }, + { + 'id': '5ff36d3c30964b83b3ba8f4819f45d93', + 'dataType': 'TransformTrack', + 'children': [], + 'clips': [ + { + 'start': 0, + 'duration': 5, + 'endBehavior': 5, + 'asset': { + 'id': 'c94c61ae3c384ba396261f4f93c5b4fb', + }, + }, + ], + }, + { + 'id': '7695800886c846308d5436acade4c8df', + 'dataType': 'SpriteColorTrack', + 'children': [], + 'clips': [ + { + 'start': 0, + 'duration': 5, + 'endBehavior': 5, + 'asset': { + 'id': '75ae320c918345e994898d378cbc4b5a', + }, + }, + ], + }, + { + 'id': '75f0686d9d8341bf90a1711610e1d2fd', + 'dataType': 'ObjectBindingTrack', + 'children': [ + { + 'id': '11111878de5e49e198c062f29f3c6c38', + }, + { + 'id': '5ff36d3c30964b83b3ba8f4819f45d93', + }, + { + 'id': '7695800886c846308d5436acade4c8df', + }, + ], + 'clips': [], + }, + { + 'id': 'e5a205def3dd43d6b5ab1984962e3a90', + 'dataType': 'ActivationPlayableAsset', + }, + { + 'id': '3f9afeb4198043af90c2c8579f111901', + 'dataType': 'ActivationTrack', + 'children': [], + 'clips': [ + { + 'start': 0, + 'duration': 5, + 'endBehavior': 4, + 'asset': { + 'id': 'e5a205def3dd43d6b5ab1984962e3a90', + }, + }, + ], + }, + { + 'id': 'cb6a906e43204b198ecdd323b6a4965e', + 'dataType': 'ObjectBindingTrack', + 'children': [ + { + 'id': '3f9afeb4198043af90c2c8579f111901', + }, + ], + 'clips': [], + }, + ], + 'compositionId': '5', +}; +let player: Player; +const container = document.getElementById('J-container'); +const addButton = document.getElementById('J-add'); +const updateButton = document.getElementById('J-update'); +const inputEle = document.getElementById('J-input') as HTMLInputElement; + +(async () => { + try { + player = new Player({ + container, + fps: 130, + }); + + await checkAutoplayPermission(); + + await player.loadScene(json, { renderLevel: spec.RenderLevel.B }); + } catch (e) { + console.error('biz', e); + } +})(); + +addButton?.addEventListener('click', async () => { + const value = inputEle.value; + + if (value) { + const item = player.getCompositionByName('comp1')?.getItemByName('video'); + const texture = await Texture.fromVideo(value, player.renderer.engine); + + if (!item) { return; } + + const videoComponent = item.addComponent(VideoComponent); + + item.composition?.textures.push(texture); + videoComponent.item = item; + videoComponent.fromData({ + options: { + video: { + //@ts-expect-error + data: (texture.source as Texture2DSourceOptionsVideo).video, + }, + }, + renderer: { + mask: 0, + texture, + }, + }); + } +}); + +updateButton?.addEventListener('click', async () => { + const value = inputEle.value; + + if (value) { + const videoComponent = player.getCompositionByName('comp1')?.getItemByName('video')?.getComponent(VideoComponent); + + if (videoComponent) { + const texture = await Texture.fromVideo(value, player.renderer.engine); + + videoComponent.setTexture(texture); + } + } +}); diff --git a/plugin-packages/multimedia/demo/tsconfig.json b/plugin-packages/multimedia/demo/tsconfig.json new file mode 100644 index 000000000..0e8412eb6 --- /dev/null +++ b/plugin-packages/multimedia/demo/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [ + "src" + ] +} diff --git a/plugin-packages/multimedia/demo/video.html b/plugin-packages/multimedia/demo/video.html new file mode 100644 index 000000000..5e9c75196 --- /dev/null +++ b/plugin-packages/multimedia/demo/video.html @@ -0,0 +1,32 @@ + + + + + + 简单使用 - Multimedia 插件 - demo + + + + +
+
+ +
+
+ +
+
+ + +
+
+
+ + + diff --git a/plugin-packages/multimedia/package.json b/plugin-packages/multimedia/package.json new file mode 100644 index 000000000..b5af5965d --- /dev/null +++ b/plugin-packages/multimedia/package.json @@ -0,0 +1,44 @@ +{ + "name": "@galacean/effects-plugin-multimedia", + "version": "2.0.4", + "description": "Galacean Effects player multimedia plugin", + "module": "./dist/index.mjs", + "main": "./dist/index.js", + "browser": "./dist/index.min.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "dev": "vite", + "preview": "concurrently -k \"vite build -w\" \"sleep 6 && vite preview\"", + "prebuild": "pnpm clean", + "build": "pnpm build:declaration && pnpm build:module", + "build:module": "rollup -c", + "build:declaration": "tsc -d --emitDeclarationOnly", + "build:demo": "rimraf dist && vite build", + "clean": "rimraf dist && rimraf \"*+(.tsbuildinfo)\"", + "prepublishOnly": "pnpm build" + }, + "contributors": [ + { + "name": "云垣" + } + ], + "author": "Ant Group CO., Ltd.", + "license": "MIT", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "devDependencies": { + "@galacean/effects": "workspace:*" + } +} diff --git a/plugin-packages/multimedia/rollup.config.js b/plugin-packages/multimedia/rollup.config.js new file mode 100644 index 000000000..ebda6f501 --- /dev/null +++ b/plugin-packages/multimedia/rollup.config.js @@ -0,0 +1,40 @@ +import { getBanner, getPlugins } from '../../scripts/rollup-config-helper'; + +const pkg = require('./package.json'); +const globals = { + '@galacean/effects': 'ge', +}; +const external = Object.keys(globals); +const banner = getBanner(pkg); +const plugins = getPlugins(pkg, { external }); + +export default () => { + return [{ + input: 'src/index.ts', + output: [{ + file: pkg.module, + format: 'es', + banner, + sourcemap: true, + }, { + file: pkg.main, + format: 'cjs', + banner, + sourcemap: true, + }], + external, + plugins, + }, { + input: 'src/index.ts', + output: { + file: pkg.browser, + format: 'umd', + name: 'ge.multimediaPlugin', + banner, + globals, + sourcemap: true, + }, + external, + plugins: getPlugins(pkg, { min: true, external }), + }]; +}; diff --git a/plugin-packages/multimedia/src/audio/audio-component.ts b/plugin-packages/multimedia/src/audio/audio-component.ts new file mode 100644 index 000000000..e29e71849 --- /dev/null +++ b/plugin-packages/multimedia/src/audio/audio-component.ts @@ -0,0 +1,105 @@ +import type { Asset } from '@galacean/effects'; +import { effectsClass, RendererComponent, spec } from '@galacean/effects'; +import { AudioPlayer } from './audio-player'; + +@effectsClass(spec.DataType.AudioComponent) +export class AudioComponent extends RendererComponent { + audioPlayer: AudioPlayer; + + private isVideoPlay = false; + private threshold = 0.03; + + override onUpdate (dt: number): void { + super.onUpdate(dt); + + const { time, duration, endBehavior } = this.item; + + this.audioPlayer.setOptions({ + duration, + endBehavior, + }); + + if (time >= 0 && !this.isVideoPlay) { + this.audioPlayer.play(); + this.isVideoPlay = true; + } + + if (Math.abs(time - duration) < this.threshold) { + if (endBehavior === spec.EndBehavior.destroy) { + this.audioPlayer.pause(); + } + } + } + + override fromData (data: spec.AudioComponentData): void { + super.fromData(data); + + const { options } = data; + const { playbackRate = 1, muted = false, volume = 1 } = options; + + this.audioPlayer = new AudioPlayer((options.audio as unknown as Asset).data, this.engine); + this.audioPlayer.pause(); + this.setPlaybackRate(playbackRate); + this.setMuted(muted); + this.setVolume(volume); + } + + /** + * 设置音频资源 + * @param audio - 音频资源 + */ + setAudioSource (audio: HTMLAudioElement | AudioBuffer): void { + this.audioPlayer.setAudioSource(audio); + } + + /** + * 设置音量 + * @param volume - 音量 + */ + setVolume (volume: number): void { + this.audioPlayer.setVolume(volume); + } + + /** + * 获取当前音频的播放时刻 + */ + getCurrentTime (): number { + return this.audioPlayer.getCurrentTime(); + } + + /** + * 设置是否静音 + * @param muted - 是否静音 + */ + setMuted (muted: boolean): void { + this.audioPlayer.setMuted(muted); + } + + /** + * 设置是否循环播放 + * @param loop - 是否循环播放 + */ + setLoop (loop: boolean): void { + this.audioPlayer.setLoop(loop); + } + + /** + * 设置播放速率 + * @param rate - 播放速率 + */ + setPlaybackRate (rate: number): void { + this.audioPlayer.setPlaybackRate(rate); + } + + override onDisable (): void { + super.onDisable(); + + this.audioPlayer.pause(); + } + + override dispose (): void { + super.dispose(); + + this.audioPlayer.dispose(); + } +} diff --git a/plugin-packages/multimedia/src/audio/audio-loader.ts b/plugin-packages/multimedia/src/audio/audio-loader.ts new file mode 100644 index 000000000..cdf91cbb7 --- /dev/null +++ b/plugin-packages/multimedia/src/audio/audio-loader.ts @@ -0,0 +1,22 @@ +import type { SceneLoadOptions } from '@galacean/effects'; +import { spec, AbstractPlugin } from '@galacean/effects'; +import { processMultimedia } from '../utils'; + +/** + * 音频加载插件 + */ +export class AudioLoader extends AbstractPlugin { + + static override async processAssets ( + json: spec.JSONScene, + options: SceneLoadOptions = {}, + ) { + const { audios = [] } = json; + const loadedAssets = await processMultimedia(audios, spec.MultimediaType.audio, options); + + return { + assets: audios, + loadedAssets, + }; + } +} diff --git a/plugin-packages/multimedia/src/audio/audio-player.ts b/plugin-packages/multimedia/src/audio/audio-player.ts new file mode 100644 index 000000000..df22c7242 --- /dev/null +++ b/plugin-packages/multimedia/src/audio/audio-player.ts @@ -0,0 +1,210 @@ +import type { Engine } from '@galacean/effects'; +import { spec } from '@galacean/effects'; + +interface AudioSourceInfo { + source?: AudioBufferSourceNode, + audioContext?: AudioContext, + gainNode?: GainNode, +} + +export interface AudioPlayerOptions { + endBehavior: spec.EndBehavior, + duration: number, +} + +export class AudioPlayer { + audio?: HTMLAudioElement; + audioSourceInfo: AudioSourceInfo = {}; + + private isSupportAudioContext = !!window['AudioContext']; + private options: AudioPlayerOptions = { + endBehavior: spec.EndBehavior.destroy, + duration: 0, + }; + private destroyed = false; + private started = false; + private initialized = false; + private currentVolume = 1; + + constructor ( + audio: AudioBuffer | HTMLAudioElement, + private engine: Engine, + ) { + this.setAudioSource(audio); + } + + /** + * 设置音频资源 + * @param audio - 音频资源 + */ + setAudioSource (audio: AudioBuffer | HTMLAudioElement) { + if (this.audio || this.audioSourceInfo.source) { + this.dispose(); + } + if (audio instanceof AudioBuffer) { + const audioContext = new AudioContext(); + const gainNode = audioContext.createGain(); + + gainNode.connect(audioContext.destination); + + const source = audioContext.createBufferSource(); + + source.buffer = audio; + source.connect(gainNode); + + this.audioSourceInfo = { + source, + audioContext, + gainNode, + }; + } else { + this.audio = audio; + } + if (this.started) { + this.play(); + } + } + + getCurrentTime (): number { + if (this.isSupportAudioContext) { + const { audioContext } = this.audioSourceInfo; + + return audioContext?.currentTime || 0; + } else { + return this.audio?.currentTime || 0; + } + } + + play (): void { + if (this.isSupportAudioContext) { + const { audioContext, source } = this.audioSourceInfo; + + if (source && audioContext) { + switch (this.options.endBehavior) { + case spec.EndBehavior.destroy: + case spec.EndBehavior.freeze: + source.start(0); + + break; + case spec.EndBehavior.restart: + source.loop = true; + source.loopStart = 0; + source.loopEnd = this.options.duration; + source.start(0); + + break; + default: + break; + } + } + this.started = true; + } else { + this.audio?.play().catch(e => { + this.engine.renderErrors.add(e); + }); + } + } + + pause (): void { + if (this.isSupportAudioContext) { + const { source, audioContext } = this.audioSourceInfo; + + if (!audioContext) { + return; + } + if (audioContext.currentTime > 0 && this.started) { + source?.stop(); + } + } else { + this.audio?.pause(); + } + } + + setVolume (volume: number): void { + if (this.isSupportAudioContext) { + const { gainNode } = this.audioSourceInfo; + + if (gainNode) { + gainNode.gain.value = volume; + this.currentVolume = volume; + } + } else { + if (this.audio) { + this.audio.volume = volume; + this.currentVolume = volume; + } + } + } + + setPlaybackRate (rate: number): void { + if (this.isSupportAudioContext) { + const { source } = this.audioSourceInfo; + + if (source) { + source.playbackRate.value = rate; + } + } else { + if (this.audio) { + this.audio.playbackRate = rate; + } + } + } + + setLoop (loop: boolean): void { + if (this.isSupportAudioContext) { + const { source } = this.audioSourceInfo; + + if (!source) { + this.engine.renderErrors.add(new Error('Audio source is not found.')); + } else { + source.loop = loop; + } + } else { + if (this.audio) { + this.audio.loop = loop; + } + } + } + + setOptions (options: AudioPlayerOptions): void { + if (this.initialized) { + return; + } + this.options = options; + this.initialized = true; + } + + setMuted (muted: boolean): void { + if (this.isSupportAudioContext) { + const { gainNode } = this.audioSourceInfo; + const value = muted ? 0 : this.currentVolume; + + if (gainNode) { + gainNode.gain.value = value; + } + } else { + if (this.audio) { + this.audio.muted = muted; + } + } + } + + dispose (): void { + if (this.destroyed) { + return; + } + if (this.isSupportAudioContext) { + const { audioContext, source } = this.audioSourceInfo; + + if (this.started) { + source?.stop(); + } + audioContext?.close().catch(e => { + this.engine.renderErrors.add(e); + }); + } else { + this.audio?.pause(); + } + this.destroyed = true; + } +} diff --git a/plugin-packages/multimedia/src/constants.ts b/plugin-packages/multimedia/src/constants.ts new file mode 100644 index 000000000..b52396f80 --- /dev/null +++ b/plugin-packages/multimedia/src/constants.ts @@ -0,0 +1,6 @@ +export const multimediaErrorMessageMap: Record = { + 2000: 'Autoplay permission for audio and video is not enabled', +}; +export const multimediaErrorDisplayMessageMap = { + 2000: '音视频自动播放权限未开启', +}; diff --git a/plugin-packages/multimedia/src/index.ts b/plugin-packages/multimedia/src/index.ts new file mode 100644 index 000000000..22a1553e4 --- /dev/null +++ b/plugin-packages/multimedia/src/index.ts @@ -0,0 +1,27 @@ +import * as EFFECTS from '@galacean/effects'; +import { logger, registerPlugin, VFXItem } from '@galacean/effects'; +import { VideoLoader } from './video/video-loader'; +import { AudioLoader } from './audio/audio-loader'; + +export * from './video/video-component'; +export * from './audio/audio-component'; +export * from './audio/audio-player'; +export * from './constants'; +export * from './utils'; + +/** + * 插件版本号 + */ +export const version = __VERSION__; + +registerPlugin('video', VideoLoader, VFXItem, true); +registerPlugin('audio', AudioLoader, VFXItem, true); + +logger.info(`Plugin multimedia version: ${version}.`); + +if (version !== EFFECTS.version) { + console.error( + '注意:请统一 Multimedia 插件与 Player 版本,不统一的版本混用会有不可预知的后果!', + '\nAttention: Please ensure the Multimedia plugin is synchronized with the Player version. Mixing and matching incompatible versions may result in unpredictable consequences!' + ); +} diff --git a/plugin-packages/multimedia/src/type.ts b/plugin-packages/multimedia/src/type.ts new file mode 100644 index 000000000..3d1584753 --- /dev/null +++ b/plugin-packages/multimedia/src/type.ts @@ -0,0 +1,7 @@ +import type { Engine } from '@galacean/effects'; + +export interface PluginData { + hookTimeInfo: (label: string, fn: () => Promise) => Promise, + engine?: Engine, + assets: Record, +} diff --git a/plugin-packages/multimedia/src/utils.ts b/plugin-packages/multimedia/src/utils.ts new file mode 100644 index 000000000..2f3ea1ae0 --- /dev/null +++ b/plugin-packages/multimedia/src/utils.ts @@ -0,0 +1,107 @@ +import type { SceneLoadOptions } from '@galacean/effects'; +import { loadBinary, loadVideo, spec, passRenderLevel, isFunction } from '@galacean/effects'; +import { multimediaErrorMessageMap } from './constants'; + +export async function processMultimedia ( + media: spec.AssetBase[], + type: spec.MultimediaType, + options: SceneLoadOptions, +): Promise { + const { renderLevel } = options; + const jobs = media.map(medium => { + if (passRenderLevel(medium.renderLevel, renderLevel)) { + const url = new URL(medium.url, location.href).href; + + if (type === spec.MultimediaType.video) { + return loadVideo(url); + } else if (type === spec.MultimediaType.audio) { + return loadAudio(url); + } + } + + throw new Error(`Invalid ${type} source: ${JSON.stringify(media)}.`); + }); + + return Promise.all(jobs) as Promise; +} + +/** + * 判断音视频浏览器是否允许播放 + */ +export async function checkAutoplayPermission () { + const audio = document.createElement('audio'); + + // src 为长度为 0.1s 的音频片段 + audio.src = 'data:audio/mpeg;base64,//uQxAACFCEW8uewc8Nfu50FhJuQAGAFJCAhaIHoMhfEnBzixrwNsCGGGmyeCECGGRKWwnB0MkzGh6Hn7JLEstwCADQsJwBAAAOOhOAGAcd0gJgTBuW7HBgJBgfvEgCAEBEiOyeTDxyiyjZLEsRDyEzMz+921nJyWJZn6w8P1769e/AYCQI6tIJAkGHL16zpxhY5VeYGCdd3/93d9w4tygIO/4IHBOD8Hz/4f+IDkEHU4n8PqBMMBCQ0iXWYFnCIGqooaZHfRqOrgxuOtjmpCJCaYjmQqDz3NJUBTFWK4soYEoCumIJzIBldNhLUmAaDzhggZmSAkr9SqAIjGJFGEMCQlIPKDMuo24qZIrKDONHWGqlZbOymMy2yhCoBQywFQAgBEsETW0hCoIkqQWBINPWa3rCoEg1MiBIEiZMUiklyMfKVqoUOxIkSMtVTMzOMSBiKJQMAiWyROrf/5mq//8mkknNBQlFjiJFHKqqr//1VV6qq3vNVVbJpFHXkijM+pIoy1VX7zPOJEkmJAJaYgpqKBgASEAAIAAAAAAAAAAAA//uSxAAD1p0iZiw9OIgAADSAAAAEYAwFApgokRqIFjigukAADJhFjIUGoGFlRAycQGC5QsJJRWdRZRVWZVNYBStcjsRuaiMSgmOQOvVAKBSBY4ygsJGDCEoUEWrE3NBfUyJSSTTiPMpDUUvrhTsC7XR/G6bx2nse52G+cjBDhQW5tan7IZREJY6ULlDpCIhCIhkPDYgFaN2//3JZVZVY6UXQO2NXV1u//P/KSqyqSai7GFdu0DEqoheCVpA1v///6qqaaaYRKqIGKpiCmooGABIQAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='; + audio.muted = true; + audio.crossOrigin = 'anonymous'; + + try { + await audio.play(); + } catch (_) { + throw new MultimediaError(2000, multimediaErrorMessageMap[2000]); + } +} + +/** + * 异步加载一个音频文件 + * @param url - 音频文件的 URL 或 MediaProvider 对象 + */ +export async function loadAudio (url: string): Promise { + const isSupportAudioContext = !!window['AudioContext']; + + if (isSupportAudioContext) { + try { + const audioContext = new AudioContext(); + const buffer = await loadBinary(url); + const decodedData = await audioContext.decodeAudioData(buffer); + + return decodedData; + } catch (_) { + throw new Error(`Failed to load audio from ${url}.`); + } + } + + return new Promise((resolve, reject) => { + const audio = new Audio(); + + // 设置音频源 + if (typeof url === 'string') { + audio.src = url; + } else { + audio.srcObject = url; + } + + audio.muted = false; + + // 监听加载事件 + audio.addEventListener('canplaythrough', () => { + resolve(audio); + }); + + // 监听错误事件 + audio.addEventListener('error', () => { + reject(new Error(`Failed to load audio from ${audio.src}.`)); + }); + + // 开始加载音频 + audio.load(); + }); +} + +export class MultimediaError extends Error { + /** + * 报错代码 + */ + code: number; + + constructor (code: number, message: string) { + super(message); + this.code = code; + this.name = this.constructor.name; + + if ('captureStackTrace' in Error && isFunction(Error.captureStackTrace)) { + Error.captureStackTrace(this, MultimediaError); + } + } +} diff --git a/plugin-packages/multimedia/src/video/video-component.ts b/plugin-packages/multimedia/src/video/video-component.ts new file mode 100644 index 000000000..faa81892d --- /dev/null +++ b/plugin-packages/multimedia/src/video/video-component.ts @@ -0,0 +1,258 @@ +import type { + Texture, Engine, Texture2DSourceOptionsVideo, Asset, SpriteItemProps, + GeometryFromShape, +} from '@galacean/effects'; +import { spec, math, BaseRenderComponent, effectsClass, glContext, getImageItemRenderInfo } from '@galacean/effects'; + +/** + * 用于创建 videoItem 的数据类型, 经过处理后的 spec.VideoContent + */ +export interface VideoItemProps extends Omit { + listIndex?: number, + renderer: { + mask: number, + shape?: GeometryFromShape, + texture: Texture, + } & Omit, +} + +let seed = 0; + +@effectsClass(spec.DataType.VideoComponent) +export class VideoComponent extends BaseRenderComponent { + video?: HTMLVideoElement; + + private threshold = 0.03; + + constructor (engine: Engine) { + super(engine); + + this.name = 'MVideo' + seed++; + this.geometry = this.createGeometry(glContext.TRIANGLES); + } + + override setTexture (texture: Texture): void { + const oldTexture = this.renderer.texture; + const composition = this.item.composition; + + if (!composition) { return; } + + composition.textures.forEach((cachedTexture, index) => { + if (cachedTexture === oldTexture) { + composition.textures[index] = texture; + } + }); + + this.engine.removeTexture(oldTexture); + this.renderer.texture = texture; + this.material.setTexture('_MainTex', texture); + this.video = (texture.source as Texture2DSourceOptionsVideo).video; + } + + override fromData (data: VideoItemProps): void { + super.fromData(data); + + const { interaction, options, listIndex = 0 } = data; + const { + video, + startColor = [1, 1, 1, 1], + playbackRate = 1, + volume = 1, + muted = false, + } = options; + let renderer = data.renderer; + + if (!renderer) { + renderer = {} as SpriteItemProps['renderer']; + } + if (video) { + this.video = (video as unknown as Asset).data; + this.setPlaybackRate(playbackRate); + this.setVolume(volume); + this.setMuted(muted); + const endBehavior = this.item.endBehavior; + + // 如果元素设置为 destroy + if (endBehavior === spec.EndBehavior.destroy) { + this.setLoop(false); + } + } + + this.renderer = { + renderMode: renderer.renderMode ?? spec.RenderMode.BILLBOARD, + blending: renderer.blending ?? spec.BlendingMode.ALPHA, + texture: renderer.texture ?? this.engine.emptyTexture, + occlusion: !!renderer.occlusion, + transparentOcclusion: !!renderer.transparentOcclusion || (renderer.maskMode === spec.MaskMode.MASK), + side: renderer.side ?? spec.SideMode.DOUBLE, + mask: renderer.mask ?? 0, + maskMode: renderer.maskMode ?? spec.MaskMode.NONE, + order: listIndex, + shape: renderer.shape, + }; + + this.interaction = interaction; + this.pauseVideo(); + this.renderInfo = getImageItemRenderInfo(this); + + const geometry = this.createGeometry(glContext.TRIANGLES); + const material = this.createMaterial(this.renderInfo, 2); + + this.worldMatrix = math.Matrix4.fromIdentity(); + this.material = material; + this.geometry = geometry; + + this.material.setVector4('_Color', new math.Vector4().setFromArray(startColor)); + this.material.setVector4('_TexOffset', new math.Vector4().setFromArray([0, 0, 1, 1])); + + this.setItem(); + + } + + override onStart (): void { + super.onStart(); + this.item.composition?.on('goto', (option: { time: number }) => { + if (option.time > 0) { + this.setCurrentTime(option.time); + } + }); + } + + override onUpdate (dt: number): void { + super.onUpdate(dt); + + const { time, duration, endBehavior } = this.item; + + if (time > 0) { + this.setVisible(true); + this.playVideo(); + } + + if (time === 0 && this.item.composition?.rootItem.endBehavior === spec.EndBehavior.freeze) { + this.pauseVideo(); + this.setCurrentTime(0); + } + if (Math.abs(time - duration) < this.threshold) { + if (endBehavior === spec.EndBehavior.freeze) { + this.pauseVideo(); + } else if (endBehavior === spec.EndBehavior.restart) { + this.setVisible(false); + // 重播 + this.pauseVideo(); + this.setCurrentTime(0); + } + } + } + + /** + * 获取当前视频时长 + * @returns 视频时长 + */ + getDuration (): number { + return this.video ? this.video.duration : 0; + } + + /** + * 获取当前视频播放时刻 + * @returns 当前视频播放时刻 + */ + getCurrentTime (): number { + return this.video ? this.video.currentTime : 0; + } + + /** + * 设置阈值(由于视频是单独的 update,有时并不能完全对其 GE 的 update) + * @param threshold 阈值 + */ + setThreshold (threshold: number) { + this.threshold = threshold; + } + + /** + * 设置当前视频播放时刻 + * @param time 视频播放时刻 + */ + setCurrentTime (time: number) { + if (this.video) { + this.video.currentTime = time; + } + } + + /** + * 设置视频是否循环播放 + * @param loop 是否循环播放 + */ + setLoop (loop: boolean) { + if (this.video) { + this.video.loop = loop; + } + } + + /** + * 设置视频是否静音 + * @param muted 是否静音 + */ + setMuted (muted: boolean) { + if (this.video && this.video.muted !== muted) { + this.video.muted = muted; + } + } + + /** + * 设置视频音量 + * @param volume 视频音量 + */ + setVolume (volume: number) { + if (this.video && this.video.volume !== volume) { + this.video.volume = volume; + } + } + + /** + * 设置视频播放速率 + * @param rate 视频播放速率 + */ + setPlaybackRate (rate: number) { + if (!this.video || this.video.playbackRate === rate) { + return; + } + this.video.playbackRate = rate; + } + + private playVideo (): void { + if (this.video) { + this.video.play().catch(error => { + this.engine.renderErrors.add(error); + }); + } + } + + private pauseVideo (): void { + if (this.video && !this.video.paused) { + this.video.pause(); + } + } + + override onDestroy (): void { + super.onDestroy(); + + if (this.video) { + this.video.pause(); + this.video.src = ''; + this.video.load(); + } + } + + override onDisable (): void { + super.onDisable(); + + this.setCurrentTime(0); + this.video?.pause(); + } + + override onEnable (): void { + super.onEnable(); + + this.playVideo(); + } +} diff --git a/plugin-packages/multimedia/src/video/video-loader.ts b/plugin-packages/multimedia/src/video/video-loader.ts new file mode 100644 index 000000000..bbca56041 --- /dev/null +++ b/plugin-packages/multimedia/src/video/video-loader.ts @@ -0,0 +1,22 @@ +import type { SceneLoadOptions } from '@galacean/effects'; +import { spec, AbstractPlugin } from '@galacean/effects'; +import { processMultimedia } from '../utils'; + +/** + * 视频加载插件 + */ +export class VideoLoader extends AbstractPlugin { + + static override async processAssets ( + json: spec.JSONScene, + options: SceneLoadOptions = {}, + ) { + const { videos = [] } = json; + const loadedAssets = await processMultimedia(videos, spec.MultimediaType.video, options); + + return { + assets: videos, + loadedAssets, + }; + } +} diff --git a/plugin-packages/multimedia/test/index.html b/plugin-packages/multimedia/test/index.html new file mode 100644 index 000000000..3c279bc78 --- /dev/null +++ b/plugin-packages/multimedia/test/index.html @@ -0,0 +1,46 @@ + + + + + Plugin Media Tests + + + + + + + + + +
+ + + + + + + + + + + diff --git a/plugin-packages/multimedia/test/src/audio-component.spec.ts b/plugin-packages/multimedia/test/src/audio-component.spec.ts new file mode 100644 index 000000000..fae9ad5e9 --- /dev/null +++ b/plugin-packages/multimedia/test/src/audio-component.spec.ts @@ -0,0 +1,260 @@ +import { spec } from '@galacean/effects'; +import { generateGUID, Player } from '@galacean/effects'; +import { AudioComponent } from '@galacean/effects-plugin-multimedia'; + +interface AudioCompositionOptions { + duration: number, + endBehavior: spec.EndBehavior, + audios: spec.AssetBase[], + start: number, + options: spec.AudioContentOptions, +} + +const { expect } = chai; +const player = new Player({ + canvas: document.createElement('canvas'), +}); + +describe('audioComponent ', function () { + it('audioComponent:create', async function () { + const id = generateGUID(); + const options: AudioCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + audios: [{ id, url: 'https://mdn.alipayobjects.com/huamei_s9rwo4/afts/file/A*zERYT5qS-7kAAAAAAAAAAAAADiqKAQ' }], + start: 0, + options: { audio: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const audio = composition.getItemByName('audio'); + + if (!audio) { throw new Error('audio is null'); } + expect(audio.endBehavior).to.equal(options.endBehavior); + expect(audio.duration).to.equal(options.duration); + expect(audio.start).to.equal(options.start); + const audioComponent = audio.getComponent(AudioComponent); + + expect(audioComponent).to.be.instanceOf(AudioComponent); + composition.dispose(); + }); + + it('audioComponent:destroy', async function () { + const id = generateGUID(); + const options: AudioCompositionOptions = { + duration: 3, + endBehavior: spec.EndBehavior.destroy, + audios: [{ id, url: 'https://mdn.alipayobjects.com/huamei_s9rwo4/afts/file/A*zERYT5qS-7kAAAAAAAAAAAAADiqKAQ' }], + start: 0, + options: { audio: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + + player.gotoAndPlay(4); + const audio = composition.getItemByName('audio'); + + if (!audio) { throw new Error('audio is null'); } + const audioComponent = audio.getComponent(AudioComponent); + + expect(audioComponent).to.be.instanceOf(AudioComponent); + expect(audioComponent.enabled).to.be.false; + composition.dispose(); + + }); + + it('audioComponent:setVolume', async function () { + const id = generateGUID(); + const options: AudioCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + audios: [{ id, url: 'https://mdn.alipayobjects.com/huamei_s9rwo4/afts/file/A*zERYT5qS-7kAAAAAAAAAAAAADiqKAQ' }], + start: 0, + options: { audio: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const audio = composition.getItemByName('audio'); + + if (!audio) { throw new Error('audio is null'); } + expect(audio.endBehavior).to.equal(options.endBehavior); + expect(audio.duration).to.equal(options.duration); + expect(audio.start).to.equal(options.start); + const audioComponent = audio.getComponent(AudioComponent); + + expect(audioComponent).to.be.instanceOf(AudioComponent); + audioComponent.setVolume(0.5); + expect(audioComponent.audioPlayer.audioSourceInfo.gainNode?.gain.value).to.equal(0.5); + composition.dispose(); + }); + + it('audioComponent:setMuted', async function () { + const id = generateGUID(); + const options: AudioCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + audios: [{ id, url: 'https://mdn.alipayobjects.com/huamei_s9rwo4/afts/file/A*zERYT5qS-7kAAAAAAAAAAAAADiqKAQ' }], + start: 0, + options: { audio: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const audio = composition.getItemByName('audio'); + + if (!audio) { throw new Error('audio is null'); } + expect(audio.endBehavior).to.equal(options.endBehavior); + expect(audio.duration).to.equal(options.duration); + expect(audio.start).to.equal(options.start); + const audioComponent = audio.getComponent(AudioComponent); + + expect(audioComponent).to.be.instanceOf(AudioComponent); + audioComponent.setMuted(false); + expect(audioComponent.audioPlayer.audioSourceInfo.gainNode?.gain.value).to.equal(1); + composition.dispose(); + }); + + it('audioComponent:setLoop', async function () { + const id = generateGUID(); + const options: AudioCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + audios: [{ id, url: 'https://mdn.alipayobjects.com/huamei_s9rwo4/afts/file/A*zERYT5qS-7kAAAAAAAAAAAAADiqKAQ' }], + start: 0, + options: { audio: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const audio = composition.getItemByName('audio'); + + if (!audio) { throw new Error('audio is null'); } + expect(audio.endBehavior).to.equal(options.endBehavior); + expect(audio.duration).to.equal(options.duration); + expect(audio.start).to.equal(options.start); + const audioComponent = audio.getComponent(AudioComponent); + + expect(audioComponent).to.be.instanceOf(AudioComponent); + audioComponent.setLoop(true); + expect(audioComponent.audioPlayer.audioSourceInfo.source?.loop).to.equal(true); + composition.dispose(); + }); + + it('audioComponent:setPlaybackRate', async function () { + const id = generateGUID(); + const options: AudioCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + audios: [{ id, url: 'https://mdn.alipayobjects.com/huamei_s9rwo4/afts/file/A*zERYT5qS-7kAAAAAAAAAAAAADiqKAQ' }], + start: 0, + options: { audio: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const audio = composition.getItemByName('audio'); + + if (!audio) { throw new Error('audio is null'); } + expect(audio.endBehavior).to.equal(options.endBehavior); + expect(audio.duration).to.equal(options.duration); + expect(audio.start).to.equal(options.start); + const audioComponent = audio.getComponent(AudioComponent); + + expect(audioComponent).to.be.instanceOf(AudioComponent); + audioComponent.setPlaybackRate(2); + expect(audioComponent.audioPlayer.audioSourceInfo.source?.playbackRate.value).to.equal(2); + composition.dispose(); + }); +}); + +function getVideoJson (options: AudioCompositionOptions) { + return { + playerVersion: { web: '2.0.4', native: '0.0.1.202311221223' }, + images: [], + fonts: [], + version: '3.0', + shapes: [], + plugins: [], + audios: options.audios, + type: 'ge', + compositions: [ + { + id: '5', + name: 'audioTest', + duration: 10, + startTime: 0, + endBehavior: 2, + previewSize: [750, 1624], + items: [{ id: '147e873c89b34c6f96108ccc4d6e6f83' }], + camera: { fov: 60, far: 40, near: 0.1, clipMode: 1, position: [0, 0, 8], rotation: [0, 0, 0] }, + sceneBindings: [ + { key: { id: 'c3cffe498bec4da195ecb68569806ca4' }, value: { id: '147e873c89b34c6f96108ccc4d6e6f83' } }, + ], + timelineAsset: { id: '71ed8f480c64458d94593279bcf831aa' }, + }, + ], + components: [ + { + id: '6dc07c93b035442a93dc3f3ebdba0796', + item: { id: '147e873c89b34c6f96108ccc4d6e6f83' }, + dataType: 'AudioComponent', + options: options.options, + }, + ], + geometries: [], + materials: [], + items: [ + { + id: '147e873c89b34c6f96108ccc4d6e6f83', + name: 'audio', + duration: options.duration, + type: '1', + visible: true, + endBehavior: options.endBehavior, + delay: options.start, + renderLevel: 'B+', + components: [{ id: '6dc07c93b035442a93dc3f3ebdba0796' }], + transform: { + position: { x: 0, y: 4.6765, z: 0 }, + eulerHint: { x: 0, y: 0, z: 0 }, + anchor: { x: 0, y: 0 }, + size: { x: 3.1492, y: 3.1492 }, + scale: { x: 1, y: 1, z: 1 }, + }, + dataType: 'VFXItemData', + }, + ], + shaders: [], + bins: [], + textures: [], + animations: [], + miscs: [ + { + id: '71ed8f480c64458d94593279bcf831aa', + dataType: 'TimelineAsset', + tracks: [{ id: 'c3cffe498bec4da195ecb68569806ca4' }], + }, + { id: 'acfa5d2ad9be40f991db5e9d93864803', dataType: 'ActivationPlayableAsset' }, + { id: '063079d00a6749419976693d32f0d42a', dataType: 'TransformPlayableAsset', positionOverLifetime: {} }, + { + id: 'b5b10964ddb54ce29ed1370c62c02e89', + dataType: 'ActivationTrack', + children: [], + clips: [{ start: options.start, duration: options.duration, endBehavior: options.endBehavior, asset: { id: 'acfa5d2ad9be40f991db5e9d93864803' } }], + }, + { + id: '0259077ac16c4c498fcc91ed341f1909', + dataType: 'TransformTrack', + children: [], + clips: [{ start: options.start, duration: options.duration, endBehavior: options.endBehavior, asset: { id: '063079d00a6749419976693d32f0d42a' } }], + }, + { + id: 'c3cffe498bec4da195ecb68569806ca4', + dataType: 'ObjectBindingTrack', + children: [ + { id: 'b5b10964ddb54ce29ed1370c62c02e89' }, + { id: '0259077ac16c4c498fcc91ed341f1909' }, + ], + clips: [], + }, + ], + compositionId: '5', + }; +} diff --git a/plugin-packages/multimedia/test/src/index.ts b/plugin-packages/multimedia/test/src/index.ts new file mode 100644 index 000000000..51f894510 --- /dev/null +++ b/plugin-packages/multimedia/test/src/index.ts @@ -0,0 +1,3 @@ +import '@galacean/effects-plugin-multimedia'; +import './audio-component.spec'; +import './video-component.spec'; diff --git a/plugin-packages/multimedia/test/src/video-component.spec.ts b/plugin-packages/multimedia/test/src/video-component.spec.ts new file mode 100644 index 000000000..b02c5d623 --- /dev/null +++ b/plugin-packages/multimedia/test/src/video-component.spec.ts @@ -0,0 +1,393 @@ +import { generateGUID, Player, spec } from '@galacean/effects'; +import { VideoComponent } from '@galacean/effects-plugin-multimedia'; +interface VideoCompositionOptions { + duration: number, + endBehavior: spec.EndBehavior, + id: string, + videos: spec.AssetBase[], + start: number, + options: spec.VideoContentOptions, +} + +const { expect } = chai; +const player = new Player({ + canvas: document.createElement('canvas'), +}); + +describe('videoComponent ', function () { + it('videoComponent:create', async function () { + const id = generateGUID(); + const options: VideoCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + id, + videos: [ + { + id, + url: 'https://gw.alipayobjects.com/v/huamei_s9rwo4/afts/video/A*pud9Q7-6P7QAAAAAAAAAAAAADiqKAQ', + }, + ], + start: 0, + options: { video: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const video = composition.getItemByName('video'); + + if (!video) { throw new Error('video is null'); } + expect(video.endBehavior).to.equal(options.endBehavior); + expect(video.duration).to.equal(options.duration); + expect(video.start).to.equal(options.start); + const videoComponent = video.getComponent(VideoComponent); + + expect(videoComponent).to.be.instanceOf(VideoComponent); + composition.dispose(); + }); + + it('videoComponent:dispose', async function () { + const id = generateGUID(); + const options: VideoCompositionOptions = { + duration: 2, + endBehavior: spec.EndBehavior.destroy, + id, + videos: [ + { + id, + url: 'https://gw.alipayobjects.com/v/huamei_s9rwo4/afts/video/A*pud9Q7-6P7QAAAAAAAAAAAAADiqKAQ', + }, + ], + start: 0, + options: { video: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const video = composition.getItemByName('video'); + + player.gotoAndPlay(4); + + if (!video) { throw new Error('video is null'); } + expect(video.endBehavior).to.equal(options.endBehavior); + expect(video.duration).to.equal(options.duration); + expect(video.start).to.equal(options.start); + const videoComponent = video.getComponent(VideoComponent); + + expect(videoComponent).to.be.instanceOf(VideoComponent); + expect(videoComponent.enabled).to.be.false; + + composition.dispose(); + }); + + it('videoComponent:getDuration', async function () { + const id = generateGUID(); + const options: VideoCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + id, + videos: [ + { + id, + url: 'https://gw.alipayobjects.com/v/huamei_s9rwo4/afts/video/A*pud9Q7-6P7QAAAAAAAAAAAAADiqKAQ', + }, + ], + start: 0, + options: { video: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const video = composition.getItemByName('video'); + + if (!video) { throw new Error('video is null'); } + expect(video.endBehavior).to.equal(options.endBehavior); + expect(video.duration).to.equal(options.duration); + expect(video.start).to.equal(options.start); + const videoComponent = video.getComponent(VideoComponent); + + expect(videoComponent).to.be.instanceOf(VideoComponent); + const duration = videoComponent.getDuration(); + //@ts-expect-error + const videoAsset = videoComponent.engine.objectInstance[options.id].data; + + expect(duration).to.equal(videoAsset.duration); + + composition.dispose(); + }); + + it('videoComponent:setCurrentTime', async function () { + const id = generateGUID(); + const options: VideoCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + id, + videos: [ + { + id, + url: 'https://gw.alipayobjects.com/v/huamei_s9rwo4/afts/video/A*pud9Q7-6P7QAAAAAAAAAAAAADiqKAQ', + }, + ], + start: 0, + options: { video: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const video = composition.getItemByName('video'); + + if (!video) { throw new Error('video is null'); } + expect(video.endBehavior).to.equal(options.endBehavior); + expect(video.duration).to.equal(options.duration); + expect(video.start).to.equal(options.start); + const videoComponent = video.getComponent(VideoComponent); + + expect(videoComponent).to.be.instanceOf(VideoComponent); + videoComponent.setCurrentTime(3); + //@ts-expect-error + const videoAsset = videoComponent.engine.objectInstance[options.id].data; + + expect(videoAsset.currentTime).to.equal(3); + composition.dispose(); + }); + + it('videoComponent:setLoop', async function () { + const id = generateGUID(); + const options: VideoCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + id, + videos: [ + { + id, + url: 'https://gw.alipayobjects.com/v/huamei_s9rwo4/afts/video/A*pud9Q7-6P7QAAAAAAAAAAAAADiqKAQ', + }, + ], + start: 0, + options: { video: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const video = composition.getItemByName('video'); + + if (!video) { throw new Error('video is null'); } + expect(video.endBehavior).to.equal(options.endBehavior); + expect(video.duration).to.equal(options.duration); + expect(video.start).to.equal(options.start); + const videoComponent = video.getComponent(VideoComponent); + + expect(videoComponent).to.be.instanceOf(VideoComponent); + videoComponent.setLoop(true); + //@ts-expect-error + const videoAsset = videoComponent.engine.objectInstance[options.id].data; + + expect(videoAsset.loop).to.equal(true); + composition.dispose(); + }); + + it('videoComponent:setMuted', async function () { + const id = generateGUID(); + const options: VideoCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + id, + videos: [ + { + id, + url: 'https://gw.alipayobjects.com/v/huamei_s9rwo4/afts/video/A*pud9Q7-6P7QAAAAAAAAAAAAADiqKAQ', + }, + ], + start: 0, + options: { video: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const video = composition.getItemByName('video'); + + if (!video) { throw new Error('video is null'); } + expect(video.endBehavior).to.equal(options.endBehavior); + expect(video.duration).to.equal(options.duration); + expect(video.start).to.equal(options.start); + const videoComponent = video.getComponent(VideoComponent); + + expect(videoComponent).to.be.instanceOf(VideoComponent); + videoComponent.setMuted(true); + //@ts-expect-error + const videoAsset = videoComponent.engine.objectInstance[options.id].data; + + expect(videoAsset.muted).to.equal(true); + composition.dispose(); + }); + + it('videoComponent:setVolume', async function () { + const id = generateGUID(); + const options: VideoCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + id, + videos: [ + { + id, + url: 'https://gw.alipayobjects.com/v/huamei_s9rwo4/afts/video/A*pud9Q7-6P7QAAAAAAAAAAAAADiqKAQ', + }, + ], + start: 0, + options: { video: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const video = composition.getItemByName('video'); + + if (!video) { throw new Error('video is null'); } + expect(video.endBehavior).to.equal(options.endBehavior); + expect(video.duration).to.equal(options.duration); + expect(video.start).to.equal(options.start); + const videoComponent = video.getComponent(VideoComponent); + + expect(videoComponent).to.be.instanceOf(VideoComponent); + videoComponent.setVolume(0.5); + //@ts-expect-error + const videoAsset = videoComponent.engine.objectInstance[options.id].data; + + expect(videoAsset.volume).to.equal(0.5); + composition.dispose(); + }); + + it('videoComponent:setPlaybackRate', async function () { + const id = generateGUID(); + const options: VideoCompositionOptions = { + duration: 10, + endBehavior: spec.EndBehavior.destroy, + id, + videos: [ + { + id, + url: 'https://gw.alipayobjects.com/v/huamei_s9rwo4/afts/video/A*pud9Q7-6P7QAAAAAAAAAAAAADiqKAQ', + }, + ], + start: 0, + options: { video: { id } }, + }; + const videoJson = getVideoJson(options); + const composition = await player.loadScene(videoJson); + const video = composition.getItemByName('video'); + + if (!video) { throw new Error('video is null'); } + expect(video.endBehavior).to.equal(options.endBehavior); + expect(video.duration).to.equal(options.duration); + expect(video.start).to.equal(options.start); + const videoComponent = video.getComponent(VideoComponent); + + expect(videoComponent).to.be.instanceOf(VideoComponent); + videoComponent.setPlaybackRate(0.5); + //@ts-expect-error + const videoAsset = videoComponent.engine.objectInstance[options.id].data; + + expect(videoAsset.playbackRate).to.equal(0.5); + composition.dispose(); + }); +}); + +function getVideoJson (options: VideoCompositionOptions) { + return { + playerVersion: { web: '2.0.4', native: '0.0.1.202311221223' }, + images: [], + fonts: [], + version: '3.0', + shapes: [], + plugins: [], + videos: options.videos, + type: 'ge', + compositions: [ + { + id: '5', + name: 'videoTest', + duration: 10, + startTime: 0, + endBehavior: 2, + previewSize: [750, 1624], + items: [{ id: '147e873c89b34c6f96108ccc4d6e6f83' }], + camera: { fov: 60, far: 40, near: 0.1, clipMode: 1, position: [0, 0, 8], rotation: [0, 0, 0] }, + sceneBindings: [ + { key: { id: 'c3cffe498bec4da195ecb68569806ca4' }, value: { id: '147e873c89b34c6f96108ccc4d6e6f83' } }, + ], + timelineAsset: { id: '71ed8f480c64458d94593279bcf831aa' }, + }, + ], + components: [ + { + id: '6dc07c93b035442a93dc3f3ebdba0796', + item: { id: '147e873c89b34c6f96108ccc4d6e6f83' }, + dataType: 'VideoComponent', + options: options.options, + renderer: { + 'renderMode': 1, + 'texture': { + 'id': 'b582d21fdd524c4684f1c057b220ddd0', + }, + }, + }, + ], + textures: [ + { + 'id': 'b582d21fdd524c4684f1c057b220ddd0', + 'source': { + 'id': options.id, + }, + 'flipY': true, + }, + ], + geometries: [], + materials: [], + items: [ + { + id: '147e873c89b34c6f96108ccc4d6e6f83', + name: 'video', + duration: options.duration, + type: '1', + visible: true, + endBehavior: options.endBehavior, + delay: options.start, + renderLevel: 'B+', + components: [{ id: '6dc07c93b035442a93dc3f3ebdba0796' }], + transform: { + position: { x: 0, y: 4.6765, z: 0 }, + eulerHint: { x: 0, y: 0, z: 0 }, + anchor: { x: 0, y: 0 }, + size: { x: 3.1492, y: 3.1492 }, + scale: { x: 1, y: 1, z: 1 }, + }, + dataType: 'VFXItemData', + }, + ], + shaders: [], + bins: [], + animations: [], + miscs: [ + { + id: '71ed8f480c64458d94593279bcf831aa', + dataType: 'TimelineAsset', + tracks: [{ id: 'c3cffe498bec4da195ecb68569806ca4' }], + }, + { id: 'acfa5d2ad9be40f991db5e9d93864803', dataType: 'ActivationPlayableAsset' }, + { id: '063079d00a6749419976693d32f0d42a', dataType: 'TransformPlayableAsset', positionOverLifetime: {} }, + { + id: 'b5b10964ddb54ce29ed1370c62c02e89', + dataType: 'ActivationTrack', + children: [], + clips: [{ start: options.start, duration: options.duration, endBehavior: options.endBehavior, asset: { id: 'acfa5d2ad9be40f991db5e9d93864803' } }], + }, + { + id: '0259077ac16c4c498fcc91ed341f1909', + dataType: 'TransformTrack', + children: [], + clips: [{ start: options.start, duration: options.duration, endBehavior: options.endBehavior, asset: { id: '063079d00a6749419976693d32f0d42a' } }], + }, + { + id: 'c3cffe498bec4da195ecb68569806ca4', + dataType: 'ObjectBindingTrack', + children: [ + { id: 'b5b10964ddb54ce29ed1370c62c02e89' }, + { id: '0259077ac16c4c498fcc91ed341f1909' }, + ], + clips: [], + }, + ], + compositionId: '5', + }; +} diff --git a/plugin-packages/multimedia/test/tsconfig.json b/plugin-packages/multimedia/test/tsconfig.json new file mode 100644 index 000000000..0e8412eb6 --- /dev/null +++ b/plugin-packages/multimedia/test/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [ + "src" + ] +} diff --git a/plugin-packages/multimedia/tsconfig.json b/plugin-packages/multimedia/tsconfig.json new file mode 100644 index 000000000..ecbcdd515 --- /dev/null +++ b/plugin-packages/multimedia/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true + }, + "include": [ + "src", + "../../types" + ], + "references": [ + { + "path": "../../packages/effects" + } + ] +} diff --git a/plugin-packages/multimedia/typedoc.json b/plugin-packages/multimedia/typedoc.json new file mode 100644 index 000000000..b6aba3cbd --- /dev/null +++ b/plugin-packages/multimedia/typedoc.json @@ -0,0 +1,8 @@ +{ + "extends": [ + "../../typedoc.base.json" + ], + "entryPoints": [ + "src/index.ts" + ] +} diff --git a/plugin-packages/multimedia/vite.config.js b/plugin-packages/multimedia/vite.config.js new file mode 100644 index 000000000..6616abb43 --- /dev/null +++ b/plugin-packages/multimedia/vite.config.js @@ -0,0 +1,71 @@ +import { resolve } from 'path'; +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import legacy from '@vitejs/plugin-legacy'; +import ip from 'ip'; +import { glslInner, getSWCPlugin } from '../../scripts/rollup-config-helper'; + +export default defineConfig(({ mode }) => { + const development = mode === 'development'; + + return { + base: './', + build: { + rollupOptions: { + input: { + 'index': resolve(__dirname, 'demo/index.html'), + 'video': resolve(__dirname, 'demo/video.html'), + 'audio': resolve(__dirname, 'demo/audio.html'), + } + }, + minify: false, // iOS 9 等低版本加载压缩代码报脚本异常 + }, + server: { + host: '0.0.0.0', + port: 8081, + }, + preview: { + host: '0.0.0.0', + port: 8081, + }, + define: { + __VERSION__: 0, + __DEBUG__: development, + }, + plugins: [ + legacy({ + targets: ['iOS >= 9'], + modernPolyfills: ['es/global-this'], + }), + glslInner(), + getSWCPlugin({ + baseUrl: resolve(__dirname, '..', '..'), + }), + tsconfigPaths(), + configureServerPlugin(), + ], + }; +}); + +// 用于配置开发服务器的钩子 +function configureServerPlugin() { + const handleServer = function (server) { + const host = ip.address() ?? 'localhost'; + const port = server.config.server.port; + const baseUrl = `http://${host}:${port}`; + + setTimeout(() => { + console.log(` \x1b[1m\x1b[32m->\x1b[97m Demo: \x1b[0m\x1b[96m${baseUrl}/demo/index.html\x1b[0m`); + }, 1000); + } + + return { + name: 'configure-server', + configurePreviewServer(server) { + server.httpServer.once('listening', handleServer.bind(this, server)); + }, + configureServer(server) { + server.httpServer.once('listening', handleServer.bind(this, server)); + }, + } +} diff --git a/plugin-packages/orientation-transformer/src/index.ts b/plugin-packages/orientation-transformer/src/index.ts index 0ed61f403..c543be67f 100644 --- a/plugin-packages/orientation-transformer/src/index.ts +++ b/plugin-packages/orientation-transformer/src/index.ts @@ -2,18 +2,15 @@ import * as EFFECTS from '@galacean/effects'; import { VFXItem, logger, registerPlugin } from '@galacean/effects'; import { OrientationPluginLoader } from './orientation-plugin-loader'; -declare global { - interface Window { - ge: any, - } -} - registerPlugin('orientation-transformer', OrientationPluginLoader, VFXItem); export { getAdapter, closeDeviceMotion, openDeviceMotion, OrientationPluginLoader } from './orientation-plugin-loader'; export { OrientationAdapterAcceler } from './orientation-adapter-acceler'; export * from './orientation-component'; +/** + * 插件版本号 + */ export const version = __VERSION__; logger.info(`Plugin orientation transformer version: ${version}.`); diff --git a/plugin-packages/orientation-transformer/src/orientation-component.ts b/plugin-packages/orientation-transformer/src/orientation-component.ts index db1ee5865..9bf22cb7a 100644 --- a/plugin-packages/orientation-transformer/src/orientation-component.ts +++ b/plugin-packages/orientation-transformer/src/orientation-component.ts @@ -27,7 +27,7 @@ export class OrientationComponent extends Behaviour { } - override start () { + override onStart () { const transformer = this.item.composition?.loaderData.deviceTransformer as CompositionTransformerAcceler; if (transformer) { @@ -36,7 +36,7 @@ export class OrientationComponent extends Behaviour { } } - override update (dt: number) { + override onUpdate (dt: number) { const transformer = this.item.composition?.loaderData.deviceTransformer as CompositionTransformerAcceler; if (transformer) { diff --git a/plugin-packages/rich-text/LICENSE b/plugin-packages/rich-text/LICENSE new file mode 100644 index 000000000..93808239d --- /dev/null +++ b/plugin-packages/rich-text/LICENSE @@ -0,0 +1,22 @@ +MIT LICENSE + +Copyright (c) 2019-present Ant Group Co., Ltd. https://www.antgroup.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. \ No newline at end of file diff --git a/plugin-packages/rich-text/README.md b/plugin-packages/rich-text/README.md new file mode 100644 index 000000000..650a3554c --- /dev/null +++ b/plugin-packages/rich-text/README.md @@ -0,0 +1,25 @@ +# Galacean Effects RichText Plugin + +## Usage + +### Simple Import + +``` ts +import { Player } from '@galacean/effects'; +import '@galacean/effects-plugin-rich-text'; +``` + +## Development + +### Getting Started + +``` bash +# demo +pnpm --filter @galacean/effects-plugin-rich-text dev +``` + +> [Open in browser](http://localhost:8081/demo/) + +## Frame Comparison Testing + +> [Open in browser](http://localhost:8081/test/) diff --git a/plugin-packages/rich-text/demo/index.html b/plugin-packages/rich-text/demo/index.html new file mode 100644 index 000000000..549253607 --- /dev/null +++ b/plugin-packages/rich-text/demo/index.html @@ -0,0 +1,18 @@ + + + + +RichText 插件 - demo + + + + + +
+
RichText 插件 Demo
+ +
+ + diff --git a/plugin-packages/rich-text/demo/simple.html b/plugin-packages/rich-text/demo/simple.html new file mode 100644 index 000000000..dafd0a7be --- /dev/null +++ b/plugin-packages/rich-text/demo/simple.html @@ -0,0 +1,22 @@ + + + + + + 简单使用 - 富文本 插件 - demo + + + + +
+
+
+ + + diff --git a/plugin-packages/rich-text/demo/src/simple.ts b/plugin-packages/rich-text/demo/src/simple.ts new file mode 100644 index 000000000..38ee546e2 --- /dev/null +++ b/plugin-packages/rich-text/demo/src/simple.ts @@ -0,0 +1,122 @@ +import { Player, spec } from '@galacean/effects'; +import '@galacean/effects-plugin-rich-text'; + +const json = { + playerVersion: { web: '2.0.6', native: '0.0.1.202311221223' }, + images: [], + fonts: [], + version: '3.0', + shapes: [], + plugins: ['richtext'], + type: 'ge', + compositions: [ + { + id: '9', + name: 'richtext', + duration: 5, + startTime: 0, + endBehavior: 5, + previewSize: [750, 1624], + items: [{ id: '41716f6d8a1748a2b09fd36a09c91fd4' }], + camera: { fov: 60, far: 40, near: 0.1, clipMode: 1, position: [0, 0, 8], rotation: [0, 0, 0] }, + sceneBindings: [ + { key: { id: 'ac2826cbde3d4b2aa6a2bc99b34eef7d' }, value: { id: '41716f6d8a1748a2b09fd36a09c91fd4' } }, + ], + timelineAsset: { id: '420a2bf13d60445aa8e42e71cb248d8d' }, + }, + ], + components: [ + { + id: 'a7f9b9e3127e4ffa9682f7692ce90e09', + item: { id: '41716f6d8a1748a2b09fd36a09c91fd4' }, + dataType: 'RichTextComponent', + options: { + text: ' We are absolutely definitely not \nWe are green with envy \nWe are DDDD amused. \nWe are not amused. \nWe are usually not amused. \nWe are largely unaffected. \nWe are colorfully amused \n88$', + fontFamily: 'sans-serif', + fontSize: 30, + textColor: [255, 255, 10, 1], + fontWeight: 'bold', + textAlign: 1, + fontStyle: 'normal', + }, + renderer: { renderMode: 1 }, + }, + ], + geometries: [], + materials: [], + items: [ + { + id: '41716f6d8a1748a2b09fd36a09c91fd4', + name: 'richtext', + duration: 5, + type: 'text', + visible: true, + endBehavior: 5, + delay: 0, + renderLevel: 'B+', + transform: { + position: { x: 0.2202, y: 2.5601, z: 0 }, + eulerHint: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 }, + }, + components: [{ id: 'a7f9b9e3127e4ffa9682f7692ce90e09' }], + dataType: 'VFXItemData', + }, + ], + shaders: [], + bins: [], + textures: [], + animations: [], + miscs: [ + { + id: '420a2bf13d60445aa8e42e71cb248d8d', + dataType: 'TimelineAsset', + tracks: [{ id: 'ac2826cbde3d4b2aa6a2bc99b34eef7d' }], + }, + { id: '6ab1609043ee4e1db92bcfa6d2587045', dataType: 'ActivationPlayableAsset' }, + { id: '394c84cd0cec4295993206d2a0f74695', dataType: 'TransformPlayableAsset', positionOverLifetime: {} }, + { id: 'da4c8f544dd44c20b93d6098c93398e2', dataType: 'SpriteColorPlayableAsset' }, + { + id: '99811f1e59d44335a27c58e44b5ede02', + dataType: 'ActivationTrack', + children: [], + clips: [{ start: 0, duration: 5, endBehavior: 5, asset: { id: '6ab1609043ee4e1db92bcfa6d2587045' } }], + }, + { + id: '563df4e1a3ec44bcb4632b1c31feb5ba', + dataType: 'TransformTrack', + children: [], + clips: [{ start: 0, duration: 5, endBehavior: 5, asset: { id: '394c84cd0cec4295993206d2a0f74695' } }], + }, + { + id: '18e32d67d8154082b6235d009a73d782', + dataType: 'SpriteColorTrack', + children: [], + clips: [{ start: 0, duration: 5, endBehavior: 5, asset: { id: 'da4c8f544dd44c20b93d6098c93398e2' } }], + }, + { + id: 'ac2826cbde3d4b2aa6a2bc99b34eef7d', + dataType: 'ObjectBindingTrack', + children: [ + { id: '99811f1e59d44335a27c58e44b5ede02' }, + { id: '563df4e1a3ec44bcb4632b1c31feb5ba' }, + { id: '18e32d67d8154082b6235d009a73d782' }, + ], + clips: [], + }, + ], + compositionId: '9', +}; +const container = document.getElementById('J-container'); + +(async () => { + try { + const player = new Player({ + container, + }); + + await player.loadScene(json); + } catch (e) { + console.error('biz', e); + } +})(); diff --git a/plugin-packages/rich-text/demo/tsconfig.json b/plugin-packages/rich-text/demo/tsconfig.json new file mode 100644 index 000000000..0e8412eb6 --- /dev/null +++ b/plugin-packages/rich-text/demo/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [ + "src" + ] +} diff --git a/plugin-packages/rich-text/package.json b/plugin-packages/rich-text/package.json new file mode 100644 index 000000000..404d7e19c --- /dev/null +++ b/plugin-packages/rich-text/package.json @@ -0,0 +1,59 @@ +{ + "name": "@galacean/effects-plugin-rich-text", + "version": "2.0.4", + "description": "Galacean Effects player richtext plugin", + "module": "./dist/index.mjs", + "main": "./dist/index.js", + "browser": "./dist/index.min.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./alipay": { + "import": "./dist/alipay.mjs", + "require": "./dist/alipay.js", + "types": "./dist/index.d.ts" + }, + "./weapp": { + "import": "./dist/weapp.mjs", + "require": "./dist/weapp.js", + "types": "./dist/index.d.ts" + }, + "./douyin": { + "import": "./dist/douyin.mjs", + "require": "./dist/douyin.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "dev": "vite", + "preview": "concurrently -k \"vite build -w\" \"sleep 6 && vite preview\"", + "prebuild": "pnpm clean", + "build": "pnpm build:declaration && pnpm build:module", + "build:module": "rollup -c", + "build:declaration": "tsc -d --emitDeclarationOnly", + "build:demo": "rimraf dist && vite build", + "clean": "rimraf dist && rimraf \"*+(.tsbuildinfo)\"", + "prepublishOnly": "pnpm build" + }, + "contributors": [ + { + "name": "云垣" + } + ], + "author": "Ant Group CO., Ltd.", + "license": "MIT", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "devDependencies": { + "@galacean/effects": "workspace:*" + } +} diff --git a/plugin-packages/rich-text/rollup.appx.config.js b/plugin-packages/rich-text/rollup.appx.config.js new file mode 100644 index 000000000..dbe5e7d40 --- /dev/null +++ b/plugin-packages/rich-text/rollup.appx.config.js @@ -0,0 +1,27 @@ +/** + * 小程序产物编译配置 + */ +export default [ + 'alipay', + 'weapp', + 'douyin', +].map(platform => { + const paths = { '@galacean/effects': `@galacean/effects/${platform}` }; + + return { + input: `src/index.ts`, + output: [{ + file: `./dist/${platform}.mjs`, + format: 'es', + sourcemap: true, + paths, + }, { + file: `./dist/${platform}.js`, + format: 'cjs', + sourcemap: true, + paths, + }], + external: ['@galacean/effects'], + plugins: [], + }; +}); diff --git a/plugin-packages/rich-text/rollup.config.js b/plugin-packages/rich-text/rollup.config.js new file mode 100644 index 000000000..ff9f62217 --- /dev/null +++ b/plugin-packages/rich-text/rollup.config.js @@ -0,0 +1,44 @@ +import { getBanner, getPlugins } from '../../scripts/rollup-config-helper'; +import appxConfig from './rollup.appx.config'; + +const pkg = require('./package.json'); +const globals = { + '@galacean/effects': 'ge', +}; +const external = Object.keys(globals); +const banner = getBanner(pkg); +const plugins = getPlugins(pkg, { external }); + +export default () => { + return [ + { + input: 'src/index.ts', + output: [{ + file: pkg.module, + format: 'es', + banner, + sourcemap: true, + }, { + file: pkg.main, + format: 'cjs', + banner, + sourcemap: true, + }], + external, + plugins, + }, { + input: 'src/index.ts', + output: { + file: pkg.browser, + format: 'umd', + name: 'ge.richTextPlugin', + banner, + globals, + sourcemap: true, + }, + external, + plugins: getPlugins(pkg, { min: true, external }), + }, + ...appxConfig.map(config => ({ ...config, plugins: config.plugins.concat(plugins) })) + ]; +}; diff --git a/plugin-packages/rich-text/src/color-utils.ts b/plugin-packages/rich-text/src/color-utils.ts new file mode 100644 index 000000000..7ce25ac85 --- /dev/null +++ b/plugin-packages/rich-text/src/color-utils.ts @@ -0,0 +1,76 @@ +import type { spec } from '@galacean/effects'; + +/** + * 将颜色名称转换为 RGBA + * @param colorName - 颜色名称 + * @returns RGBA 颜色字符串 + */ +export function colorNameToRGBA (colorName: string): string { + if (typeof colorName !== 'string' || !colorName) { + throw new Error('Invalid color name provided'); + } + if (typeof document === 'undefined') { + throw new Error('This method requires a browser environment'); + } + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + if (context) { + try { + context.fillStyle = colorName; + const result = context.fillStyle; + + return result; + } finally { + // Clean up DOM element + canvas.remove(); + } + } + + throw new Error('Failed to get 2D context for color conversion!'); +} + +/** + * 将 16 进制颜色转换为 RGBA + * @param hex - 16 进制颜色 + * @param alpha - 透明度 + * @returns - RGBA 颜色 + */ +export function hexToRGBA (hex: string, alpha: number = 1): spec.vec4 { + hex = hex.replace(/^#/, ''); + + if (hex.length === 3 || hex.length === 4) { + hex = hex.split('').map(char => char + char).join(''); + } + + // Handle alpha channel in hex + if (hex.length === 8) { + const a = parseInt(hex.slice(6, 8), 16) / 255; + + hex = hex.slice(0, 6); + alpha = a; + } + const bigint = parseInt(hex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + + return [r, g, b, alpha]; +} + +/** + * 将颜色字符串转换为 RGBA + * @param color - 颜色字符串 + * @param alpha - 透明度 + * @returns - RGBA 颜色 + */ +export function toRGBA (color: string, alpha: number = 1): spec.vec4 { + if (typeof color !== 'string' || !color) { + throw new Error('Invalid color string'); + } + if (color.startsWith('#')) { + return hexToRGBA(color, alpha); + } else { + return hexToRGBA(colorNameToRGBA(color)); + } +} diff --git a/plugin-packages/rich-text/src/index.ts b/plugin-packages/rich-text/src/index.ts new file mode 100644 index 000000000..c90525c5a --- /dev/null +++ b/plugin-packages/rich-text/src/index.ts @@ -0,0 +1,22 @@ +import * as EFFECTS from '@galacean/effects'; +import { logger, registerPlugin, VFXItem } from '@galacean/effects'; +import { RichTextLoader } from './rich-text-loader'; + +export * from './rich-text-parser'; +export * from './rich-text-component'; + +/** + * 插件版本号 + */ +export const version = __VERSION__; + +registerPlugin('richtext', RichTextLoader, VFXItem, true); + +logger.info(`Plugin rich text version: ${version}.`); + +if (version !== EFFECTS.version) { + console.error( + '注意:请统一 RichText 插件与 Player 版本,不统一的版本混用会有不可预知的后果!', + '\nAttention: Please ensure the RichText plugin is synchronized with the Player version. Mixing and matching incompatible versions may result in unpredictable consequences!' + ); +} diff --git a/plugin-packages/rich-text/src/rich-text-component.ts b/plugin-packages/rich-text/src/rich-text-component.ts new file mode 100644 index 000000000..d1a44465e --- /dev/null +++ b/plugin-packages/rich-text/src/rich-text-component.ts @@ -0,0 +1,223 @@ +import type { Engine } from '@galacean/effects'; +import { effectsClass, glContext, spec, TextComponent, Texture, TextLayout, TextStyle } from '@galacean/effects'; +import { generateProgram } from './rich-text-parser'; +import { toRGBA } from './color-utils'; + +/** + * + */ +export interface RichTextOptions { + text: string, + fontSize: number, + fontFamily?: string, + fontWeight?: spec.TextWeight, + fontStyle?: spec.FontStyle, + fontColor?: spec.vec4, + textStyle?: TextStyle, + isNewLine: boolean, +} + +interface RichCharInfo { + offsetX: number[], + /** + * 字符参数 + */ + richOptions: RichTextOptions[], + /** + * 段落宽度 + */ + width: number, + /** + * 段落高度 + */ + lineHeight: number, + /** + * 字体偏移高度 + */ + offsetY: number, +} + +let seed = 0; + +@effectsClass(spec.DataType.RichTextComponent) +export class RichTextComponent extends TextComponent { + processedTextOptions: RichTextOptions[] = []; + + private singleLineHeight: number = 1.571; + + constructor (engine: Engine) { + super(engine); + + this.name = 'MRichText' + seed++; + } + + private generateTextProgram (text: string) { + this.processedTextOptions = []; + const program = generateProgram((text, context) => { + // 如果富文本仅包含换行符,则在每个换行符后添加一个空格 + if (/^\n+$/.test(text)) { + text = text.replace(/\n/g, '\n '); + } + const textArr = text.split('\n'); + + textArr.forEach((text, index) => { + const options: RichTextOptions = { + text, + fontSize: this.textStyle.fontSize, + isNewLine: false, + }; + + if (index > 0) { + options.isNewLine = true; + } + if ('b' in context) { + options.fontWeight = spec.TextWeight.bold; + } + + if ('i' in context) { + options.fontStyle = spec.FontStyle.italic; + } + + if ('size' in context && context.size) { + options.fontSize = parseInt(context.size, 10); + } + + if ('color' in context && context.color) { + options.fontColor = toRGBA(context.color); + } + this.processedTextOptions.push(options); + }); + + }); + + program(text); + } + + override updateTexture (flipY = true) { + if (!this.isDirty || !this.context || !this.canvas) { + return; + } + this.generateTextProgram(this.text); + let width = 0, height = 0; + const { textLayout, textStyle } = this; + + const context = this.context; + + context.save(); + const charsInfo: RichCharInfo[] = []; + const fontHeight = textStyle.fontSize * this.textStyle.fontScale; + let charInfo: RichCharInfo = { + richOptions: [], + offsetX: [], + width: 0, + lineHeight: fontHeight * this.singleLineHeight, + offsetY: fontHeight * (this.singleLineHeight - 1) / 2, + }; + + this.processedTextOptions.forEach((options, index) => { + const { text, isNewLine, fontSize } = options; + + if (isNewLine) { + charsInfo.push(charInfo); + width = Math.max(width, charInfo.width); + charInfo = { + richOptions: [], + offsetX: [], + width: 0, + lineHeight: fontHeight * this.singleLineHeight, + offsetY: fontHeight * (this.singleLineHeight - 1) / 2, + }; + height += charInfo.lineHeight; + } + const textWidth = context.measureText(text).width; + const textHeight = fontSize * this.singleLineHeight * this.textStyle.fontScale; + + if (textHeight > charInfo.lineHeight) { + height += textHeight - charInfo.lineHeight; + + charInfo.lineHeight = textHeight; + charInfo.offsetY = fontSize * this.textStyle.fontScale * (this.singleLineHeight - 1) / 2; + } + charInfo.offsetX.push(charInfo.width); + + charInfo.width += textWidth * fontSize * this.SCALE_FACTOR * this.textStyle.fontScale; + charInfo.richOptions.push(options); + }); + charsInfo.push(charInfo); + width = Math.max(width, charInfo.width); + height += charInfo.lineHeight; + if (width === 0 || height === 0) { + this.isDirty = false; + + return; + } + const size = this.item.transform.size; + + this.item.transform.size.set(size.x * width * this.SCALE_FACTOR * this.SCALE_FACTOR, size.y * height * this.SCALE_FACTOR * this.SCALE_FACTOR); + this.textLayout.width = width / textStyle.fontScale; + this.textLayout.height = height / textStyle.fontScale; + this.canvas.width = width; + this.canvas.height = height; + context.clearRect(0, 0, width, height); + // fix bug 1/255 + context.fillStyle = `rgba(255, 255, 255, ${this.ALPHA_FIX_VALUE})`; + if (!flipY) { + context.translate(0, height); + context.scale(1, -1); + } + let charsLineHeight = charsInfo[0].lineHeight - charsInfo[0].offsetY; + + if (charsInfo.length === 0) { + return; + } + charsInfo.forEach((charInfo, index) => { + const { richOptions, offsetX } = charInfo; + const x = textLayout.getOffsetX(textStyle, charInfo.width); + + if (index > 0) { + charsLineHeight += charInfo.lineHeight - charInfo.offsetY + charsInfo[index - 1].offsetY; + } + richOptions.forEach((options, index) => { + const { fontScale, textColor, fontFamily: textFamily, textWeight, fontStyle: richStyle } = textStyle; + const { text, fontSize, fontColor = textColor, fontFamily = textFamily, fontWeight = textWeight, fontStyle = richStyle } = options; + + context.font = `${fontStyle} ${fontWeight} ${fontSize * fontScale}px ${fontFamily}`; + + context.fillStyle = `rgba(${fontColor[0]}, ${fontColor[1]}, ${fontColor[2]}, ${fontColor[3]})`; + + context.fillText(text, offsetX[index] + x, charsLineHeight); + }); + }); + + //与 toDataURL() 两种方式都需要像素读取操作 + const imageData = context.getImageData(0, 0, this.canvas.width, this.canvas.height); + + this.material.setTexture('_MainTex', + Texture.createWithData( + this.engine, + { + data: new Uint8Array(imageData.data), + width: imageData.width, + height: imageData.height, + }, + { + flipY, + magFilter: glContext.LINEAR, + minFilter: glContext.LINEAR, + wrapS: glContext.CLAMP_TO_EDGE, + wrapT: glContext.CLAMP_TO_EDGE, + }, + ), + ); + + this.isDirty = false; + context.restore(); + } + + override updateWithOptions (options: spec.TextContentOptions) { + this.textStyle = new TextStyle(options); + this.textLayout = new TextLayout(options); + this.text = options.text ? options.text.toString() : ' '; + } + +} diff --git a/plugin-packages/rich-text/src/rich-text-loader.ts b/plugin-packages/rich-text/src/rich-text-loader.ts new file mode 100644 index 000000000..d7939a9b6 --- /dev/null +++ b/plugin-packages/rich-text/src/rich-text-loader.ts @@ -0,0 +1,5 @@ +import { AbstractPlugin } from '@galacean/effects'; + +export class RichTextLoader extends AbstractPlugin { + override name = 'richtext'; +} diff --git a/plugin-packages/rich-text/src/rich-text-parser.ts b/plugin-packages/rich-text/src/rich-text-parser.ts new file mode 100644 index 000000000..57b239def --- /dev/null +++ b/plugin-packages/rich-text/src/rich-text-parser.ts @@ -0,0 +1,221 @@ +enum TokenType { + ContextStart = 'ContextStart', + Text = 'Text', + ContextEnd = 'ContextEnd', +} + +export type Token = { + tokenType: TokenType, + value: string, +}; + +const contextStartRegexp = /^<([a-z]+)(=([^>]+))?>$/; +const contextEndRegexp = /^<\/([a-z]+)>$/; + +const rules: [TokenType, RegExp][] = [ + [TokenType.ContextStart, /^<[a-z]+(=[^>]+)?>/], + [TokenType.Text, /^[^=]+/], + [TokenType.ContextEnd, /^<\/[a-z]+>/], +]; + +export const lexer = (input: string, lexed: Token[] = [], cursor = 0): Token[] => { + if (!input) { + return lexed; + } + + for (const [tokenType, regex] of rules) { + const [tokenMatch] = regex.exec(input) ?? []; + + if (tokenMatch) { + + const len = tokenMatch.length; + + return lexer(input.slice(len), lexed.concat({ tokenType, value: tokenMatch }), cursor + len); + } + } + + throw new Error(`Unexpected token: "${input[0]}" at position ${cursor} while reading "${input}"`); +}; + +export type Attribute = { + attributeName?: string, + attributeParam?: string, +}; + +export type RichTextAST = { + attributes: Attribute[], + text: string, +}; + +export const richTextParser = (input: string): RichTextAST[] => { + const lexed = lexer(input); + let cursor = 0; + + const shift = () => { + const shifted = lexed.shift(); + + cursor += shifted?.value.length ?? 0; + + return shifted; + }; + + const peek = () => { + return lexed[0]; + }; + + const ast: RichTextAST[] = []; + + function Grammar (attributes: Attribute[] = [], expectedEndAttributeName = '') { + const parsing = true; + + while (parsing) { + const maybeText = Text(); + + if (maybeText) { + ast.push({ + attributes, + text: maybeText, + }); + + continue; + } + + const { attributeName, attributeParam } = ContextStart(); + + if (attributeName) { + Grammar(attributes.concat({ attributeName, attributeParam }), attributeName); + + continue; + } + + if (expectedEndAttributeName) { + const { attributeName: endAttributeName } = ContextEnd(); + + if (!endAttributeName) { + throw new Error('Expect an end tag marker "' + expectedEndAttributeName + '" at position ' + cursor + ' but found no tag!'); + } + + if (endAttributeName !== expectedEndAttributeName) { + throw new Error('Expect an end tag marker "' + expectedEndAttributeName + '" at position ' + cursor + ' but found tag "' + endAttributeName + '"'); + } + + return; + } + + break; + } + } + + function Text (): string | undefined { + const maybeText = peek(); + + if (maybeText?.tokenType === TokenType.Text) { + shift(); + + return maybeText.value; + } + + return undefined; + } + + function ContextStart (): { attributeName?: string, attributeParam?: string } { + const maybeContextStart = peek(); + + if (maybeContextStart?.tokenType === TokenType.ContextStart) { + shift(); + + const matches = maybeContextStart.value.match(contextStartRegexp); + + if (matches) { + const attributeName = matches[1]; + const attributeParam = matches[3] ?? ''; + + return { attributeName, attributeParam }; + } + + throw new Error('Expected a start tag marker at position ' + cursor); + } + + return {}; + } + + function ContextEnd (): { attributeName?: string } { + const maybeContextEnd = peek(); + + if (maybeContextEnd?.tokenType === TokenType.ContextEnd) { + shift(); + + const matches = maybeContextEnd.value.match(contextEndRegexp); + + if (matches) { + const attributeName = matches[1]; + + return { attributeName }; + } + + throw new Error('Expect an end tag marker at position ' + cursor); + } + + return {}; + } + + Grammar(); + + return ast; +}; + +export function generateProgram (textHandler: (text: string, context: Record) => void) { + return (richText: string) => { + const ast = richTextParser(richText); + + for (const node of ast) { + const text = node.text; + const context = node.attributes.reduce>((ctx, { attributeName, attributeParam }) => { + if (attributeName) { + ctx[attributeName] = attributeParam; + } + + return ctx; + }, {}); + + textHandler(text, context); + } + }; +} + +export function isRichText (text: string): boolean { + const lexed = lexer(text); + + const contextTokens = lexed.filter(({ tokenType }) => tokenType === TokenType.ContextStart || tokenType === TokenType.ContextEnd); + + const contextStartTokens = contextTokens.filter(({ tokenType }) => tokenType === TokenType.ContextStart); + const contextEndTokens = contextTokens.filter(({ tokenType }) => tokenType === TokenType.ContextEnd); + + if (contextStartTokens.length !== contextEndTokens.length || !contextStartTokens.length) { + return false; + } + + const tokensOfAttribute = contextTokens.map(({ tokenType, value }) => ({ tokenType, value: tokenType === TokenType.ContextStart ? value.match(contextStartRegexp)![1] : value.match(contextEndRegexp)![1] })); + + function checkPaired ([token, ...restToken]: Token[], startContextAttributes: string[] = []): boolean { + if (!token) { + return startContextAttributes.length === 0; + } + + if (token.tokenType === TokenType.ContextStart) { + return checkPaired(restToken, startContextAttributes.concat(token.value)); + } else if (token.tokenType === TokenType.ContextEnd) { + const attributeName = startContextAttributes[startContextAttributes.length - 1]; + + if (attributeName !== token.value) { + return false; + } + + return checkPaired(restToken, startContextAttributes.slice(0, -1)); + } + + throw new Error('Unexpected token: ' + token.tokenType); + } + + return checkPaired(tokensOfAttribute); +} diff --git a/plugin-packages/rich-text/test/index.html b/plugin-packages/rich-text/test/index.html new file mode 100644 index 000000000..6ec1a7c41 --- /dev/null +++ b/plugin-packages/rich-text/test/index.html @@ -0,0 +1,46 @@ + + + + + Plugin RichText Tests + + + + + + + + + +
+ + + + + + + + + + + diff --git a/plugin-packages/rich-text/test/src/index.ts b/plugin-packages/rich-text/test/src/index.ts new file mode 100644 index 000000000..e335e5fc0 --- /dev/null +++ b/plugin-packages/rich-text/test/src/index.ts @@ -0,0 +1,2 @@ +import '@galacean/effects-plugin-rich-text'; +import './rich-text-parser.spec'; diff --git a/plugin-packages/rich-text/test/src/rich-text-parser.spec.ts b/plugin-packages/rich-text/test/src/rich-text-parser.spec.ts new file mode 100644 index 000000000..54d319b29 --- /dev/null +++ b/plugin-packages/rich-text/test/src/rich-text-parser.spec.ts @@ -0,0 +1,289 @@ +import { lexer, richTextParser as parser, generateProgram, isRichText, type RichTextAST, type Attribute } from '@galacean/effects-plugin-rich-text'; +const { expect } = chai; +const richText = ` + We are absolutely definitely not amused + We are green with envy + We are amused. + We are not amused. + We are usually not amused. + We are largely unaffected. + We are colorfully amused +`; + +const richTextTokens = [ + { + 'tokenType': 'Text', + 'value': '\n We are ', + }, { + 'tokenType': 'ContextStart', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': 'absolutely ', + }, { + 'tokenType': 'ContextStart', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': 'definitely', + }, { + 'tokenType': 'ContextEnd', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': ' not', + }, { + 'tokenType': 'ContextEnd', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': ' amused\n We are ', + }, { + 'tokenType': 'ContextStart', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': 'green', + }, { + 'tokenType': 'ContextEnd', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': ' with envy\n We are ', + }, { + 'tokenType': 'ContextStart', + 'value': '', + }, { + 'tokenType': 'ContextEnd', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': ' amused.\n We are ', + }, { + 'tokenType': 'ContextStart', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': 'not', + }, { + 'tokenType': 'ContextEnd', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': ' amused.\n We are ', + }, { + 'tokenType': 'ContextStart', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': 'usually', + }, { + 'tokenType': 'ContextEnd', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': ' not amused.\n We are ', + }, { + 'tokenType': 'ContextStart', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': 'largely', + }, { + 'tokenType': 'ContextEnd', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': ' unaffected.\n We are ', + }, { + 'tokenType': 'ContextStart', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': 'colorfully', + }, { + 'tokenType': 'ContextEnd', + 'value': '', + }, { + 'tokenType': 'Text', + 'value': ' amused\n', + }, +]; + +const richTextTokenValues = [ + '\n' + + ' We are ', '', 'absolutely ', '', 'definitely', '', ' not', '', ' amused\n' + + ' We are ', '', 'green', '', ' with envy\n' + + ' We are ', '', '', ' amused.\n' + + ' We are ', '', 'not', '', ' amused.\n' + + ' We are ', '', 'usually', '', ' not amused.\n' + + ' We are ', '', 'largely', '', ' unaffected.\n' + + ' We are ', '', 'colorfully', '', ' amused\n', +]; + +const richTextAst: RichTextAST[] = [ + { attributes: [], text: '\n We are ' }, + { attributes: [{ attributeName: 'b', attributeParam: '' }], text: 'absolutely ' }, + { attributes: [{ attributeName: 'b', attributeParam: '' }, { attributeName: 'i', attributeParam: '' }], text: 'definitely' }, + { attributes: [{ attributeName: 'b', attributeParam: '' }], text: ' not' }, + { attributes: [], text: ' amused\n We are ' }, + { attributes: [{ attributeName: 'color', attributeParam: 'green' }], text: 'green' }, + { attributes: [], text: ' with envy\n We are ' }, + { attributes: [], text: ' amused.\n We are ' }, + { attributes: [{ attributeName: 'b', attributeParam: '' }], text: 'not' }, + { attributes: [], text: ' amused.\n We are ' }, + { attributes: [{ attributeName: 'i', attributeParam: '' }], text: 'usually' }, + { attributes: [], text: ' not amused.\n We are ' }, + { attributes: [{ attributeName: 'size', attributeParam: '50' }], text: 'largely' }, + { attributes: [], text: ' unaffected.\n We are ' }, + { attributes: [{ attributeName: 'color', attributeParam: '#ff0000ff' }], text: 'colorfully' }, + { attributes: [], text: ' amused\n' }, +]; + +const richTextAndContext = [ + { text: '\n We are ', context: {} }, + { text: 'absolutely ', context: { b: '' } }, + { text: 'definitely', context: { b: '', i: '' } }, + { text: ' not', context: { b: '' } }, + { text: ' amused\n We are ', context: {} }, + { text: 'green', context: { color: 'green' } }, + { text: ' with envy\n We are ', context: {} }, + { text: ' amused.\n We are ', context: {} }, + { text: 'not', context: { b: '' } }, + { text: ' amused.\n We are ', context: {} }, + { text: 'usually', context: { i: '' } }, + { text: ' not amused.\n We are ', context: {} }, + { text: 'largely', context: { size: '50' } }, + { text: ' unaffected.\n We are ', context: {} }, + { text: 'colorfully', context: { color: '#ff0000ff' } }, + { text: ' amused\n', context: {} }, +]; + +describe('test lexer and parser', () => { + it('lexer', () => { + const lexed = lexer(richText); + + expect(lexed).to.deep.equals(richTextTokens); + + expect(lexed.map(token => token.value)).to.deep.equals(richTextTokenValues); + }); + + it('parser', () => { + const parsed = parser(richText); + + expect(parsed).to.deep.equals(richTextAst); + }); + + it('generateProgram', () => { + const processedTextAndContext: Array<{ text: string, context: Record }> = []; + + const program = generateProgram((text, context) => { + processedTextAndContext.push({ text, context }); + }); + + program(richText); + + expect(processedTextAndContext).to.deep.equals(richTextAndContext); + }); +}); + +describe('test lexer and parser with wrapped rich text', () => { + const wrappedRichText = '' + richText + ''; + + const wrappedRichTextTokens = [{ + tokenType: 'ContextStart', + value: '', + }].concat(richTextTokens).concat([{ + tokenType: 'ContextEnd', + value: '', + }]); + + const wrappedRichTextTokenValues = [''].concat(richTextTokenValues).concat(['']); + + const wrappedRichTextAst: RichTextAST[] = richTextAst.map(node => ({ + ...node, + attributes: ([{ attributeName: 'del', attributeParam: '' }] as Attribute[]).concat(node.attributes), + })); + + const wrappedRichTextAndContext = richTextAndContext.map(node => ({ + ...node, + context: { ...node.context, del: '' }, + })); + + it('lexer', () => { + const lexed = lexer(wrappedRichText); + + expect(lexed).to.deep.equals(wrappedRichTextTokens); + + expect(lexed.map(token => token.value)).to.deep.equals(wrappedRichTextTokenValues); + }); + + it('parser', () => { + const parsed = parser(wrappedRichText); + + expect(parsed).to.deep.equals(wrappedRichTextAst); + }); + + it('generateProgram', () => { + const processedTextAndContext: Array<{ text: string, context: Record }> = []; + + const program = generateProgram((text, context) => { + processedTextAndContext.push({ text, context }); + }); + + program(wrappedRichText); + + expect(processedTextAndContext).to.deep.equals(wrappedRichTextAndContext); + }); +}); + +const unparsableRichText1 = ` + We are absolutely definitely not amused +`; + +const unparsableRichText2 = ` + We are absolutely +`; + +describe('test unparsable text', () => { + it('case 1', () => { + const lexed = lexer(unparsableRichText1); + + expect(lexed).to.deep.equals([ + { tokenType: 'Text', value: '\n We are ' }, + { tokenType: 'ContextStart', value: '' }, + { tokenType: 'Text', value: 'absolutely ' }, + { tokenType: 'ContextStart', value: '' }, + { tokenType: 'Text', value: 'definitely' }, + { tokenType: 'ContextEnd', value: '' }, + { tokenType: 'Text', value: ' not' }, + { tokenType: 'ContextEnd', value: '' }, + { tokenType: 'Text', value: ' amused\n' }, + ]); + + expect(() => parser(unparsableRichText1)).to.throw('Expect an end tag marker "i" at position 41 but found tag "b"'); + }); + + it('case 2', () => { + const lexed = lexer(unparsableRichText2); + + expect(lexed).to.deep.equals([ + { tokenType: 'Text', value: '\n We are ' }, + { tokenType: 'ContextStart', value: '' }, + { tokenType: 'Text', value: 'absolutely\n' }, + ]); + + expect(() => parser(unparsableRichText2)).to.throw('Expect an end tag marker "color" at position 34 but found no tag!'); + }); +}); + +describe('test isRichText', () => { + it('should return true for rich text', () => { + expect(isRichText(richText)).to.be.true; + }); + + it('should return false for unparsable rich text', () => { + expect(isRichText(unparsableRichText1)).to.be.false; + expect(isRichText(unparsableRichText2)).to.be.false; + }); +}); diff --git a/plugin-packages/rich-text/test/tsconfig.json b/plugin-packages/rich-text/test/tsconfig.json new file mode 100644 index 000000000..0e8412eb6 --- /dev/null +++ b/plugin-packages/rich-text/test/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [ + "src" + ] +} diff --git a/plugin-packages/rich-text/tsconfig.json b/plugin-packages/rich-text/tsconfig.json new file mode 100644 index 000000000..ecbcdd515 --- /dev/null +++ b/plugin-packages/rich-text/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "composite": true + }, + "include": [ + "src", + "../../types" + ], + "references": [ + { + "path": "../../packages/effects" + } + ] +} diff --git a/plugin-packages/rich-text/typedoc.json b/plugin-packages/rich-text/typedoc.json new file mode 100644 index 000000000..b6aba3cbd --- /dev/null +++ b/plugin-packages/rich-text/typedoc.json @@ -0,0 +1,8 @@ +{ + "extends": [ + "../../typedoc.base.json" + ], + "entryPoints": [ + "src/index.ts" + ] +} diff --git a/plugin-packages/rich-text/vite.config.js b/plugin-packages/rich-text/vite.config.js new file mode 100644 index 000000000..fac66d149 --- /dev/null +++ b/plugin-packages/rich-text/vite.config.js @@ -0,0 +1,70 @@ +import { resolve } from 'path'; +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import legacy from '@vitejs/plugin-legacy'; +import ip from 'ip'; +import { glslInner, getSWCPlugin } from '../../scripts/rollup-config-helper'; + +export default defineConfig(({ mode }) => { + const development = mode === 'development'; + + return { + base: './', + build: { + rollupOptions: { + input: { + 'index': resolve(__dirname, 'demo/index.html'), + 'simple': resolve(__dirname, 'demo/simple.html'), + } + }, + minify: false, // iOS 9 等低版本加载压缩代码报脚本异常 + }, + server: { + host: '0.0.0.0', + port: 8081, + }, + preview: { + host: '0.0.0.0', + port: 8081, + }, + define: { + __VERSION__: 0, + __DEBUG__: development, + }, + plugins: [ + legacy({ + targets: ['iOS >= 9'], + modernPolyfills: ['es/global-this'], + }), + glslInner(), + getSWCPlugin({ + baseUrl: resolve(__dirname, '..', '..'), + }), + tsconfigPaths(), + configureServerPlugin(), + ], + }; +}); + +// 用于配置开发服务器的钩子 +function configureServerPlugin() { + const handleServer = function (server) { + const host = ip.address() ?? 'localhost'; + const port = server.config.server.port; + const baseUrl = `http://${host}:${port}`; + + setTimeout(() => { + console.log(` \x1b[1m\x1b[32m->\x1b[97m Demo: \x1b[0m\x1b[96m${baseUrl}/demo/index.html\x1b[0m`); + }, 1000); + } + + return { + name: 'configure-server', + configurePreviewServer(server) { + server.httpServer.once('listening', handleServer.bind(this, server)); + }, + configureServer(server) { + server.httpServer.once('listening', handleServer.bind(this, server)); + }, + } +} diff --git a/plugin-packages/spine/src/index.ts b/plugin-packages/spine/src/index.ts index 3abc646a3..2ad451ada 100644 --- a/plugin-packages/spine/src/index.ts +++ b/plugin-packages/spine/src/index.ts @@ -20,6 +20,9 @@ export { registerPlugin('spine', class SpineLoader extends AbstractPlugin { }, VFXItem); +/** + * 插件版本号 + */ export const version = __VERSION__; logger.info(`Plugin spine version: ${version}.`); diff --git a/plugin-packages/spine/src/spine-component.ts b/plugin-packages/spine/src/spine-component.ts index 873b1c4c7..52d0d93f4 100644 --- a/plugin-packages/spine/src/spine-component.ts +++ b/plugin-packages/spine/src/spine-component.ts @@ -57,9 +57,10 @@ export interface SpineMaskOptions { } /** + * Spine 组件 * @since 2.0.0 */ -@effectsClass('SpineComponent') +@effectsClass(spec.DataType.SpineComponent) export class SpineComponent extends RendererComponent { startSize: number; /** @@ -147,8 +148,8 @@ export class SpineComponent extends RendererComponent { } } - override start () { - super.start(); + override onStart () { + super.onStart(); if (!this.cache) { return; } @@ -160,11 +161,11 @@ export class SpineComponent extends RendererComponent { return; } this.state.apply(this.skeleton); - this.update(0); + this.onUpdate(0); this.resize(); } - override update (dt: number) { + override onUpdate (dt: number) { if (!(this.state && this.skeleton)) { return; } diff --git a/plugin-packages/stats/src/index.ts b/plugin-packages/stats/src/index.ts index c7e87e1f9..eaffac1ca 100644 --- a/plugin-packages/stats/src/index.ts +++ b/plugin-packages/stats/src/index.ts @@ -1 +1,18 @@ +import * as EFFECTS from '@galacean/effects'; +import { logger } from '@galacean/effects'; + export * from './stats'; + +/** + * 插件版本号 + */ +export const version = __VERSION__; + +logger.info(`Plugin stats version: ${version}.`); + +if (version !== EFFECTS.version) { + console.error( + '注意:请统一 Stats 插件与 Player 版本,不统一的版本混用会有不可预知的后果!', + '\nAttention: Please ensure the Stats plugin is synchronized with the Player version. Mixing and matching incompatible versions may result in unpredictable consequences!' + ); +} diff --git a/plugin-packages/stats/src/stats-component.ts b/plugin-packages/stats/src/stats-component.ts index 94c80211f..84a7b201d 100644 --- a/plugin-packages/stats/src/stats-component.ts +++ b/plugin-packages/stats/src/stats-component.ts @@ -7,14 +7,14 @@ export class StatsComponent extends Behaviour { */ monitor: Monitor; - override start (): void { + override onStart (): void { const renderer = this.engine.renderer as GLRenderer; const gl = renderer.pipelineContext.gl; this.monitor = new Monitor(gl); } - override update (dt: number): void { + override onUpdate (dt: number): void { this.monitor.update(dt); } diff --git a/plugin-packages/stats/src/stats.ts b/plugin-packages/stats/src/stats.ts index ceaa035cc..2645a7c3f 100644 --- a/plugin-packages/stats/src/stats.ts +++ b/plugin-packages/stats/src/stats.ts @@ -79,9 +79,17 @@ const json = { 'compositionId': '2', }; +/** + * + */ export class Stats { static options: StatsOptions; + /** + * + * @param player + * @param options + */ constructor ( public readonly player: Player, options: StatsOptions = { debug: false }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51734b7cc..57e8baa3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,11 +150,14 @@ importers: specifier: 1.1.0 version: 1.1.0 '@galacean/effects-specification': - specifier: 2.0.2 - version: 2.0.2 + specifier: 2.1.0 + version: 2.1.0 flatbuffers: specifier: 24.3.25 version: 24.3.25 + libtess: + specifier: 1.2.2 + version: 1.2.2 uuid: specifier: 9.0.1 version: 9.0.1 @@ -224,8 +227,8 @@ importers: specifier: ^2.0.45 version: 2.0.45 '@vvfx/resource-detection': - specifier: ^0.6.2 - version: 0.6.2 + specifier: ^0.7.0 + version: 0.7.0 fpsmeter: specifier: ^0.3.1 version: 0.3.1 @@ -233,12 +236,24 @@ importers: specifier: ^2.0.8 version: 2.0.8 + plugin-packages/multimedia: + devDependencies: + '@galacean/effects': + specifier: workspace:* + version: link:../../packages/effects + plugin-packages/orientation-transformer: devDependencies: '@galacean/effects': specifier: workspace:* version: link:../../packages/effects + plugin-packages/rich-text: + devDependencies: + '@galacean/effects': + specifier: workspace:* + version: link:../../packages/effects + plugin-packages/spine: dependencies: '@esotericsoftware/spine-core': @@ -323,8 +338,8 @@ importers: specifier: workspace:* version: link:../../packages/effects-webgl '@vvfx/resource-detection': - specifier: ^0.6.2 - version: 0.6.2 + specifier: ^0.7.0 + version: 0.7.0 packages: @@ -2198,11 +2213,11 @@ packages: /@galacean/effects-specification@2.0.0: resolution: {integrity: sha512-7ieZALZYn5fwKLIGAskmYSKGX82ZkLRdDUInG21KsOJ2eQ5+HcI6mJU5tmN8vjZxQUc+RVuUcssExGbiaj2+5w==} - - /@galacean/effects-specification@2.0.2: - resolution: {integrity: sha512-9X7MJ79Y2LQ1W7PEl+wJqlYwBLNe3kffDL2mIV5C9U3wwYbUW9giBFHTKhgsSSHMXKbGR1v3JBUyYH0zckdGTQ==} dev: false + /@galacean/effects-specification@2.1.0: + resolution: {integrity: sha512-gad/CBHvTlL182QExrPGqzafzfMjSEfVWRA4+mufHMvxlsDF0kEIvZ2EEcyG9IgvRkX81F1PU1vk+or6G1j2nw==} + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -2513,6 +2528,7 @@ packages: resolution: {integrity: sha512-bur4JOxvYxfrAmocRJIW0SADs3QdEYK6TQ7dTNz6Z4/lySeu3Z1H/+tl0a4qDYv0bCdBpUYM0sYa/X+9ZqgfSQ==} cpu: [arm64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -2521,6 +2537,7 @@ packages: resolution: {integrity: sha512-ssp77SjcDIUSoUyj7DU7/5iwM4ZEluY+N8umtCT9nBRs3u045t0KkW02LTyHouHDomnMXaXSZcCSr2bdMK63kA==} cpu: [arm64] os: [linux] + libc: [musl] requiresBuild: true dev: true optional: true @@ -2529,6 +2546,7 @@ packages: resolution: {integrity: sha512-Jv1DkIvwEPAb+v25/Unrnnq9BO3F5cbFPT821n3S5litkz+O5NuXuNhqtPx5KtcwOTtaqkTsO+IVzJOsxd11aQ==} cpu: [riscv64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -2537,6 +2555,7 @@ packages: resolution: {integrity: sha512-U564BrhEfaNChdATQaEODtquCC7Ez+8Hxz1h5MAdMYj0AqD0GA9rHCpElajb/sQcaFL6NXmHc5O+7FXpWMa73Q==} cpu: [s390x] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -2545,6 +2564,7 @@ packages: resolution: {integrity: sha512-zGRDulLTeDemR8DFYyFIQ8kMP02xpUsX4IBikc7lwL9PrwR3gWmX2NopqiGlI2ZVWMl15qZeUjumTwpv18N7sQ==} cpu: [x64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -2553,6 +2573,7 @@ packages: resolution: {integrity: sha512-VTk/MveyPdMFkYJJPCkYBw07KcTkGU2hLEyqYMsU4NjiOfzoaDTW9PWGRsNwiOA3qI0k/JQPjkl/4FCK1smskQ==} cpu: [x64] os: [linux] + libc: [musl] requiresBuild: true dev: true optional: true @@ -2618,6 +2639,7 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -2627,6 +2649,7 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] requiresBuild: true dev: true optional: true @@ -2636,6 +2659,7 @@ packages: engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -2645,6 +2669,7 @@ packages: engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] requiresBuild: true dev: true optional: true @@ -2993,6 +3018,26 @@ packages: ndarray-lanczos: 0.1.2 ndarray-pixels: 1.0.0 node-fetch: 3.3.2 + dev: false + + /@vvfx/resource-detection@0.7.0: + resolution: {integrity: sha512-fV34OZRbRIjP1OMDjPfuLsZszpGg56DhSf1NeLUu4g6RJszwNFJ/Uazh7PVCrV8NyZhrWWyGQub3BA2CbE1SxQ==} + engines: {node: '>=10.0.0'} + dependencies: + '@galacean/effects-math': 1.1.0 + '@galacean/effects-specification': 2.1.0 + '@types/draco3dgltf': 1.4.3 + '@types/ndarray': 1.0.11 + '@types/sharp': 0.31.1 + fflate: 0.7.4 + gl-matrix: 3.4.3 + ktx-parse: 0.4.5 + maxrects-packer: 2.7.3 + meshoptimizer: 0.18.1 + ndarray: 1.0.19 + ndarray-lanczos: 0.1.2 + ndarray-pixels: 1.0.0 + node-fetch: 3.3.2 /JSONStream@1.3.5: resolution: {integrity: sha1-MgjB8I06TZkmGrZPkjArwV4RHKA=, tarball: https://registry.npmmirror.com/JSONStream/download/JSONStream-1.3.5.tgz} @@ -4917,6 +4962,10 @@ packages: type-check: 0.4.0 dev: true + /libtess@1.2.2: + resolution: {integrity: sha512-Nps8HPeVVcsmJxUvFLKVJcCgcz+1ajPTXDVAVPs6+giOQP4AHV31uZFFkh+CKow/bkB7GbZWKmwmit7myaqDSw==} + dev: false + /lines-and-columns@1.2.4: resolution: {integrity: sha1-7KKE910pZQeTCdwK2SVauy68FjI=, tarball: https://registry.npmmirror.com/lines-and-columns/download/lines-and-columns-1.2.4.tgz} dev: true diff --git a/tsconfig.base.json b/tsconfig.base.json index e5c4b870c..94cae4817 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -23,6 +23,7 @@ "target": "ESNext", "lib": [ "DOM", + "DOM.Iterable", "ES2015", "ESNext" ], diff --git a/typedoc.json b/typedoc.json index a483d36da..cbfe2fd8e 100644 --- a/typedoc.json +++ b/typedoc.json @@ -5,10 +5,14 @@ "packages/effects", "packages/effects-helper", "packages/effects-threejs", + "plugin-packages/downgrade", "plugin-packages/editor-gizmo", "plugin-packages/model", + "plugin-packages/multimedia", "plugin-packages/orientation-transformer", - "plugin-packages/spine" + "plugin-packages/rich-text", + "plugin-packages/spine", + "plugin-packages/stats" ], "entryPointStrategy": "packages", "out": "api", diff --git a/types/shim.d.ts b/types/shim.d.ts index 9f19c9558..5603385da 100644 --- a/types/shim.d.ts +++ b/types/shim.d.ts @@ -1,5 +1,6 @@ declare module 'string-hash'; declare module 'uuid'; +declare module 'libtess'; declare module 'earcut' { export interface Node { diff --git a/types/vendors.d.ts b/types/vendors.d.ts index 3f028e16d..c3752f99b 100644 --- a/types/vendors.d.ts +++ b/types/vendors.d.ts @@ -11,6 +11,7 @@ interface Window { _createOffscreenCanvas: (width: number, height: number) => HTMLCanvasElement; AlipayJSBridge: any; WindVane: any; + ge: any, __wxjs_environment: any; } diff --git a/web-packages/demo/html/inspire/index.html b/web-packages/demo/html/inspire/index.html index f768c86f1..734309d36 100644 --- a/web-packages/demo/html/inspire/index.html +++ b/web-packages/demo/html/inspire/index.html @@ -45,23 +45,19 @@
-
- -
-
- -
+ + +
+
+
- +
-
- -
diff --git a/web-packages/demo/html/post-processing.html b/web-packages/demo/html/post-processing.html index f4808db48..9f961907a 100644 --- a/web-packages/demo/html/post-processing.html +++ b/web-packages/demo/html/post-processing.html @@ -7,43 +7,41 @@ +
+
-
-
-
-
- -
-
-
- -
-
-
-
- +
+
+
+
+
- +
+
+
-
-
- + - diff --git a/web-packages/demo/html/shader-compile.html b/web-packages/demo/html/shader-compile.html new file mode 100644 index 000000000..64da0dd02 --- /dev/null +++ b/web-packages/demo/html/shader-compile.html @@ -0,0 +1,42 @@ + + + + + + Shader 编译测试 - demo + + + +

优化后

+
+
+ +
+
+ + + + diff --git a/web-packages/demo/html/shape.html b/web-packages/demo/html/shape.html new file mode 100644 index 000000000..c4caf0406 --- /dev/null +++ b/web-packages/demo/html/shape.html @@ -0,0 +1,21 @@ + + + + + + 图形元素 - demo + + + +
+ + + + diff --git a/web-packages/demo/index.html b/web-packages/demo/index.html index 6fcef3b31..1bb7b57bf 100644 --- a/web-packages/demo/index.html +++ b/web-packages/demo/index.html @@ -12,16 +12,18 @@
Runtime Demo
Inspire compare Demo
diff --git a/web-packages/demo/html/assets/find-flower/bins/29840fd84400b6d9c353c7fd68795862.bin b/web-packages/demo/public/assets/find-flower/bins/29840fd84400b6d9c353c7fd68795862.bin similarity index 100% rename from web-packages/demo/html/assets/find-flower/bins/29840fd84400b6d9c353c7fd68795862.bin rename to web-packages/demo/public/assets/find-flower/bins/29840fd84400b6d9c353c7fd68795862.bin diff --git "a/web-packages/demo/html/assets/find-flower/downgrade/\346\230\245\350\212\261 .png" "b/web-packages/demo/public/assets/find-flower/downgrade/\346\230\245\350\212\261 .png" similarity index 100% rename from "web-packages/demo/html/assets/find-flower/downgrade/\346\230\245\350\212\261 .png" rename to "web-packages/demo/public/assets/find-flower/downgrade/\346\230\245\350\212\261 .png" diff --git a/web-packages/demo/html/assets/find-flower/flower.json b/web-packages/demo/public/assets/find-flower/flower.json similarity index 100% rename from web-packages/demo/html/assets/find-flower/flower.json rename to web-packages/demo/public/assets/find-flower/flower.json diff --git a/web-packages/demo/html/assets/find-flower/images/0aa1a16c1c322c07a97c4ca8a13158ab.webp b/web-packages/demo/public/assets/find-flower/images/0aa1a16c1c322c07a97c4ca8a13158ab.webp similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/0aa1a16c1c322c07a97c4ca8a13158ab.webp rename to web-packages/demo/public/assets/find-flower/images/0aa1a16c1c322c07a97c4ca8a13158ab.webp diff --git a/web-packages/demo/html/assets/find-flower/images/1514bc9f1cc698dbca055077b9add948.webp b/web-packages/demo/public/assets/find-flower/images/1514bc9f1cc698dbca055077b9add948.webp similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/1514bc9f1cc698dbca055077b9add948.webp rename to web-packages/demo/public/assets/find-flower/images/1514bc9f1cc698dbca055077b9add948.webp diff --git a/web-packages/demo/html/assets/find-flower/images/2445f46b64fa6078f73b84667d29ee31.png b/web-packages/demo/public/assets/find-flower/images/2445f46b64fa6078f73b84667d29ee31.png similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/2445f46b64fa6078f73b84667d29ee31.png rename to web-packages/demo/public/assets/find-flower/images/2445f46b64fa6078f73b84667d29ee31.png diff --git a/web-packages/demo/html/assets/find-flower/images/2d95dcb61e63295b52239745d10e8335.png b/web-packages/demo/public/assets/find-flower/images/2d95dcb61e63295b52239745d10e8335.png similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/2d95dcb61e63295b52239745d10e8335.png rename to web-packages/demo/public/assets/find-flower/images/2d95dcb61e63295b52239745d10e8335.png diff --git a/web-packages/demo/html/assets/find-flower/images/32c2e4b9cbe849bc6c2f326483088919.png b/web-packages/demo/public/assets/find-flower/images/32c2e4b9cbe849bc6c2f326483088919.png similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/32c2e4b9cbe849bc6c2f326483088919.png rename to web-packages/demo/public/assets/find-flower/images/32c2e4b9cbe849bc6c2f326483088919.png diff --git a/web-packages/demo/html/assets/find-flower/images/32d0ff42fcb9c6393a8f0c728fdb7b01.webp b/web-packages/demo/public/assets/find-flower/images/32d0ff42fcb9c6393a8f0c728fdb7b01.webp similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/32d0ff42fcb9c6393a8f0c728fdb7b01.webp rename to web-packages/demo/public/assets/find-flower/images/32d0ff42fcb9c6393a8f0c728fdb7b01.webp diff --git a/web-packages/demo/html/assets/find-flower/images/3e7a36e83f8f11dd9bcabc7e9a67f58b.webp b/web-packages/demo/public/assets/find-flower/images/3e7a36e83f8f11dd9bcabc7e9a67f58b.webp similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/3e7a36e83f8f11dd9bcabc7e9a67f58b.webp rename to web-packages/demo/public/assets/find-flower/images/3e7a36e83f8f11dd9bcabc7e9a67f58b.webp diff --git a/web-packages/demo/html/assets/find-flower/images/53abeccb8b994a6a8f687ae8169fa09b.webp b/web-packages/demo/public/assets/find-flower/images/53abeccb8b994a6a8f687ae8169fa09b.webp similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/53abeccb8b994a6a8f687ae8169fa09b.webp rename to web-packages/demo/public/assets/find-flower/images/53abeccb8b994a6a8f687ae8169fa09b.webp diff --git a/web-packages/demo/html/assets/find-flower/images/54c88949e7c40c717f1cb4de849b1254.png b/web-packages/demo/public/assets/find-flower/images/54c88949e7c40c717f1cb4de849b1254.png similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/54c88949e7c40c717f1cb4de849b1254.png rename to web-packages/demo/public/assets/find-flower/images/54c88949e7c40c717f1cb4de849b1254.png diff --git a/web-packages/demo/html/assets/find-flower/images/5aa626d56b3438de0e8f0574e2bccaf7.webp b/web-packages/demo/public/assets/find-flower/images/5aa626d56b3438de0e8f0574e2bccaf7.webp similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/5aa626d56b3438de0e8f0574e2bccaf7.webp rename to web-packages/demo/public/assets/find-flower/images/5aa626d56b3438de0e8f0574e2bccaf7.webp diff --git a/web-packages/demo/html/assets/find-flower/images/6d0e80e4a52a7bbe752c0c0397ca7b8c.png b/web-packages/demo/public/assets/find-flower/images/6d0e80e4a52a7bbe752c0c0397ca7b8c.png similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/6d0e80e4a52a7bbe752c0c0397ca7b8c.png rename to web-packages/demo/public/assets/find-flower/images/6d0e80e4a52a7bbe752c0c0397ca7b8c.png diff --git a/web-packages/demo/html/assets/find-flower/images/87269cd72fb97a8f6f9ec3aa26d48a9b.png b/web-packages/demo/public/assets/find-flower/images/87269cd72fb97a8f6f9ec3aa26d48a9b.png similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/87269cd72fb97a8f6f9ec3aa26d48a9b.png rename to web-packages/demo/public/assets/find-flower/images/87269cd72fb97a8f6f9ec3aa26d48a9b.png diff --git a/web-packages/demo/html/assets/find-flower/images/97e25a8ee94e3afdf2883f5b8cbcdc2d.webp b/web-packages/demo/public/assets/find-flower/images/97e25a8ee94e3afdf2883f5b8cbcdc2d.webp similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/97e25a8ee94e3afdf2883f5b8cbcdc2d.webp rename to web-packages/demo/public/assets/find-flower/images/97e25a8ee94e3afdf2883f5b8cbcdc2d.webp diff --git a/web-packages/demo/html/assets/find-flower/images/9aefb8d71a0d41f7f1f095871d3f606f.png b/web-packages/demo/public/assets/find-flower/images/9aefb8d71a0d41f7f1f095871d3f606f.png similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/9aefb8d71a0d41f7f1f095871d3f606f.png rename to web-packages/demo/public/assets/find-flower/images/9aefb8d71a0d41f7f1f095871d3f606f.png diff --git a/web-packages/demo/html/assets/find-flower/images/9eafe210d133af4ff3e37fefce471b0e.webp b/web-packages/demo/public/assets/find-flower/images/9eafe210d133af4ff3e37fefce471b0e.webp similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/9eafe210d133af4ff3e37fefce471b0e.webp rename to web-packages/demo/public/assets/find-flower/images/9eafe210d133af4ff3e37fefce471b0e.webp diff --git a/web-packages/demo/html/assets/find-flower/images/c2515f3630679cebeb1c6463819c9b84.webp b/web-packages/demo/public/assets/find-flower/images/c2515f3630679cebeb1c6463819c9b84.webp similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/c2515f3630679cebeb1c6463819c9b84.webp rename to web-packages/demo/public/assets/find-flower/images/c2515f3630679cebeb1c6463819c9b84.webp diff --git a/web-packages/demo/html/assets/find-flower/images/c6f2a6bdada8e8cbbc5409ea25b3173b.png b/web-packages/demo/public/assets/find-flower/images/c6f2a6bdada8e8cbbc5409ea25b3173b.png similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/c6f2a6bdada8e8cbbc5409ea25b3173b.png rename to web-packages/demo/public/assets/find-flower/images/c6f2a6bdada8e8cbbc5409ea25b3173b.png diff --git a/web-packages/demo/html/assets/find-flower/images/ed39cf3a5793b16bde5c363c50546ed1.png b/web-packages/demo/public/assets/find-flower/images/ed39cf3a5793b16bde5c363c50546ed1.png similarity index 100% rename from web-packages/demo/html/assets/find-flower/images/ed39cf3a5793b16bde5c363c50546ed1.png rename to web-packages/demo/public/assets/find-flower/images/ed39cf3a5793b16bde5c363c50546ed1.png diff --git a/web-packages/demo/src/assets/cube-textures.ts b/web-packages/demo/src/assets/cube-textures.ts deleted file mode 100644 index 7bcb8d07a..000000000 --- a/web-packages/demo/src/assets/cube-textures.ts +++ /dev/null @@ -1,62 +0,0 @@ -export default { - 'compositionId': 1, - 'requires': [], - 'compositions': [{ - 'name': 'composition_1', - 'id': 1, - 'duration': 50, - 'camera': { 'fov': 30, 'far': 20, 'near': 0.1, 'position': [0, 0, 8], 'clipMode': 1 }, - 'items': [{ - 'name': 'item_1', - 'delay': 0, - 'id': 1, - 'type': '1', - 'ro': 0.1, - 'sprite': { - 'options': { - 'startLifetime': 2, - 'startSize': 1.2, - 'sizeAspect': 1.320754716981132, - 'startColor': ['color', [255, 255, 255]], - 'duration': 20, - 'gravityModifier': 1, - 'renderLevel': 'B+', - }, 'renderer': { 'renderMode': 1, 'anchor': [0.5, 0.5], 'texture': 0 }, - }, - }], - 'meta': { 'previewSize': [750, 1624] }, - }], - 'gltf': [], - images: [], - 'textures': [{ - 'minFilter': 9987, - 'magFilter': 9729, - 'wrapS': 33071, - 'wrapT': 33071, - 'target': 34067, - 'format': 6408, - 'internalFormat': 6408, - 'type': 5121, - 'mipmaps': [ - [[20, [5, 0, 24661]], [20, [5, 24664, 26074]], [20, [5, 50740, 26845]], [20, [5, 77588, 24422]], [20, [5, 102012, 24461]], [20, [5, 126476, 27099]]], - [[20, [5, 153576, 7699]], [20, [5, 161276, 7819]], [20, [5, 169096, 8919]], [20, [5, 178016, 7004]], [20, [5, 185020, 7657]], [20, [5, 192680, 8515]]], - [[20, [5, 201196, 2305]], [20, [5, 203504, 2388]], [20, [5, 205892, 2789]], [20, [5, 208684, 2147]], [20, [5, 210832, 2351]], [20, [5, 213184, 2541]]], - [[20, [5, 215728, 755]], [20, [5, 216484, 810]], [20, [5, 217296, 902]], [20, [5, 218200, 727]], [20, [5, 218928, 775]], [20, [5, 219704, 835]]], - [[20, [5, 220540, 292]], [20, [5, 220832, 301]], [20, [5, 221136, 317]], [20, [5, 221456, 285]], [20, [5, 221744, 301]], [20, [5, 222048, 307]]], - [[20, [5, 222356, 147]], [20, [5, 222504, 147]], [20, [5, 222652, 149]], [20, [5, 222804, 149]], [20, [5, 222956, 149]], [20, [5, 223108, 149]]], - [[20, [5, 223260, 96]], [20, [5, 223356, 96]], [20, [5, 223452, 96]], [20, [5, 223548, 97]], [20, [5, 223648, 97]], [20, [5, 223748, 97]]], - [[20, [5, 223848, 83]], [20, [5, 223932, 83]], [20, [5, 224016, 83]], [20, [5, 224100, 83]], [20, [5, 224184, 83]], [20, [5, 224268, 83]]]], - 'sourceType': 7, - }], - 'bins': [ - new ArrayBuffer(1), - new ArrayBuffer(1), - new ArrayBuffer(1), - new ArrayBuffer(1), - new ArrayBuffer(1), - { 'url': 'https://mdn.alipayobjects.com/mars/afts/file/A*pGF4QJqDT3wAAAAAAAAAAAAADlB4AQ' }], - 'version': '0.9.0', - 'shapes': [], - 'plugins': [], - 'type': 'mars', -}; diff --git a/web-packages/demo/src/assets/post-processing-list.ts b/web-packages/demo/src/assets/post-processing-list.ts new file mode 100644 index 000000000..0abc40d3a --- /dev/null +++ b/web-packages/demo/src/assets/post-processing-list.ts @@ -0,0 +1,19 @@ +export default { + spring: { + url: 'https://mdn.alipayobjects.com/mars/afts/file/A*oF1NRJG7GU4AAAAAAAAAAAAADlB4AQ', + name: '春促-主页互动', + }, + rotate: { + url: 'https://mdn.alipayobjects.com/mars/afts/file/A*AGu-So0y5YkAAAAAAAAAAAAADlB4AQ', + name: '旋转出场', + }, + robin: { + url: 'https://mdn.alipayobjects.com/mars/afts/file/A*YIKpS69QTaoAAAAAAAAAAAAADlB4AQ', + name: 'Robin', + }, + bloomTest: { + url: 'https://mdn.alipayobjects.com/mars/afts/file/A*y3eeR41N5dgAAAAAAAAAAAAADlB4AQ', + name: 'BloomTest', + }, +}; + diff --git a/web-packages/demo/src/common/inspire-list.ts b/web-packages/demo/src/common/inspire-list.ts index 68d8587f3..05269a1ab 100644 --- a/web-packages/demo/src/common/inspire-list.ts +++ b/web-packages/demo/src/common/inspire-list.ts @@ -1,12 +1,10 @@ -// @ts-nocheck - import inspireList from '../assets/inspire-list'; export class InspireList { currentInspire = inspireList['ribbons'].url; - private readonly startEle = document.getElementById('J-start'); - private readonly pauseEle = document.getElementById('J-pause'); + private readonly startEle = document.getElementById('J-start') as HTMLElement; + private readonly pauseEle = document.getElementById('J-pause') as HTMLElement; private readonly loopCheckboxEle = document.getElementById('J-loop'); private readonly frameworkEle = document.getElementById('J-framework') as HTMLSelectElement; @@ -52,7 +50,7 @@ export class InspireList { }); selectEle.innerHTML = options.join(''); selectEle.onchange = () => { - const selected = selectEle.value; + const selected = selectEle.value as keyof typeof inspireList; this.currentInspire = inspireList[selected].url; }; diff --git a/web-packages/demo/src/dashboard.ts b/web-packages/demo/src/dashboard.ts index 3d00fb5ba..4634008d5 100644 --- a/web-packages/demo/src/dashboard.ts +++ b/web-packages/demo/src/dashboard.ts @@ -1,6 +1,5 @@ import type { Composition } from '@galacean/effects'; import { Player } from '@galacean/effects'; -import cubeTextures from './assets/cube-textures'; const jsons = [ // 方块 @@ -25,9 +24,6 @@ const jsons = [ 'https://mdn.alipayobjects.com/mars/afts/file/A*dnU-SprU5pAAAAAAAAAAAAAADlB4AQ', ]; -// @ts-expect-error -jsons.push(cubeTextures); - (async () => { try { const container = createContainer(); diff --git a/web-packages/demo/src/dynamic-video.ts b/web-packages/demo/src/dynamic-video.ts index 6bad2a54f..ecc0a8691 100644 --- a/web-packages/demo/src/dynamic-video.ts +++ b/web-packages/demo/src/dynamic-video.ts @@ -8,10 +8,9 @@ const container = document.getElementById('J-container'); const player = new Player({ container, }); - const composition = await player.loadScene(json, { variables: { - video: 'https://gw.alipayobjects.com/v/huamei_p0cigc/afts/video/A*7gPzSo3RxlQAAAAAAAAAAAAADtN3AQ', + video: 'https://mdn.alipayobjects.com/huamei_p0cigc/afts/file/A*ZOgXRbmVlsIAAAAAAAAAAAAADoB5AQ', text_3: 'Dynamic Video', }, }); diff --git a/web-packages/demo/src/gui/inspector-gui.ts b/web-packages/demo/src/gui/inspector-gui.ts deleted file mode 100644 index 8868b6c9d..000000000 --- a/web-packages/demo/src/gui/inspector-gui.ts +++ /dev/null @@ -1,311 +0,0 @@ -import type { EffectComponentData, EffectsObject, Engine, Material, SceneData, VFXItem } from '@galacean/effects'; -import { EffectComponent, Behaviour, RendererComponent, SerializationHelper, Texture, generateGUID, glContext, loadImage, spec } from '@galacean/effects'; - -export class InspectorGui { - gui: any; - item: VFXItem; - itemDirtyFlag = false; - - sceneData: SceneData; - guiControllers: any[] = []; - - constructor () { - //@ts-expect-error - this.gui = new GUI(); - this.gui.addFolder('Inspector'); - - // setInterval(this.updateInspector, 500); - } - - setItem (item: VFXItem) { - if (this.item === item) { - return; - } - this.item = item; - this.itemDirtyFlag = true; - } - - update = () => { - if (this.item && this.itemDirtyFlag) { - this.guiControllers = []; - this.gui.destroy(); - //@ts-expect-error - this.gui = new GUI(); - this.gui.add(this.item, 'name'); - - const transformFolder = this.gui.addFolder('Transform'); - const positionFolder = transformFolder.addFolder('Position'); - const rotationFolder = transformFolder.addFolder('Rotation'); - const scaleFolder = transformFolder.addFolder('Scale'); - - transformFolder.open(); - positionFolder.open(); - rotationFolder.open(); - scaleFolder.open(); - - const transform = this.item.transform; - const transformData = transform.toData(); - - this.guiControllers.push(positionFolder.add(transformData.position, 'x').name('x').step(0.03).onChange(() => { transform.fromData(transformData); })); - this.guiControllers.push(positionFolder.add(transformData.position, 'y').name('y').step(0.03).onChange(() => { transform.fromData(transformData); })); - this.guiControllers.push(positionFolder.add(transformData.position, 'z').name('z').step(0.03).onChange(() => { transform.fromData(transformData); })); - - // @ts-expect-error - this.guiControllers.push(rotationFolder.add(transformData.rotation, 'x').name('x').step(0.03).onChange(() => { transform.fromData(transformData); })); - // @ts-expect-error - this.guiControllers.push(rotationFolder.add(transformData.rotation, 'y').name('y').step(0.03).onChange(() => { transform.fromData(transformData); })); - // @ts-expect-error - this.guiControllers.push(rotationFolder.add(transformData.rotation, 'z').name('z').step(0.03).onChange(() => { transform.fromData(transformData); })); - - this.guiControllers.push(scaleFolder.add(transformData.scale, 'x').name('x').step(0.03).onChange(() => { transform.fromData(transformData); })); - this.guiControllers.push(scaleFolder.add(transformData.scale, 'y').name('y').step(0.03).onChange(() => { transform.fromData(transformData); })); - this.guiControllers.push(scaleFolder.add(transformData.scale, 'z').name('z').step(0.03).onChange(() => { transform.fromData(transformData); })); - - for (const component of this.item.components) { - const componentFolder = this.gui.addFolder(component.constructor.name); - - if (component instanceof RendererComponent) { - const controller = componentFolder.add(component, '_enabled'); - - this.guiControllers.push(controller); - } - - if (component instanceof EffectComponent) { - componentFolder.add({ - click: async () => { - await selectJsonFile((data: spec.EffectsPackageData) => { - for (const effectsObjectData of data.exportObjects) { - this.item.engine.jsonSceneData[effectsObjectData.id] = effectsObjectData; - const effectComponent = this.item.getComponent(RendererComponent); - - if (effectComponent) { - const guid = effectComponent.getInstanceId(); - - (this.item.engine.jsonSceneData[guid] as EffectComponentData).materials[0] = { id: effectsObjectData.id }; - SerializationHelper.deserializeTaggedProperties(this.item.engine.jsonSceneData[guid], effectComponent); - } - } - this.itemDirtyFlag = true; - }); - }, - }, 'click').name('Material'); - - componentFolder.add({ - click: async () => { - await selectJsonFile((data: spec.EffectsPackageData) => { - for (const effectsObjectData of data.exportObjects) { - this.item.engine.jsonSceneData[effectsObjectData.id] = effectsObjectData; - const effectComponent = this.item.getComponent(EffectComponent); - - if (effectComponent) { - const guid = effectComponent.getInstanceId(); - - (this.item.engine.jsonSceneData[guid] as EffectComponentData).geometry = { id: effectsObjectData.id }; - SerializationHelper.deserializeTaggedProperties(this.item.engine.jsonSceneData[guid], effectComponent); - } - } - }); - }, - }, 'click').name('Geometry'); - } - - if (component instanceof Behaviour) { - const controller = componentFolder.add(component, '_enabled'); - - this.guiControllers.push(controller); - } - - componentFolder.open(); - } - const rendererComponent = this.item.getComponent(RendererComponent); - - if (rendererComponent) { - for (const material of rendererComponent.materials) { - this.setMaterialGui(material); - } - } - - this.itemDirtyFlag = false; - } - - if (this.item) { - const rendererComponent = this.item.getComponent(RendererComponent); - - if (rendererComponent) { - for (const material of rendererComponent.materials) { - // material.toData(); - } - } - } - - for (const controller of this.guiControllers) { - controller.updateDisplay(); - } - }; - - // const properties = ` - // _2D("2D", 2D) = "" {} - // _Color("Color",Color) = (1,1,1,1) - // _Value("Value",Range(0,10)) = 2.5 - // _Float("Float",Float) = 0 - // _Vector("Vector",Vector) = (0,0,0,0) - // _Rect("Rect",Rect) = "" {} - // _Cube("Cube",Cube) = "" {} - // `; - - private parseMaterialProperties (material: Material, gui: any, serializeObject: SerializedObject) { - const serializedData = serializeObject.serializedData; - const shaderProperties = (material.shaderSource as spec.ShaderData).properties; - - if (!shaderProperties) { - return; - } - const lines = shaderProperties.split('\n'); - - for (const property of lines) { - // 提取材质属性信息 - // 如 “_Float1("Float2", Float) = 0” - // 提取出 “_Float1” “Float2” “Float” “0” - const regex = /\s*(.+?)\s*\(\s*"(.+?)"\s*,\s*(.+?)\s*\)\s*=\s*(.+)\s*/; - const matchResults = property.match(regex); - - if (!matchResults) { - return; - } - const uniformName = matchResults[1]; - const inspectorName = matchResults[2]; - const type = matchResults[3]; - const value = matchResults[4]; - - // 提取 Range(a, b) 的 a 和 b - const match = type.match(/\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/); - - if (match) { - const start = Number(match[1]); - const end = Number(match[2]); - - // materialData.floats[uniformName] = Number(value); - this.guiControllers.push(gui.add(serializedData.floats, uniformName, start, end).onChange(() => { - // this.item.getComponent(RendererComponent)?.material.fromData(materialData); - serializeObject.applyModifiedProperties(); - })); - } else if (type === 'Float') { - // materialData.floats[uniformName] = Number(value); - this.guiControllers.push(gui.add(serializedData.floats, uniformName).name(inspectorName).onChange(() => { - serializeObject.applyModifiedProperties(); - })); - } else if (type === 'Color') { - this.guiControllers.push(gui.addColor({ color: [0, 0, 0, 0] }, 'color').name(inspectorName).onChange((value: number[]) => { - serializeObject.serializedData['vector4s'][uniformName] = { x: value[0], y: value[1], z: value[2], w: value[3] }; - serializeObject.applyModifiedProperties(); - })); - } else if (type === '2D') { - const controller = this.gui.add({ - click: async () => { - const fileHandle: FileSystemFileHandle[] = await window.showOpenFilePicker(); - const file = await fileHandle[0].getFile(); - const assetUuid = generateGUID(); - - // 生成纹理资产对象 - const reader = new FileReader(); - - reader.onload = async function (e) { - const result = e.target?.result; - const textureData = { id: assetUuid, source: result, dataType: spec.DataType.Texture, flipY: true, wrapS: glContext.REPEAT, wrapT: glContext.REPEAT }; - - serializeObject.engine.jsonSceneData[textureData.id] = textureData; - }; - reader.onerror = event => { - console.error('文件读取出错:', reader.error); - }; - - reader.readAsDataURL(file); - - // 加载 image - const image = await loadImage(file); - - image.width = 50; - image.height = 50; - image.id = inspectorName; - const lastImage = document.getElementById(inspectorName); - - if (lastImage) { - controller.domElement.removeChild(lastImage); - } - controller.domElement.appendChild(image); - - // 根据 image 生成纹理对象 - const texture = Texture.create(this.item.engine, { image: image, flipY: true, wrapS: glContext.REPEAT, wrapT: glContext.REPEAT }); - - texture.setInstanceId(assetUuid); - serializeObject.engine.addInstance(texture); - serializeObject.serializedData.textures[uniformName] = { id: texture.getInstanceId() }; - serializeObject.applyModifiedProperties(); - }, - }, 'click').name(inspectorName); - } - } - } - - // dat gui 参数及修改 - private setMaterialGui (material: Material) { - const materialGUI = this.gui.addFolder('Material'); - - materialGUI.open(); - const serializeObject = new SerializedObject(material); - const serializedData = serializeObject.serializedData; - - serializedData.blending = false; - serializedData.zTest = false; - serializedData.zWrite = false; - serializeObject.update(); - - this.guiControllers.push(materialGUI.add(serializedData, 'blending').onChange(() => { - serializeObject.applyModifiedProperties(); - })); - this.guiControllers.push(materialGUI.add(serializedData, 'zTest').onChange(() => { - serializeObject.applyModifiedProperties(); - })); - this.guiControllers.push(materialGUI.add(serializedData, 'zWrite').onChange(() => { - serializeObject.applyModifiedProperties(); - })); - this.parseMaterialProperties(material, materialGUI, serializeObject); - } -} - -async function selectJsonFile (callback: (data: any) => void) { - const fileHandle: FileSystemFileHandle[] = await window.showOpenFilePicker(); - const file = await fileHandle[0].getFile(); - const reader = new FileReader(); - - reader.onload = () => { - if (typeof reader.result !== 'string') { - return; - } - const data = JSON.parse(reader.result); - - callback(data); - }; - reader.readAsText(file); -} - -export class SerializedObject { - engine: Engine; - serializedData: Record; - target: EffectsObject; - - constructor (target: EffectsObject) { - this.target = target; - this.engine = target.engine; - this.serializedData = {}; - this.update(); - } - - update () { - SerializationHelper.serializeTaggedProperties(this.target, this.serializedData); - } - - applyModifiedProperties () { - SerializationHelper.deserializeTaggedProperties(this.serializedData as spec.EffectsObjectData, this.target); - } -} diff --git a/web-packages/demo/src/interactive.ts b/web-packages/demo/src/interactive.ts index 6f840f620..4f3753144 100644 --- a/web-packages/demo/src/interactive.ts +++ b/web-packages/demo/src/interactive.ts @@ -19,6 +19,9 @@ const container = document.getElementById('J-container'); player.on('update', e => { document.getElementById('J-playerState')!.innerText = `[player update] - player is ${e.playing ? 'playing' : 'paused'}`; }); + player.on('pause', () => { + console.info('[player pause] - player is paused.'); + }); document.getElementById('J-pauseBtn')?.addEventListener('click', () => { player.pause(); diff --git a/web-packages/demo/src/local-file.ts b/web-packages/demo/src/local-file.ts index d90a67f8c..3d3b30445 100644 --- a/web-packages/demo/src/local-file.ts +++ b/web-packages/demo/src/local-file.ts @@ -6,7 +6,7 @@ const container = document.getElementById('J-container'); (async () => { try { const player = new Player({ container }); - const composition = await player.loadScene('./assets/find-flower/flower.json'); + const composition = await player.loadScene('/assets/find-flower/flower.json'); setTimeout(() => { composition.setSpeed(-1); diff --git a/web-packages/demo/src/post-processing.ts b/web-packages/demo/src/post-processing.ts index fde2953e6..7119b181d 100644 --- a/web-packages/demo/src/post-processing.ts +++ b/web-packages/demo/src/post-processing.ts @@ -1,84 +1,73 @@ -//@ts-nocheck import type { Composition } from '@galacean/effects'; -import { POST_PROCESS_SETTINGS, Player, PostProcessVolume, defaultGlobalVolume, setConfig } from '@galacean/effects'; -import { InspireList } from './common/inspire-list'; -import { InspectorGui } from './gui/inspector-gui'; - -const url = 'https://mdn.alipayobjects.com/mars/afts/file/A*YIKpS69QTaoAAAAAAAAAAAAADlB4AQ'; -//const url = 'https://mdn.alipayobjects.com/mars/afts/file/A*6j_ZQan_MhMAAAAAAAAAAAAADlB4AQ'; // BloomTest -const container = document.getElementById('J-container'); -const speed = 0.5; -const inspireList = new InspireList(); - -const inspectorGui = new InspectorGui(); - -setInterval(()=>{ - inspectorGui.update(); -}, 100); - -let gui = new GUI(); -let player; +import { POST_PROCESS_SETTINGS, Player, PostProcessVolume, setConfig } from '@galacean/effects'; +import postProcessingList from './assets/post-processing-list'; // DATUI 参数面板 const postProcessSettings = { // Particle color: [0, 0, 0], intensity: 1.0, - // Bloom - useBloom: 1.0, - threshold: 1.0, - bloomIntensity: 1.0, - // ColorAdjustments - brightness: 0, - saturation: 0, - contrast: 0, - // ToneMapping - useToneMapping: 1, // 1: true, 0: false }; +const container = document.getElementById('J-container'); +const resumeBtn = document.getElementById('J-resume'); +const url = postProcessingList['bloomTest'].url; +let player: Player; +let gui: any; + +initSelectList(); +setConfig(POST_PROCESS_SETTINGS, postProcessSettings); (async () => { - setConfig(POST_PROCESS_SETTINGS, postProcessSettings); - player = new Player({ - container, - pixelRatio: window.devicePixelRatio, - }); - await handlePlay(url); + try { + player = new Player({ + container, + }); + + await handleLoadScene(url); + } catch (e) { + console.error('biz', e); + } })(); -bindEventListeners(); +resumeBtn?.addEventListener('click', () => handleLoadScene(url)); -function bindEventListeners () { - inspireList.handleStart = () => { - handlePause(); - void handlePlay(inspireList.currentInspire); - }; - inspireList.handlePause = handlePause; -} +async function handleLoadScene (url: string) { + const json = await (await fetch(url)).json(); -async function handlePlay (url) { - try { - const json = await (await fetch(url)).json(); + json.renderSettings = { + postProcessingEnabled: true, + }; + player.destroyCurrentCompositions(); - player.destroyCurrentCompositions(); - const comp: Composition = await player.loadScene(json); + const composition = await player.loadScene(json); - comp.rootItem.addComponent(PostProcessVolume); - void player.play(comp, { speed }); - setDatGUI(comp); + composition.rootItem.addComponent(PostProcessVolume); - } catch (e) { - console.error('biz', e); - } + setDatGUI(composition); } -function handlePause () { - player.pause(); +function initSelectList () { + const selectEle = document.getElementById('J-select') as HTMLSelectElement; + const options: string[] = []; + + Object.entries(postProcessingList).map(([key, object]) => { + options.push(``); + }); + selectEle.innerHTML = options.join(''); + selectEle.onchange = () => { + const name = selectEle.value as keyof typeof postProcessingList; + + void handleLoadScene(postProcessingList[name].url); + }; } // dat gui 参数及修改 function setDatGUI (composition: Composition) { - gui.destroy(); - gui = new GUI(); + if (gui) { + gui.destroy(); + } + // @ts-expect-error + gui = new window.GUI(); const ParticleFolder = gui.addFolder('Particle'); const BloomFolder = gui.addFolder('Bloom'); const ToneMappingFlolder = gui.addFolder('ToneMapping'); @@ -87,24 +76,28 @@ function setDatGUI (composition: Composition) { const globalVolume = composition.renderFrame.globalVolume; + if (!globalVolume) { + return; + } + ParticleFolder.addColor(postProcessSettings, 'color'); ParticleFolder.add(postProcessSettings, 'intensity', -10, 10).step(0.1); ParticleFolder.open(); - BloomFolder.add(globalVolume, 'useBloom', 0, 1).step(1); - BloomFolder.add(globalVolume, 'threshold', 0, 40).step(0.1); - BloomFolder.add(globalVolume, 'bloomIntensity', 0, 10); + BloomFolder.add(globalVolume.bloom, 'active', 0, 1).step(1); + BloomFolder.add(globalVolume.bloom, 'threshold', 0, 40).step(0.1); + BloomFolder.add(globalVolume.bloom, 'intensity', 0, 10); BloomFolder.open(); - VignetteFolder.add(globalVolume, 'vignetteIntensity', 0, 2); - VignetteFolder.add(globalVolume, 'vignetteSmoothness', 0, 2); - VignetteFolder.add(globalVolume, 'vignetteRoundness', 0, 1.5); + VignetteFolder.add(globalVolume.vignette, 'intensity', 0, 2); + VignetteFolder.add(globalVolume.vignette, 'smoothness', 0, 2); + VignetteFolder.add(globalVolume.vignette, 'roundness', 0, 1.5); - ColorAdjustmentsFolder.add(globalVolume, 'brightness', -5, 5).step(0.1); - ColorAdjustmentsFolder.add(globalVolume, 'saturation', 0, 2); - ColorAdjustmentsFolder.add(globalVolume, 'contrast', 0, 2); + ColorAdjustmentsFolder.add(globalVolume.colorAdjustments, 'brightness').step(0.1); + ColorAdjustmentsFolder.add(globalVolume.colorAdjustments, 'saturation', -100, 100); + ColorAdjustmentsFolder.add(globalVolume.colorAdjustments, 'contrast', -100, 100); ColorAdjustmentsFolder.open(); - ToneMappingFlolder.add(globalVolume, 'useToneMapping', 0, 1).step(1); + ToneMappingFlolder.add(globalVolume.tonemapping, 'active', 0, 1).step(1); ToneMappingFlolder.open(); -} \ No newline at end of file +} diff --git a/web-packages/demo/src/shader-compile.ts b/web-packages/demo/src/shader-compile.ts new file mode 100644 index 000000000..0cafb59d8 --- /dev/null +++ b/web-packages/demo/src/shader-compile.ts @@ -0,0 +1,52 @@ +import type { Composition } from '@galacean/effects'; +import { Player } from '@galacean/effects'; +import '@galacean/effects-plugin-spine'; + +// 大量粒子 +// const json = 'https://mdn.alipayobjects.com/mars/afts/file/A*aCeuQ5RQZj4AAAAAAAAAAAAADlB4AQ'; +// 新年烟花 +const json = [ + 'https://gw.alipayobjects.com/os/gltf-asset/mars-cli/ILDKKFUFMVJA/1705406034-80896.json', + 'https://mdn.alipayobjects.com/graph_jupiter/afts/file/A*qTquTKYbk6EAAAAAAAAAAAAADsF2AQ', +]; +// 混合测试 +// const json = [ +// 'https://mdn.alipayobjects.com/mars/afts/file/A*QyX8Rp-4fmUAAAAAAAAAAAAADlB4AQ', +// 'https://mdn.alipayobjects.com/mars/afts/file/A*bi3HRobVsk8AAAAAAAAAAAAADlB4AQ', +// 'https://mdn.alipayobjects.com/graph_jupiter/afts/file/A*sEdkT5cdXGEAAAAAAAAAAAAADsF2AQ', +// ]; +// 塔奇 +// const json = 'https://mdn.alipayobjects.com/mars/afts/file/A*uU2JRIjcLIcAAAAAAAAAAAAADlB4AQ'; +// const json = 'https://gw.alipayobjects.com/os/gltf-asset/mars-cli/TAJIINQOUUKP/-799304223-0ee5d.json'; +const container = document.getElementById('J-container'); + +document.getElementById('J-button')!.addEventListener('click', () => { + (async () => { + try { + container?.classList.add('active'); + + const player = new Player({ + container, + // renderFramework: 'webgl2', + }); + const compositions = await player.loadScene(Array.isArray(json) ? json : [json]) as unknown as Composition[]; + + compositions.forEach(composition => { + const dt = document.createElement('dt'); + + dt.innerHTML = `>>> composition: ${composition.name}`; + document.getElementById('J-statistic')?.appendChild(dt); + + for (const key in composition.statistic) { + const p = document.createElement('dd'); + + // @ts-expect-error + p.innerHTML = `${key}: ${composition.statistic[key]}`; + document.getElementById('J-statistic')?.appendChild(p); + } + }); + } catch (e) { + console.error('biz', e); + } + })(); +}); diff --git a/web-packages/demo/src/shape.ts b/web-packages/demo/src/shape.ts new file mode 100644 index 000000000..cedc08fc2 --- /dev/null +++ b/web-packages/demo/src/shape.ts @@ -0,0 +1,216 @@ +import { Player, ShapeComponent } from '@galacean/effects'; + +const json = { + 'playerVersion': { + 'web': '2.0.4', + 'native': '0.0.1.202311221223', + }, + 'images': [], + 'fonts': [], + 'version': '3.0', + 'shapes': [], + 'plugins': [], + 'type': 'ge', + 'compositions': [ + { + 'id': '1', + 'name': '新建合成1', + 'duration': 6, + 'startTime': 0, + 'endBehavior': 4, + 'previewSize': [ + 750, + 1624, + ], + 'items': [ + { + 'id': '21135ac68dfc49bcb2bc7552cbb9ad07', + }, + ], + 'camera': { + 'fov': 60, + 'far': 40, + 'near': 0.1, + 'clipMode': 1, + 'position': [ + 0, + 0, + 8, + ], + 'rotation': [ + 0, + 0, + 0, + ], + }, + 'sceneBindings': [ + ], + 'timelineAsset': { + 'id': 'dd50ad0de3f044a5819576175acf05f7', + }, + }, + ], + 'components': [ + { + 'id': 'b7890caa354a4c279ff9678c5530cd83', + 'item': { + 'id': '21135ac68dfc49bcb2bc7552cbb9ad07', + }, + 'dataType': 'ShapeComponent', + 'type': 0, + 'points': [ + { + 'x': -1, + 'y': -1, + 'z': 0, + }, + { + 'x': 1, + 'y': -1, + 'z': 0, + }, + { + 'x': 0, + 'y': 1, + 'z': 0, + }, + ], + 'easingIns': [ + { + 'x': -1, + 'y': -0.5, + 'z': 0, + }, + { + 'x': 0.5, + 'y': -1.5, + 'z': 0, + }, + { + 'x': 0.5, + 'y': 1, + 'z': 0, + }, + ], + 'easingOuts': [ + { + 'x': -0.5, + 'y': -1.5, + 'z': 0, + }, + { + 'x': 1, + 'y': -0.5, + 'z': 0, + }, + { + 'x': -0.5, + 'y': 1, + 'z': 0, + }, + ], + 'shapes': [ + { + 'verticalToPlane': 'z', + 'indexes': [ + { + 'point': 0, + 'easingIn': 0, + 'easingOut': 0, + }, + { + 'point': 1, + 'easingIn': 1, + 'easingOut': 1, + }, + { + 'point': 2, + 'easingIn': 2, + 'easingOut': 2, + }, + ], + 'close': true, + 'fill': { + 'color': { 'r':1, 'g':0.7, 'b':0.5, 'a':1 }, + }, + }, + ], + 'renderer': { + 'renderMode': 1, + }, + }, + ], + 'geometries': [], + 'materials': [], + 'items': [ + { + 'id': '21135ac68dfc49bcb2bc7552cbb9ad07', + 'name': 'Shape', + 'duration': 5, + 'type': '1', + 'visible': true, + 'endBehavior': 0, + 'delay': 0, + 'renderLevel': 'B+', + 'components': [ + { + 'id': 'b7890caa354a4c279ff9678c5530cd83', + }, + ], + 'transform': { + 'position': { + 'x': 0, + 'y': 0, + 'z': 0, + }, + 'eulerHint': { + 'x': 0, + 'y': 0, + 'z': 0, + }, + 'anchor': { + 'x': 0, + 'y': 0, + }, + 'size': { + 'x': 1.2, + 'y': 1.2, + }, + 'scale': { + 'x': 1, + 'y': 1, + 'z': 1, + }, + }, + 'dataType': 'VFXItemData', + }, + ], + 'shaders': [], + 'bins': [], + 'textures': [], + 'animations': [], + 'miscs': [ + { + 'id': 'dd50ad0de3f044a5819576175acf05f7', + 'dataType': 'TimelineAsset', + 'tracks': [ + ], + }, + ], + 'compositionId': '1', +}; +const container = document.getElementById('J-container'); + +(async () => { + try { + const player = new Player({ + container, + }); + + const composition = await player.loadScene(json); + const item = composition.getItemByName('Shape'); + const shapeComponent = item?.getComponent(ShapeComponent); + } catch (e) { + console.error('biz', e); + } +})(); diff --git a/web-packages/demo/src/single.ts b/web-packages/demo/src/single.ts index a17707430..6978974fb 100644 --- a/web-packages/demo/src/single.ts +++ b/web-packages/demo/src/single.ts @@ -1,16 +1,35 @@ -import { Player } from '@galacean/effects'; +import { AssetManager, Player } from '@galacean/effects'; import '@galacean/effects-plugin-spine'; +import { JSONConverter } from '@galacean/effects-plugin-model'; -const json = 'https://mdn.alipayobjects.com/mars/afts/file/A*CquWQrVCGyUAAAAAAAAAAAAADlB4AQ'; +// const json = 'https://gw.alipayobjects.com/os/gltf-asset/mars-cli/YDITHDADWXXM/1601633123-e644d.json'; +// 蒙版 +// const json = 'https://gw.alipayobjects.com/os/gltf-asset/mars-cli/HCQBCOWGHRQC/273965510-c5c29.json'; +// 蒙版新数据 +// const json = 'https://mdn.alipayobjects.com/mars/afts/file/A*36ybTZJI4JEAAAAAAAAAAAAADlB4AQ'; +// 普通拖尾 +// const json = 'https://gw.alipayobjects.com/os/gltf-asset/mars-cli/RYYAXEAYMIYJ/1314733612-96c0b.json'; +// 图贴拖尾 +// const json = 'https://mdn.alipayobjects.com/mars/afts/file/A*VRedS5UU8DAAAAAAAAAAAAAADlB4AQ'; +// 3D +// const json = 'https://mdn.alipayobjects.com/mars/afts/file/A*sA-6TJ695dYAAAAAAAAAAAAADlB4AQ'; +// 特效元素 +const json = 'https://mdn.alipayobjects.com/mars/afts/file/A*GmmoRYoutZ4AAAAAAAAAAAAADlB4AQ'; const container = document.getElementById('J-container'); (async () => { try { + const assetManager = new AssetManager(); const player = new Player({ container, + interactive: true, }); + // const converter = new JSONConverter(player.renderer, true); + // const data = await converter.processScene(json); + // const scene = await assetManager.loadScene(json); await player.loadScene(json); + } catch (e) { console.error('biz', e); } diff --git a/web-packages/demo/vite.config.js b/web-packages/demo/vite.config.js index da45816a0..e77a35902 100644 --- a/web-packages/demo/vite.config.js +++ b/web-packages/demo/vite.config.js @@ -23,9 +23,12 @@ export default defineConfig(({ mode }) => { 'dashboard': resolve(__dirname, 'html/dashboard.html'), 'dynamic-image': resolve(__dirname, 'html/dynamic-image.html'), 'dynamic-video': resolve(__dirname, 'html/dynamic-video.html'), - 'render-level': resolve(__dirname, 'html/render-level.html'), + 'interactive': resolve(__dirname, 'html/interactive.html'), 'local-file': resolve(__dirname, 'html/local-file.html'), 'post-processing': resolve(__dirname, 'html/post-processing.html'), + 'render-level': resolve(__dirname, 'html/render-level.html'), + 'shader-compile': resolve(__dirname, 'html/shader-compile.html'), + 'shape': resolve(__dirname, 'html/shape.html'), 'single': resolve(__dirname, 'html/single.html'), 'text': resolve(__dirname, 'html/text.html'), 'three-particle': resolve(__dirname, 'html/three-particle.html'), diff --git a/web-packages/imgui-demo/src/core/asset-data-base.ts b/web-packages/imgui-demo/src/core/asset-data-base.ts index f9b57dd05..ead18f47e 100644 --- a/web-packages/imgui-demo/src/core/asset-data-base.ts +++ b/web-packages/imgui-demo/src/core/asset-data-base.ts @@ -361,7 +361,7 @@ export class EffectsPackage { }; for (const obj of this.exportObjects) { - effectsPackageData.exportObjects.push(SerializationHelper.serializeTaggedProperties(obj) as spec.EffectsObjectData); + effectsPackageData.exportObjects.push(SerializationHelper.serialize(obj) as spec.EffectsObjectData); } return effectsPackageData; diff --git a/web-packages/imgui-demo/src/core/ui-manager.ts b/web-packages/imgui-demo/src/core/ui-manager.ts index 854d5197a..88d384bd4 100644 --- a/web-packages/imgui-demo/src/core/ui-manager.ts +++ b/web-packages/imgui-demo/src/core/ui-manager.ts @@ -18,6 +18,8 @@ export class UIManager { // Top menu nodes private menuNodes: MenuNode[] = []; + private showCanvas = false; + constructor () { } @@ -67,7 +69,16 @@ export class UIManager { for (const panel of UIManager.editorWindows) { panel.draw(); } - // this.editor.draw(); + + const geContainer = document.getElementById('J-container'); + + if (geContainer) { + if (this.showCanvas) { + geContainer.style.zIndex = '999'; + } else { + geContainer.style.zIndex = '0'; + } + } if (ImGui.BeginMainMenuBar()) { if (ImGui.BeginMenu('File')) { @@ -75,7 +86,7 @@ export class UIManager { ImGui.EndMenu(); } if (ImGui.BeginMenu('Edit')) { - if (ImGui.BeginMenu('Test')) { + if (ImGui.MenuItem('Show Canvas', '', (_ = this.showCanvas)=>this.showCanvas = _)) { // ShowExampleMenuFile(); ImGui.EndMenu(); } diff --git a/web-packages/imgui-demo/src/ge.ts b/web-packages/imgui-demo/src/ge.ts index a4666b688..d7a81646b 100644 --- a/web-packages/imgui-demo/src/ge.ts +++ b/web-packages/imgui-demo/src/ge.ts @@ -1,5 +1,5 @@ import type { MaterialProps, Renderer } from '@galacean/effects'; -import { GLSLVersion, Geometry, Material, OrderType, Player, PostProcessVolume, RenderPass, RenderPassPriorityPostprocess, VFXItem, glContext, math } from '@galacean/effects'; +import { GLSLVersion, Geometry, Material, OrderType, Player, PostProcessVolume, RenderPass, RenderPassPriorityPostprocess, RendererComponent, VFXItem, glContext, math } from '@galacean/effects'; import '@galacean/effects-plugin-model'; import { JSONConverter } from '@galacean/effects-plugin-model'; import '@galacean/effects-plugin-orientation-transformer'; @@ -79,7 +79,7 @@ export class GalaceanEffects { 'item': { 'id': '3f40a594b3f34d10b963ad4fc736e505', }, - 'dataType': 'EffectComponent', + 'dataType': 'ShapeComponent', 'geometry': { 'id': '78cc7d2350bb417bb5dc93afab243411', }, @@ -130,7 +130,7 @@ export class GalaceanEffects { 'floats': { '_Speed': 1, }, - 'stringTags':{}, + 'stringTags': {}, 'vector4s': {}, 'textures': { '_MainTex': { @@ -235,7 +235,265 @@ export class GalaceanEffects { GalaceanEffects.assetDataBase = new AssetDatabase(GalaceanEffects.player.renderer.engine); GalaceanEffects.player.renderer.engine.database = GalaceanEffects.assetDataBase; //@ts-expect-error - GalaceanEffects.playURL(json); + GalaceanEffects.playURL({ + 'playerVersion': { + 'web': '2.0.4', + 'native': '0.0.1.202311221223', + }, + 'images': [], + 'fonts': [], + 'version': '3.0', + 'shapes': [], + 'plugins': [], + 'type': 'ge', + 'compositions': [ + { + 'id': '1', + 'name': '新建合成1', + 'duration': 6, + 'startTime': 0, + 'endBehavior': 4, + 'previewSize': [ + 750, + 1624, + ], + 'items': [ + { + 'id': '21135ac68dfc49bcb2bc7552cbb9ad07', + }, + ], + 'camera': { + 'fov': 60, + 'far': 40, + 'near': 0.1, + 'clipMode': 1, + 'position': [ + 0, + 0, + 8, + ], + 'rotation': [ + 0, + 0, + 0, + ], + }, + 'sceneBindings': [ + { + 'key': { + 'id': 'f8a6089ed7794f479907ed0bcac17220', + }, + 'value': { + 'id': '21135ac68dfc49bcb2bc7552cbb9ad07', + }, + }, + ], + 'timelineAsset': { + 'id': 'dd50ad0de3f044a5819576175acf05f7', + }, + }, + ], + 'components': [ + { + 'id': 'b7890caa354a4c279ff9678c5530cd83', + 'item': { + 'id': '21135ac68dfc49bcb2bc7552cbb9ad07', + }, + 'dataType': 'ShapeComponent', + 'type': 0, + 'points': [ + { + 'x': -1, + 'y': -1, + 'z': 0, + }, + { + 'x': 1, + 'y': -1, + 'z': 0, + }, + { + 'x': 0, + 'y': 1, + 'z': 0, + }, + ], + 'easingIns': [ + { + 'x': -1, + 'y': -0.5, + 'z': 0, + }, + { + 'x': 0.5, + 'y': -1.5, + 'z': 0, + }, + { + 'x': 0.5, + 'y': 1, + 'z': 0, + }, + ], + 'easingOuts': [ + { + 'x': -0.5, + 'y': -1.5, + 'z': 0, + }, + { + 'x': 1, + 'y': -0.5, + 'z': 0, + }, + { + 'x': -0.5, + 'y': 1, + 'z': 0, + }, + ], + 'shapes': [ + { + 'verticalToPlane': 'z', + 'indexes': [ + { + 'point': 0, + 'easingIn': 0, + 'easingOut': 0, + }, + { + 'point': 1, + 'easingIn': 1, + 'easingOut': 1, + }, + { + 'point': 2, + 'easingIn': 2, + 'easingOut': 2, + }, + ], + 'close': true, + 'fill': { + 'color': { 'r': 1, 'g': 0.7, 'b': 0.5, 'a': 1 }, + }, + }, + ], + 'renderer': { + 'renderMode': 1, + }, + }, + ], + 'geometries': [], + 'materials': [], + 'items': [ + { + 'id': '21135ac68dfc49bcb2bc7552cbb9ad07', + 'name': 'sprite_1', + 'duration': 5, + 'type': '1', + 'visible': true, + 'endBehavior': 0, + 'delay': 0, + 'renderLevel': 'B+', + 'components': [ + { + 'id': 'b7890caa354a4c279ff9678c5530cd83', + }, + ], + 'transform': { + 'position': { + 'x': 0, + 'y': 0, + 'z': 0, + }, + 'eulerHint': { + 'x': 0, + 'y': 0, + 'z': 0, + }, + 'anchor': { + 'x': 0, + 'y': 0, + }, + 'size': { + 'x': 1.2, + 'y': 1.2, + }, + 'scale': { + 'x': 1, + 'y': 1, + 'z': 1, + }, + }, + 'dataType': 'VFXItemData', + }, + ], + 'shaders': [], + 'bins': [], + 'textures': [], + 'animations': [], + 'miscs': [ + { + 'id': 'dd50ad0de3f044a5819576175acf05f7', + 'dataType': 'TimelineAsset', + 'tracks': [ + ], + }, + { + 'id': '51ed062462544526998daf514c320854', + 'dataType': 'ActivationPlayableAsset', + }, + { + 'id': '9fd3412cf92c4dc19a2deabb942dadb2', + 'dataType': 'TransformPlayableAsset', + 'positionOverLifetime': {}, + }, + { + 'id': 'a59a15a3f3b4414abb81217733561926', + 'dataType': 'ActivationTrack', + 'children': [], + 'clips': [ + { + 'start': 0, + 'duration': 5, + 'endBehavior': 0, + 'asset': { + 'id': '51ed062462544526998daf514c320854', + }, + }, + ], + }, + { + 'id': '9436a285d586414ab72622f64b11f54d', + 'dataType': 'TransformTrack', + 'children': [], + 'clips': [ + { + 'start': 0, + 'duration': 5, + 'endBehavior': 0, + 'asset': { + 'id': '9fd3412cf92c4dc19a2deabb942dadb2', + }, + }, + ], + }, + { + 'id': 'f8a6089ed7794f479907ed0bcac17220', + 'dataType': 'ObjectBindingTrack', + 'children': [ + { + 'id': 'a59a15a3f3b4414abb81217733561926', + }, + { + 'id': '9436a285d586414ab72622f64b11f54d', + }, + ], + 'clips': [], + }, + ], + 'compositionId': '1', + }); } static playURL (url: string, use3DConverter = false) { @@ -243,8 +501,8 @@ export class GalaceanEffects { if (use3DConverter) { const converter = new JSONConverter(GalaceanEffects.player.renderer); - void converter.processScene(url).then(async (scene: any) =>{ - const composition = await GalaceanEffects.player.loadScene(scene, { autoplay:true }); + void converter.processScene(url).then(async (scene: any) => { + const composition = await GalaceanEffects.player.loadScene(scene, { autoplay: true }); composition.renderFrame.addRenderPass(new OutlinePass(composition.renderer, { name: 'OutlinePass', @@ -253,8 +511,11 @@ export class GalaceanEffects { }),); }); } else { - void GalaceanEffects.player.loadScene(url, { autoplay:true }).then(composition=>{ + void GalaceanEffects.player.loadScene(url, { autoplay: true }).then(composition => { + // composition.postProcessingEnabled = true; + // composition.createRenderFrame(); composition.rootItem.addComponent(PostProcessVolume); + composition.renderFrame.addRenderPass(new OutlinePass(composition.renderer, { name: 'OutlinePass', priority: RenderPassPriorityPostprocess, @@ -340,16 +601,16 @@ export class OutlinePass extends RenderPass { ]), }, }, - mode:glContext.LINE_LOOP, - drawCount:6, + mode: glContext.LINE_LOOP, + drawCount: 6, }); } if (!this.material) { const materialProps: MaterialProps = { shader: { - vertex:this.vert, - fragment:this.frag, + vertex: this.vert, + fragment: this.frag, glslVersion: GLSLVersion.GLSL1, }, }; diff --git a/web-packages/imgui-demo/src/main.ts b/web-packages/imgui-demo/src/main.ts index 4eef8671d..d5ab33609 100644 --- a/web-packages/imgui-demo/src/main.ts +++ b/web-packages/imgui-demo/src/main.ts @@ -85,10 +85,11 @@ async function _init (): Promise { io.ConfigDockingAlwaysTabBar = true; // Setup Dear ImGui style - ImGui.StyleColorsDark(); - //ImGui.StyleColorsClassic(); + // ImGui.StyleColorsDark(); + // ImGui.StyleColorsClassic(); // embraceTheDarkness(); - SoDark(0.548); + // SoDark(0.548); + styleBlack(); // Load Fonts // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them. @@ -171,7 +172,7 @@ function _loop (time: number): void { // 2. Show a simple window that we create ourselves. We use a Begin/End pair to created a named window. { - // static float f = 0.0f; + // static float f = 0.0; // static int counter = 0; ImGui.Begin('Hello, world!'); // Create a window called "Hello, world!" and append into it. @@ -757,6 +758,73 @@ function SoDark (hue: number) { return style; } +function styleBlack () { + const style = ImGui.GetStyle(); + const colors = style.Colors; + + colors[ImGui.ImGuiCol.Text] = new ImGui.ImVec4(1.000, 1.000, 1.000, 1.000); + colors[ImGui.ImGuiCol.TextDisabled] = new ImGui.ImVec4(0.500, 0.500, 0.500, 1.000); + colors[ImGui.ImGuiCol.WindowBg] = new ImGui.ImVec4(0.180, 0.180, 0.180, 1.000); + colors[ImGui.ImGuiCol.ChildBg] = new ImGui.ImVec4(0.280, 0.280, 0.280, 0.000); + colors[ImGui.ImGuiCol.PopupBg] = new ImGui.ImVec4(0.313, 0.313, 0.313, 1.000); + colors[ImGui.ImGuiCol.Border] = new ImGui.ImVec4(0.266, 0.266, 0.266, 1.000); + colors[ImGui.ImGuiCol.BorderShadow] = new ImGui.ImVec4(0.000, 0.000, 0.000, 0.000); + colors[ImGui.ImGuiCol.FrameBg] = new ImGui.ImVec4(0.160, 0.160, 0.160, 1.000); + colors[ImGui.ImGuiCol.FrameBgHovered] = new ImGui.ImVec4(0.200, 0.200, 0.200, 1.000); + colors[ImGui.ImGuiCol.FrameBgActive] = new ImGui.ImVec4(0.280, 0.280, 0.280, 1.000); + colors[ImGui.ImGuiCol.TitleBg] = new ImGui.ImVec4(0.148, 0.148, 0.148, 1.000); + colors[ImGui.ImGuiCol.TitleBgActive] = new ImGui.ImVec4(0.148, 0.148, 0.148, 1.000); + colors[ImGui.ImGuiCol.TitleBgCollapsed] = new ImGui.ImVec4(0.148, 0.148, 0.148, 1.000); + colors[ImGui.ImGuiCol.MenuBarBg] = new ImGui.ImVec4(0.195, 0.195, 0.195, 1.000); + colors[ImGui.ImGuiCol.ScrollbarBg] = new ImGui.ImVec4(0.160, 0.160, 0.160, 1.000); + colors[ImGui.ImGuiCol.ScrollbarGrab] = new ImGui.ImVec4(0.277, 0.277, 0.277, 1.000); + colors[ImGui.ImGuiCol.ScrollbarGrabHovered] = new ImGui.ImVec4(0.300, 0.300, 0.300, 1.000); + colors[ImGui.ImGuiCol.ScrollbarGrabActive] = new ImGui.ImVec4(1.000, 0.391, 0.000, 1.000); + colors[ImGui.ImGuiCol.CheckMark] = new ImGui.ImVec4(1.000, 1.000, 1.000, 1.000); + colors[ImGui.ImGuiCol.SliderGrab] = new ImGui.ImVec4(0.391, 0.391, 0.391, 1.000); + colors[ImGui.ImGuiCol.SliderGrabActive] = new ImGui.ImVec4(1.000, 0.391, 0.000, 1.000); + colors[ImGui.ImGuiCol.Button] = new ImGui.ImVec4(1.000, 1.000, 1.000, 0.000); + colors[ImGui.ImGuiCol.ButtonHovered] = new ImGui.ImVec4(1.000, 1.000, 1.000, 0.156); + colors[ImGui.ImGuiCol.ButtonActive] = new ImGui.ImVec4(1.000, 1.000, 1.000, 0.391); + colors[ImGui.ImGuiCol.Header] = new ImGui.ImVec4(0.313, 0.313, 0.313, 1.000); + colors[ImGui.ImGuiCol.HeaderHovered] = new ImGui.ImVec4(0.469, 0.469, 0.469, 1.000); + colors[ImGui.ImGuiCol.HeaderActive] = new ImGui.ImVec4(0.469, 0.469, 0.469, 1.000); + colors[ImGui.ImGuiCol.Separator] = colors[ImGui.ImGuiCol.Border]; + colors[ImGui.ImGuiCol.SeparatorHovered] = new ImGui.ImVec4(0.391, 0.391, 0.391, 1.000); + colors[ImGui.ImGuiCol.SeparatorActive] = new ImGui.ImVec4(1.000, 0.391, 0.000, 1.000); + colors[ImGui.ImGuiCol.ResizeGrip] = new ImGui.ImVec4(1.000, 1.000, 1.000, 0.250); + colors[ImGui.ImGuiCol.ResizeGripHovered] = new ImGui.ImVec4(1.000, 1.000, 1.000, 0.670); + colors[ImGui.ImGuiCol.ResizeGripActive] = new ImGui.ImVec4(1.000, 0.391, 0.000, 1.000); + colors[ImGui.ImGuiCol.Tab] = new ImGui.ImVec4(0.098, 0.098, 0.098, 1.000); + colors[ImGui.ImGuiCol.TabHovered] = new ImGui.ImVec4(0.352, 0.352, 0.352, 1.000); + colors[ImGui.ImGuiCol.TabActive] = new ImGui.ImVec4(0.195, 0.195, 0.195, 1.000); + colors[ImGui.ImGuiCol.TabUnfocused] = new ImGui.ImVec4(0.098, 0.098, 0.098, 1.000); + colors[ImGui.ImGuiCol.TabUnfocusedActive] = new ImGui.ImVec4(0.195, 0.195, 0.195, 1.000); + colors[ImGui.ImGuiCol.PlotLines] = new ImGui.ImVec4(1.000, 0.391, 0.000, 0.781); //DockingPreview + colors[ImGui.ImGuiCol.PlotLinesHovered] = new ImGui.ImVec4(0.180, 0.180, 0.180, 1.000); //DockingEmptyBg + colors[ImGui.ImGuiCol.PlotLines + 2] = new ImGui.ImVec4(0.469, 0.469, 0.469, 1.000); + colors[ImGui.ImGuiCol.PlotLinesHovered + 2] = new ImGui.ImVec4(1.000, 0.391, 0.000, 1.000); + colors[ImGui.ImGuiCol.PlotHistogram + 2] = new ImGui.ImVec4(0.586, 0.586, 0.586, 1.000); + colors[ImGui.ImGuiCol.PlotHistogramHovered + 2] = new ImGui.ImVec4(1.000, 0.391, 0.000, 1.000); + colors[ImGui.ImGuiCol.TextSelectedBg + 2] = new ImGui.ImVec4(1.000, 1.000, 1.000, 0.156); + colors[ImGui.ImGuiCol.DragDropTarget + 2] = new ImGui.ImVec4(1.000, 0.391, 0.000, 1.000); + colors[ImGui.ImGuiCol.NavHighlight + 2] = new ImGui.ImVec4(1.000, 0.391, 0.000, 1.000); + colors[ImGui.ImGuiCol.NavWindowingHighlight + 2] = new ImGui.ImVec4(1.000, 0.391, 0.000, 1.000); + colors[ImGui.ImGuiCol.NavWindowingDimBg + 2] = new ImGui.ImVec4(0.000, 0.000, 0.000, 0.586); + colors[ImGui.ImGuiCol.ModalWindowDimBg + 2] = new ImGui.ImVec4(0.000, 0.000, 0.000, 0.586); + + style.ChildRounding = 4.0; + style.FrameBorderSize = 1.0; + style.FrameRounding = 2.0; + style.GrabMinSize = 7.0; + style.PopupRounding = 2.0; + style.ScrollbarRounding = 12.0; + style.ScrollbarSize = 13.0; + style.TabBorderSize = 1.0; + style.TabRounding = 0.0; + style.WindowRounding = 0.0; +} + function ApplyHue (style: ImGui.ImGuiStyle, hue: number) { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison for (let i = 0; i < ImGui.ImGuiCol.COUNT; i++) { diff --git a/web-packages/imgui-demo/src/object-inspectors/vfx-item-inspector.ts b/web-packages/imgui-demo/src/object-inspectors/vfx-item-inspector.ts index 784651fcb..1dbc11584 100644 --- a/web-packages/imgui-demo/src/object-inspectors/vfx-item-inspector.ts +++ b/web-packages/imgui-demo/src/object-inspectors/vfx-item-inspector.ts @@ -1,8 +1,8 @@ import type { Component, Material } from '@galacean/effects'; -import { EffectsObject, RendererComponent, SerializationHelper, VFXItem, getMergedStore, spec } from '@galacean/effects'; +import { EffectsObject, RendererComponent, SerializationHelper, VFXItem, generateGUID, getMergedStore, spec } from '@galacean/effects'; import { objectInspector } from '../core/decorators'; import { ObjectInspector } from './object-inspectors'; -import type { GLMaterial } from '@galacean/effects-webgl'; +import { GLMaterial } from '@galacean/effects-webgl'; import { GLTexture } from '@galacean/effects-webgl'; import type { FileNode } from '../core/file-node'; import { UIManager } from '../core/ui-manager'; @@ -29,10 +29,17 @@ export class VFXItemInspector extends ObjectInspector { ImGui.Text('GUID'); ImGui.SameLine(alignWidth); ImGui.Text(activeObject.getInstanceId()); - ImGui.Text('Visible'); + ImGui.Text('Is Active'); ImGui.SameLine(alignWidth); - //@ts-expect-error - ImGui.Checkbox('##Visible', (_ = activeObject.visible) => activeObject.visible = _); + ImGui.Checkbox('##IsActive', (_ = activeObject.isActive) => { + activeObject.setActive(_); + + return activeObject.isActive; + }); + + ImGui.Text('End Behavior'); + ImGui.SameLine(alignWidth); + ImGui.Text(this.endBehaviorToString(activeObject.endBehavior)); if (ImGui.CollapsingHeader(('Transform'), ImGui.TreeNodeFlags.DefaultOpen)) { const transform = activeObject.transform; @@ -58,33 +65,40 @@ export class VFXItemInspector extends ObjectInspector { for (const componet of activeObject.components) { const customEditor = UIManager.customEditors.get(componet.constructor); - if (customEditor) { - if (ImGui.CollapsingHeader(componet.constructor.name, ImGui.TreeNodeFlags.DefaultOpen)) { + if (ImGui.CollapsingHeader(componet.constructor.name, ImGui.TreeNodeFlags.DefaultOpen)) { + ImGui.Text('Enabled'); + ImGui.SameLine(alignWidth); + ImGui.Checkbox('##Enabled', (_ = componet.enabled) => { + componet.enabled = _; + + return componet.enabled; + }); + + if (customEditor) { customEditor.onInspectorGUI(); + continue; + } - continue; - } - if (ImGui.CollapsingHeader(componet.constructor.name, ImGui.TreeNodeFlags.DefaultOpen)) { const propertyDecoratorStore = getMergedStore(componet); - for (const peopertyName of Object.keys(componet)) { - const key = peopertyName as keyof Component; + for (const propertyName of Object.keys(componet)) { + const key = propertyName as keyof Component; const property = componet[key]; - const ImGuiID = componet.getInstanceId() + peopertyName; + const ImGuiID = componet.getInstanceId() + propertyName; if (typeof property === 'number') { - ImGui.Text(peopertyName); + ImGui.Text(propertyName); ImGui.SameLine(alignWidth); //@ts-expect-error ImGui.DragFloat('##DragFloat' + ImGuiID, (_ = componet[key]) => componet[key] = _, 0.03); } else if (typeof property === 'boolean') { - ImGui.Text(peopertyName); + ImGui.Text(propertyName); ImGui.SameLine(alignWidth); //@ts-expect-error ImGui.Checkbox('##Checkbox' + ImGuiID, (_ = componet[key]) => componet[key] = _); } else if (property instanceof EffectsObject) { - ImGui.Text(peopertyName); + ImGui.Text(propertyName); ImGui.SameLine(alignWidth); let name = 'EffectsObject'; @@ -116,10 +130,10 @@ export class VFXItemInspector extends ObjectInspector { if (componet instanceof RendererComponent) { ImGui.Text('Material'); ImGui.SameLine(alignWidth); - ImGui.Button(componet.material.name, new ImGui.Vec2(200, 0)); + ImGui.Button(componet.material?.name ?? '', new ImGui.Vec2(200, 0)); if (ImGui.BeginDragDropTarget()) { - const payload = ImGui.AcceptDragDropPayload(componet.material.constructor.name); + const payload = ImGui.AcceptDragDropPayload(GLMaterial.name); if (payload) { void (payload.Data as FileNode).getFile().then(async (file: File | undefined)=>{ @@ -143,14 +157,16 @@ export class VFXItemInspector extends ObjectInspector { if (activeObject.getComponent(RendererComponent)) { const material = activeObject.getComponent(RendererComponent).material; - if (ImGui.CollapsingHeader(material.name + ' (Material)##CollapsingHeader', ImGui.TreeNodeFlags.DefaultOpen)) { + if (material && ImGui.CollapsingHeader(material.name + ' (Material)##CollapsingHeader', ImGui.TreeNodeFlags.DefaultOpen)) { this.drawMaterial(material); } - } } private drawMaterial (material: Material) { + if (!material) { + return; + } const glMaterial = material as GLMaterial; const serializedData = glMaterial.toData(); const shaderProperties = material.shader.shaderData.properties; @@ -250,7 +266,7 @@ export class VFXItemInspector extends ObjectInspector { if (!serializedData.vector4s[uniformName]) { serializedData.vector4s[uniformName] = { x:1.0, y:1.0, z:0.0, w:0.0 }; } - if (ImGui.DragFloat4('##' + uniformName, serializedData.vector4s[uniformName])) { + if (ImGui.DragFloat4('##' + uniformName, serializedData.vector4s[uniformName], 0.02)) { dirtyFlag = true; } } else if (type === '2D') { @@ -280,9 +296,34 @@ export class VFXItemInspector extends ObjectInspector { } } - SerializationHelper.deserializeTaggedProperties(serializedData, glMaterial); + SerializationHelper.deserialize(serializedData, glMaterial); if (dirtyFlag) { GalaceanEffects.assetDataBase.setDirty(glMaterial.getInstanceId()); } } + + private endBehaviorToString (endBehavior: spec.EndBehavior) { + let result = ''; + + switch (endBehavior) { + case spec.EndBehavior.destroy: + result = 'Destroy'; + + break; + case spec.EndBehavior.forward: + result = 'Forward'; + + break; + case spec.EndBehavior.freeze: + result = 'Freeze'; + + break; + case spec.EndBehavior.restart: + result = 'Restart'; + + break; + } + + return result; + } } \ No newline at end of file diff --git a/web-packages/imgui-demo/src/panels/main-editor.ts b/web-packages/imgui-demo/src/panels/main-editor.ts index aa82a6935..817a37d8b 100644 --- a/web-packages/imgui-demo/src/panels/main-editor.ts +++ b/web-packages/imgui-demo/src/panels/main-editor.ts @@ -45,6 +45,16 @@ export class MainEditor extends EditorWindow { sceneImageSize.y -= 40; const player = GalaceanEffects.player; + const pos = ImGui.GetWindowPos(); + const windowSize = ImGui.GetWindowSize(); + const divElement = player.container; + + if (divElement) { + divElement.style.position = 'absolute'; + divElement.style.left = (pos.x + windowSize.x / 2) + 'px'; + divElement.style.top = (pos.y + windowSize.y * 0.9) + 'px'; + } + if (player.container && (player.container.style.width !== sceneImageSize.x + 'px' || player.container.style.height !== sceneImageSize.y + 'px') ) { @@ -52,7 +62,7 @@ export class MainEditor extends EditorWindow { player.container.style.height = sceneImageSize.y + 'px'; player.resize(); } - if (GalaceanEffects.sceneRendederTexture) { + if (GalaceanEffects.sceneRendederTexture && player.container && player.container.style.zIndex !== '999') { const frame_padding: int = 0; // -1 === uses default padding (style.FramePadding) const uv0: ImGui.Vec2 = new ImGui.Vec2(0.0, 0.0); // UV coordinates for lower-left const uv1: ImGui.Vec2 = new ImGui.Vec2(1.0, 1.0);// UV coordinates for (32,32) in our texture diff --git a/web-packages/imgui-demo/src/panels/sequencer.ts b/web-packages/imgui-demo/src/panels/sequencer.ts index cfa47a2e9..ff5b6f00e 100644 --- a/web-packages/imgui-demo/src/panels/sequencer.ts +++ b/web-packages/imgui-demo/src/panels/sequencer.ts @@ -1,5 +1,5 @@ import type { Composition, TrackAsset } from '@galacean/effects'; -import { CompositionComponent, VFXItem } from '@galacean/effects'; +import { Component, CompositionComponent, VFXItem } from '@galacean/effects'; import { editorWindow, menuItem } from '../core/decorators'; import { GalaceanEffects } from '../ge'; import { ImGui } from '../imgui'; @@ -55,12 +55,17 @@ export class Sequencer extends EditorWindow { //@ts-expect-error for (const track of compositionComponent.timelineAsset.tracks) { const trackAsset = track; + const boundObject = trackAsset.boundObject; - if (!(trackAsset.binding instanceof VFXItem)) { - continue; + let trackName = ''; + + if (boundObject instanceof VFXItem) { + trackName = boundObject.name; + } else if (boundObject instanceof Component) { + trackName = boundObject.constructor.name; } - if (ImGui.CollapsingHeader(trackAsset.binding.name, ImGui.ImGuiTreeNodeFlags.DefaultOpen)) { + if (ImGui.CollapsingHeader(trackName, ImGui.ImGuiTreeNodeFlags.DefaultOpen)) { this.drawTrack(trackAsset); } } diff --git a/web-packages/test/assets/cube-texture.ts b/web-packages/test/assets/cube-texture.ts new file mode 100644 index 000000000..91b2126d2 --- /dev/null +++ b/web-packages/test/assets/cube-texture.ts @@ -0,0 +1,119 @@ +const cubeTexture1 = { + 'compositionId': 1, + 'requires': [], + 'compositions': [{ + 'name': 'composition_1', + 'id': 1, + 'duration': 5, + 'camera': { 'fov': 30, 'far': 20, 'near': 0.1, 'position': [0, 0, 8], 'clipMode': 1 }, + 'items': [{ + 'name': '11111', + 'delay': 0, + 'id': 2, + 'type': '1', + 'ro': 0.1, + 'sprite': { + 'options': { + 'startLifetime': 2, + 'startSize': 0.8355836885408324, + 'sizeAspect': 1.1403508771929824, + 'startColor': [8, [255, 255, 255]], + 'duration': 2, + 'gravityModifier': 1, + 'renderLevel': 'B+', + }, 'renderer': { 'renderMode': 1, 'anchor': [0.5, 0.5], 'texture': 0 }, + }, + }], + 'meta': { 'previewSize': [0, 0] }, + }], + 'gltf': [], + 'images': [], + 'textures': [{ + 'mipmaps': [ + [[20, [3, 0, 113649]], [20, [3, 113652, 103308]], [20, [3, 216960, 73885]], [20, [3, 290848, 115292]], [20, [3, 406140, 109199]], [20, [3, 515340, 102131]]], + [[20, [3, 617472, 26164]], [20, [3, 643636, 23931]], [20, [3, 667568, 19089]], [20, [3, 686660, 24184]], [20, [3, 710844, 25232]], [20, [3, 736076, 23683]]], + [[20, [3, 759760, 5543]], [20, [3, 765304, 4313]], [20, [3, 769620, 4236]], [20, [3, 773856, 3895]], [20, [3, 777752, 4664]], [20, [3, 782416, 4697]]], + [[20, [3, 787116, 1550]], [20, [3, 788668, 1240]], [20, [3, 789908, 1230]], [20, [3, 791140, 1176]], [20, [3, 792316, 1322]], [20, [3, 793640, 1286]]], + [[20, [3, 794928, 453]], [20, [3, 795384, 444]], [20, [3, 795828, 458]], [20, [3, 796288, 512]], [20, [3, 796800, 474]], [20, [3, 797276, 499]]], + ], + 'sourceType': 7, + 'target': 34067, + }], + 'bins': [ + { 'url': 'https://gw.alipayobjects.com/os/gltf-asset/67210123752698/data0.bin' }, + { 'url': 'https://gw.alipayobjects.com/os/gltf-asset/67210123752698/data1.bin' }, + { 'url': 'https://gw.alipayobjects.com/os/gltf-asset/67210123752698/data2.bin' }, + { 'url': 'https://gw.alipayobjects.com/os/gltf-asset/67210123752698/data3.bin' }, + ], + 'version': '0.9.0', + 'shapes': [], + 'plugins': [], + 'type': 'mars', + '_imgs': { '1': [] }, +}; + +const cubeTexture2 = { + 'compositionId': 1, + 'requires': [], + 'compositions': [{ + 'name': 'composition_1', + 'id': 1, + 'duration': 5, + 'camera': { 'fov': 30, 'far': 20, 'near': 0.1, 'position': [0, 0, 8], 'clipMode': 1 }, + 'items': [{ + 'name': 'item_1', + 'delay': 0, + 'id': 1, + 'type': '1', + 'ro': 0.1, + 'sprite': { + 'options': { + 'startLifetime': 2, + 'startSize': 1.2, + 'sizeAspect': 1.320754716981132, + 'startColor': [8, [255, 255, 255]], + 'duration': 2, + 'gravityModifier': 1, + 'renderLevel': 'B+', + }, 'renderer': { 'renderMode': 1, 'anchor': [0.5, 0.5], 'texture': 0 }, + }, + }], + 'meta': { 'previewSize': [750, 1624] }, + }], + 'gltf': [], + 'images': [], + 'textures': [{ + 'minFilter': 9987, + 'magFilter': 9729, + 'wrapS': 33071, + 'wrapT': 33071, + 'target': 34067, + 'format': 6408, + 'internalFormat': 6408, + 'type': 5121, + 'mipmaps': [ + [[20, [5, 0, 24661]], [20, [5, 24664, 26074]], [20, [5, 50740, 26845]], [20, [5, 77588, 24422]], [20, [5, 102012, 24461]], [20, [5, 126476, 27099]]], + [[20, [5, 153576, 7699]], [20, [5, 161276, 7819]], [20, [5, 169096, 8919]], [20, [5, 178016, 7004]], [20, [5, 185020, 7657]], [20, [5, 192680, 8515]]], + [[20, [5, 201196, 2305]], [20, [5, 203504, 2388]], [20, [5, 205892, 2789]], [20, [5, 208684, 2147]], [20, [5, 210832, 2351]], [20, [5, 213184, 2541]]], + [[20, [5, 215728, 755]], [20, [5, 216484, 810]], [20, [5, 217296, 902]], [20, [5, 218200, 727]], [20, [5, 218928, 775]], [20, [5, 219704, 835]]], + [[20, [5, 220540, 292]], [20, [5, 220832, 301]], [20, [5, 221136, 317]], [20, [5, 221456, 285]], [20, [5, 221744, 301]], [20, [5, 222048, 307]]], + [[20, [5, 222356, 147]], [20, [5, 222504, 147]], [20, [5, 222652, 149]], [20, [5, 222804, 149]], [20, [5, 222956, 149]], [20, [5, 223108, 149]]], + [[20, [5, 223260, 96]], [20, [5, 223356, 96]], [20, [5, 223452, 96]], [20, [5, 223548, 97]], [20, [5, 223648, 97]], [20, [5, 223748, 97]]], + [[20, [5, 223848, 83]], [20, [5, 223932, 83]], [20, [5, 224016, 83]], [20, [5, 224100, 83]], [20, [5, 224184, 83]], [20, [5, 224268, 83]]]], + 'sourceType': 7, + }], + 'bins': [ + new ArrayBuffer(1), + new ArrayBuffer(1), + new ArrayBuffer(1), + new ArrayBuffer(1), + new ArrayBuffer(1), + new ArrayBuffer(1), + ], + 'version': '0.9.0', + 'shapes': [], + 'plugins': [], + 'type': 'mars', +}; + +export { cubeTexture1, cubeTexture2 }; diff --git a/web-packages/test/case/2d/src/common/utilities.ts b/web-packages/test/case/2d/src/common/utilities.ts index cb231792b..5975dd634 100644 --- a/web-packages/test/case/2d/src/common/utilities.ts +++ b/web-packages/test/case/2d/src/common/utilities.ts @@ -9,7 +9,7 @@ const { Vector3, Matrix4 } = math; const sleepTime = 20; const params = new URLSearchParams(location.search); -const oldVersion = params.get('version') || '2.0.0'; // 旧版Player版本 +const oldVersion = params.get('version') || '2.1.0-alpha.5'; // 旧版Player版本 const playerOptions: PlayerConfig = { //env: 'editor', //pixelRatio: 2, @@ -21,12 +21,37 @@ const playerOptions: PlayerConfig = { export class TestPlayer { constructor (width, height, playerClass, playerOptions, renderFramework, registerFunc, Plugin, VFXItem, assetManager, oldVersion, is3DCase) { + + width /= 2; + height /= 2; + this.width = width; this.height = height; - // + + this.div = document.createElement('div'); + + this.div.style.position = 'absolute'; + this.div.style.width = width + 'px'; + this.div.style.height = height + 'px'; + this.div.style.backgroundColor = 'black'; + + const left = 1800; + const top = 800; + + if (oldVersion) { + this.div.style.left = left + 'px'; + this.div.style.top = top + 'px'; + } else { + this.div.style.left = (left + width) + 'px'; + this.div.style.top = top + 'px'; + } + this.canvas = document.createElement('canvas'); - this.canvas.width = width; - this.canvas.height = height; + + const body = document.getElementsByTagName('body')[0]; + + body.appendChild(this.div); + this.div.appendChild(this.canvas); this.renderFramework = renderFramework; // this.player = new playerClass({ @@ -110,7 +135,7 @@ export class TestPlayer { if (this.composition.content) { return this.composition.content.duration; } else { - return this.composition.duration; + return this.composition.getDuration(); } } @@ -210,6 +235,8 @@ export class TestPlayer { this.player = null; this.canvas.remove(); this.canvas = null; + this.div.remove(); + this.div = null; } } diff --git a/web-packages/test/case/3d/src/case.ts b/web-packages/test/case/3d/src/case.ts index b9656cd0f..0e06c5d64 100644 --- a/web-packages/test/case/3d/src/case.ts +++ b/web-packages/test/case/3d/src/case.ts @@ -104,7 +104,7 @@ function addDescribe (renderFramework) { for (let i = 0; i < timeList.length; i++) { const time = timeList[i]; - if (!oldPlayer.isLoop() && time >= oldPlayer.duration()) { + if (time >= oldPlayer.duration()) { break; } // diff --git a/web-packages/test/package.json b/web-packages/test/package.json index 641677d6c..789b0b9a3 100644 --- a/web-packages/test/package.json +++ b/web-packages/test/package.json @@ -9,6 +9,6 @@ "@galacean/effects": "workspace:*", "@galacean/effects-helper": "workspace:*", "@galacean/effects-threejs": "workspace:*", - "@vvfx/resource-detection": "^0.6.2" + "@vvfx/resource-detection": "^0.7.0" } } diff --git a/web-packages/test/unit/src/effects-core/assert-manager.spec.ts b/web-packages/test/unit/src/effects-core/assert-manager.spec.ts deleted file mode 100644 index 8c981c401..000000000 --- a/web-packages/test/unit/src/effects-core/assert-manager.spec.ts +++ /dev/null @@ -1,209 +0,0 @@ -import type { Texture2DSourceOptionsVideo } from '@galacean/effects'; -import { Player } from '@galacean/effects'; - -const { expect } = chai; - -describe('core/asset-manager', () => { - let player: Player | null; - - before(() => { - player = new Player({ canvas: document.createElement('canvas'), manualRender: true }); - }); - - after(() => { - player!.dispose(); - player = null; - }); - - it('template video', async () => { - player = player!; - const assets = { - 'images': [ - { - 'template': { - 'v': 2, - 'variables': { - 'test': 'https://mdn.alipayobjects.com/huamei_p0cigc/afts/file/A*ZOgXRbmVlsIAAAAAAAAAAAAADoB5AQ', - }, - 'width': 126, - 'height': 130, - 'background': { - 'type': 'video', - 'name': 'test', - 'url': 'https://mdn.alipayobjects.com/huamei_p0cigc/afts/file/A*ZOgXRbmVlsIAAAAAAAAAAAAADoB5AQ', - }, - }, - 'url': 'https://mdn.alipayobjects.com/mars/afts/img/A*oKwARKdkWhEAAAAAAAAAAAAADlB4AQ/original', - 'webp': 'https://mdn.alipayobjects.com/mars/afts/img/A*eOLVQpT57FcAAAAAAAAAAAAADlB4AQ/original', - 'renderLevel': 'B+', - }, - ], - 'fonts': [], - 'spines': [], - 'shapes': [], - }; - - const comp = await player.loadScene(generateScene(assets)); - const textures = comp.textures; - - expect(textures.length).to.deep.equals(1); - expect(textures[0].source).to.not.be.empty; - const videoElement = (textures[0].source as Texture2DSourceOptionsVideo).video; - - expect(videoElement).to.be.an.instanceOf(HTMLVideoElement); - - player.gotoAndStop(0.1); - }); - - it('templateV2 video variables', async () => { - player = player!; - const assets = { - 'images': [ - { - 'template': { - 'v': 2, - 'variables': { - 'test': 'https://mdn.alipayobjects.com/huamei_p0cigc/afts/file/A*ZOgXRbmVlsIAAAAAAAAAAAAADoB5AQ', - }, - 'width': 126, - 'height': 130, - 'background': { - 'type': 'video', - 'name': 'test', - 'url': 'https://mdn.alipayobjects.com/huamei_p0cigc/afts/file/A*ZOgXRbmVlsIAAAAAAAAAAAAADoB5AQ', - }, - }, - 'url': 'https://mdn.alipayobjects.com/mars/afts/img/A*oKwARKdkWhEAAAAAAAAAAAAADlB4AQ/original', - 'webp': 'https://mdn.alipayobjects.com/mars/afts/img/A*eOLVQpT57FcAAAAAAAAAAAAADlB4AQ/original', - 'renderLevel': 'B+', - }, - ], - 'fonts': [], - 'spines': [], - 'shapes': [], - }; - - const comp = await player.loadScene(generateScene(assets), { - variables: { - // 视频地址 - test: 'https://gw.alipayobjects.com/v/huamei_p0cigc/afts/video/A*dftzSq2szUsAAAAAAAAAAAAADtN3AQ', - }, - }); - - const textures = comp.textures; - - expect(textures.length).to.deep.equals(1); - expect(textures[0].source).to.not.be.empty; - const videoElement = (textures[0].source as Texture2DSourceOptionsVideo).video; - - expect(videoElement).to.be.an.instanceOf(HTMLVideoElement); - - player.gotoAndStop(0.1); - }); -}); - -//@ts-expect-error -const generateScene = assets => { - const res = { - 'playerVersion': { - 'web': '1.2.1', - 'native': '0.0.1.202311221223', - }, - 'version': '2.2', - 'type': 'ge', - 'compositions': [ - { - 'id': '1', - 'name': '新建合成1', - 'duration': 5, - 'startTime': 0, - 'endBehavior': 0, - 'previewSize': [750, 1624], - 'items': [ - { - 'id': '1', - 'name': 'sprite_1', - 'duration': 5, - 'type': '1', - 'visible': true, - 'endBehavior': 0, - 'delay': 0, - 'renderLevel': 'B+', - 'content': { - 'options': { - 'startColor': [0.9529, 1, 0.0431, 1], - }, - 'renderer': { - 'renderMode': 1, - 'texture': 0, - }, - 'positionOverLifetime': { - 'path': [12, [ - [ - [0, 0, 0, 2.3256], - [0.43, 1, 2.3256, 3.4483], - [0.72, 2, 3.4483, 0], - ], - [ - [0, 0, 0], - [0, 7.79, 0], - [3.3269, 7.79, 0], - ], - [ - [0, 1.9475, 0], - [0, 5.8425, 0], - [0.8317, 7.79, 0], - [2.4952, 7.79, 0], - ], - ], - ], - 'direction': [0, 0, 0], - 'startSpeed': 0, - 'gravity': [0, 0, 0], - 'gravityOverLifetime': [0, 1], - }, - 'sizeOverLifetime': { - 'size': [6, [ - [0.126, 1.2055, 0, 1.6835], - [0.72, 2.5395, 1.6835, 0], - ], - ], - }, - 'colorOverLifetime': { - 'opacity': [6, [ - [0, 0, 0, 1.3889], - [0.72, 1, 1.3889, 0], - ], - ], - }, - }, - 'transform': { - 'position': [0, 0, 0], - 'rotation': [0, 0, 0], - 'scale': [1.5492, 1.5984, 1], - }, - }, - ], - 'camera': { - 'fov': 60, - 'far': 40, - 'near': 0.1, - 'clipMode': 1, - 'position': [0, 0, 8], - 'rotation': [0, 0, 0], - }, - }, - ], - 'requires': [], - 'compositionId': '1', - 'bins': [], - 'textures': [ - { - 'source': 0, - 'flipY': true, - }, - ], - }; - - return { ...res, ...assets }; -}; diff --git a/web-packages/test/unit/src/effects-core/asset-manager.spec.ts b/web-packages/test/unit/src/effects-core/asset-manager.spec.ts new file mode 100644 index 000000000..e63bfe54d --- /dev/null +++ b/web-packages/test/unit/src/effects-core/asset-manager.spec.ts @@ -0,0 +1,64 @@ +import { AssetManager, TextureSourceType, spec } from '@galacean/effects'; + +const { expect } = chai; + +describe('core/asset-manager', () => { + let assetManager: AssetManager; + + before(() => { + }); + + after(() => { + assetManager?.dispose(); + }); + + it('scene renderLevel is right when pass options', async () => { + assetManager = new AssetManager({ + renderLevel: spec.RenderLevel.B, + }); + const scene = await assetManager.loadScene('https://mdn.alipayobjects.com/mars/afts/file/A*GC99RbcyZiMAAAAAAAAAAAAADlB4AQ'); + + expect(scene.renderLevel).to.eql(spec.RenderLevel.B); + }); + + it('scene renderLevel is right when not pass options', async () => { + assetManager = new AssetManager(); + const scene = await assetManager.loadScene('https://mdn.alipayobjects.com/mars/afts/file/A*GC99RbcyZiMAAAAAAAAAAAAADlB4AQ'); + + expect(scene.renderLevel).to.eql(undefined); + }); + + it('image replace right when pass variables', async () => { + const json = 'https://mdn.alipayobjects.com/mars/afts/file/A*PubBSpHUbjYAAAAAAAAAAAAADlB4AQ'; + const url = 'https://mdn.alipayobjects.com/huamei_klifp9/afts/img/A*ySrfRJvfvfQAAAAAAAAAAAAADvV6AQ/original'; + + assetManager = new AssetManager({ + variables: { + image: url, + }, + }); + const scene = await assetManager.loadScene(json); + + expect((scene.images[0] as HTMLImageElement).src).to.eql(url); + expect(scene.textureOptions[0].image.src).to.eql(url); + }); + + it('video replace right when pass variables', async () => { + const json = 'https://mdn.alipayobjects.com/mars/afts/file/A*kENFRbxlKcUAAAAAAAAAAAAADlB4AQ'; + const url = 'https://gw.alipayobjects.com/v/huamei_p0cigc/afts/video/A*7gPzSo3RxlQAAAAAAAAAAAAADtN3AQ'; + const text = 'Dynamic Video'; + + assetManager = new AssetManager({ + variables: { + video: url, + text_3: text, + }, + }); + const scene = await assetManager.loadScene(json); + + expect((scene.images[1] as HTMLVideoElement).src).to.eql(url); + expect(scene.textureOptions[1].sourceType).to.eql(TextureSourceType.video); + expect(scene.textureOptions[1].video.src).to.eql(url); + expect(scene.jsonScene.items[0].content.options.text).to.not.eql(text); + }); +}); diff --git a/web-packages/test/unit/src/effects-core/composition/composition.spec.ts b/web-packages/test/unit/src/effects-core/composition/composition.spec.ts index 27ec8f3a9..60769b7da 100644 --- a/web-packages/test/unit/src/effects-core/composition/composition.spec.ts +++ b/web-packages/test/unit/src/effects-core/composition/composition.spec.ts @@ -149,6 +149,6 @@ describe('core/composition', () => { comp.setVisible(false); - expect(comp.items[0].getVisible()).to.eql(false, 'composition visible'); + expect(comp.items[0].isActive).to.eql(false, 'composition visible'); }); }); diff --git a/web-packages/test/unit/src/effects-core/composition/on-end.spec.ts b/web-packages/test/unit/src/effects-core/composition/on-end.spec.ts index cfe89f5eb..258a12226 100644 --- a/web-packages/test/unit/src/effects-core/composition/on-end.spec.ts +++ b/web-packages/test/unit/src/effects-core/composition/on-end.spec.ts @@ -72,7 +72,7 @@ describe('core/composition/on-end', () => { const composition = await player.loadScene(JSON.parse(json)); expect(composition.startTime).to.eql(1.3); - expect(composition.time).to.eql(1.3); + expect(composition.time).to.closeTo(1.3, 0.001); }); async function test (endBehavior: spec.EndBehavior, fn: () => void) { diff --git a/web-packages/test/unit/src/effects-core/fallback/particle/base.spec.ts b/web-packages/test/unit/src/effects-core/fallback/particle/base.spec.ts index cd5904cfe..f47275259 100644 --- a/web-packages/test/unit/src/effects-core/fallback/particle/base.spec.ts +++ b/web-packages/test/unit/src/effects-core/fallback/particle/base.spec.ts @@ -195,7 +195,7 @@ describe('core/fallback/particle/base', () => { const neo = getStandardItem(item); const shape = neo.content.shape; - expect(shape.type).to.be.eql(spec.ShapeType.RECTANGLE_EDGE); + expect(shape.type).to.be.eql(spec.ParticleEmitterShapeType.RECTANGLE_EDGE); expect(shape.radius).to.be.eql(1); }); diff --git a/web-packages/test/unit/src/effects-core/index.ts b/web-packages/test/unit/src/effects-core/index.ts index 1801bdae9..f7feb93a1 100644 --- a/web-packages/test/unit/src/effects-core/index.ts +++ b/web-packages/test/unit/src/effects-core/index.ts @@ -9,7 +9,7 @@ import './plugins/common/end-behevior.spec'; import './plugins/particle'; // plugin sprite import './plugins/sprite'; -import './assert-manager.spec'; +import './asset-manager.spec'; import './texture.spec'; import './transform.spec'; import './utils.spec'; diff --git a/web-packages/test/unit/src/effects-core/interact/interact.spec.ts b/web-packages/test/unit/src/effects-core/interact/interact.spec.ts index 10293d2e1..a6a8b9045 100644 --- a/web-packages/test/unit/src/effects-core/interact/interact.spec.ts +++ b/web-packages/test/unit/src/effects-core/interact/interact.spec.ts @@ -503,7 +503,7 @@ describe('core/interact/item', () => { player?.gotoAndStop(0.3); expect(messagePhrase).to.eql(spec.MESSAGE_ITEM_PHRASE_END, 'MESSAGE_ITEM_PHRASE_END'); - expect(messageSpy).to.have.been.called.once; + expect(messageSpy).to.have.been.called.twice; comp?.dispose(); }); diff --git a/web-packages/test/unit/src/effects-core/plugins/common/end-behevior.spec.ts b/web-packages/test/unit/src/effects-core/plugins/common/end-behevior.spec.ts index 964da73df..de9fe980d 100644 --- a/web-packages/test/unit/src/effects-core/plugins/common/end-behevior.spec.ts +++ b/web-packages/test/unit/src/effects-core/plugins/common/end-behevior.spec.ts @@ -28,9 +28,9 @@ describe('core/plugins/common/item-end', () => { const item = composition.getItemByName('sprite_1'); composition.gotoAndStop(0.01 + 1); - expect(item?.ended).to.equal(false); + expect(item?.transform.getValid()).to.equal(true); composition.gotoAndStop(0.01 + 2); - expect(item?.ended).to.equal(true); + expect(item?.transform.getValid()).to.equal(false); }); it('item freeze', async () => { @@ -38,7 +38,7 @@ describe('core/plugins/common/item-end', () => { const item = composition.getItemByName('sprite_1'); composition.gotoAndStop(0.01 + 2); - expect(item?.ended).to.equal(false); + expect(item?.transform.getValid()).to.equal(true); }); it('item loop', async () => { @@ -46,7 +46,7 @@ describe('core/plugins/common/item-end', () => { const item = composition.getItemByName('sprite_1'); composition.gotoAndStop(1); - expect(item?.ended).to.equal(false); + expect(item?.transform.getValid()).to.equal(true); }); }); diff --git a/web-packages/test/unit/src/effects-core/plugins/particle/base.spec.ts b/web-packages/test/unit/src/effects-core/plugins/particle/base.spec.ts index 0222d1fd7..6618c6481 100644 --- a/web-packages/test/unit/src/effects-core/plugins/particle/base.spec.ts +++ b/web-packages/test/unit/src/effects-core/plugins/particle/base.spec.ts @@ -1,4 +1,4 @@ -import type { GradientValue, RandomSetValue, VFXItem, color } from '@galacean/effects'; +import type { GradientValue, RandomSetValue, VFXItem } from '@galacean/effects'; import { Player, ParticleSystem, spec } from '@galacean/effects'; const { expect } = chai; @@ -70,7 +70,7 @@ describe('core/plugins/particle/base', () => { const colors = comp.getItemByName('colors') as VFXItem; const gradient = comp.getItemByName('gradient') as VFXItem; const pureStartColor = pure.getComponent(ParticleSystem).options.startColor; - const colorsStartColor = colors.getComponent(ParticleSystem).options.startColor as RandomSetValue; + const colorsStartColor = colors.getComponent(ParticleSystem).options.startColor as RandomSetValue; const gradientStartColor = gradient.getComponent(ParticleSystem).options.startColor as unknown as GradientValue; expect(pureStartColor.getValue()).to.eql([255, 255, 255], 'pure color'); diff --git a/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-base.spec.ts b/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-base.spec.ts index ec7b1a023..4bb4f7e14 100644 --- a/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-base.spec.ts +++ b/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-base.spec.ts @@ -41,7 +41,7 @@ describe('core/plugins/sprite/item-base', () => { let spriteColorTrack; // @ts-expect-error - const spriteBindingTrack = comp.rootItem.getComponent(CompositionComponent).timelineAsset.tracks.find(track => track.binding === sprite1); + const spriteBindingTrack = comp.rootItem.getComponent(CompositionComponent).timelineAsset.tracks.find(track => track.boundObject === sprite1); for (const subTrack of spriteBindingTrack?.getChildTracks() ?? []) { if (subTrack instanceof SpriteColorTrack) { @@ -72,103 +72,8 @@ describe('core/plugins/sprite/item-base', () => { // 尺寸随时间变换 it('sprite sizeOverLifetime', async () => { - const items = [ - { - 'id': '5', - 'name': 'item', - 'duration': 5, - 'type': '1', - 'visible': true, - 'endBehavior': 0, - 'delay': 0, - 'renderLevel': 'B+', - 'content': { - 'options': { - 'startColor': [ - 1, - 1, - 1, - 1, - ], - }, - 'renderer': { - 'renderMode': 1, - 'texture': 0, - }, - 'positionOverLifetime': { - 'direction': [ - 0, - 0, - 0, - ], - 'startSpeed': 0, - 'gravity': [ - 0, - 0, - 0, - ], - 'gravityOverLifetime': [ - 0, - 1, - ], - }, - 'sizeOverLifetime': { - 'size': [ - 21, - [ - [ - 4, - [ - 0, - 1, - ], - ], - ], - ], - 'separateAxes': true, - 'x': [ - 21, - [ - [ - 4, - [ - 0, - 2, - ], - ], - ], - ], - }, - 'splits': [ - [ - 0, - 0, - 1, - 1, - 0, - ], - ], - }, - 'transform': { - 'position': [ - 0, - 0, - 0, - ], - 'rotation': [ - 0, - 0, - 0, - ], - 'scale': [ - 12.5965, - 12.5965, - 1, - ], - }, - }, - ] as spec.Item[]; - const comp = await player.loadScene(generateSceneJSON(items)); + const items = '[{"id":"5","name":"item","duration":5,"type":"1","visible":true,"endBehavior":0,"delay":0,"renderLevel":"B+","content":{"options":{"startColor":[1,1,1,1]},"renderer":{"renderMode":1,"texture":0},"positionOverLifetime":{"direction":[0,0,0],"startSpeed":0,"gravity":[0,0,0],"gravityOverLifetime":[0,1]},"sizeOverLifetime":{"size":[21,[[4,[0,1]]]],"separateAxes":true,"x":[21,[[4,[0,2]]]]},"splits":[[0,0,1,1,0]]},"transform":{"position":[0,0,0],"rotation":[0,0,0],"scale":[12.5965,12.5965,1]}}]'; + const comp = await player.loadScene(generateSceneJSON(JSON.parse(items))); player.gotoAndPlay(0.01); const spriteItem = comp.getItemByName('item')?.getComponent(SpriteComponent); @@ -205,7 +110,7 @@ describe('core/plugins/sprite/item-base', () => { expect(texOffset2?.[2]).to.be.closeTo(0.1248, 0.001); expect(texOffset2?.[3]).to.be.closeTo(0.1249, 0.001); - expect(texOffset3?.[0]).to.be.closeTo(0.5, 0.001); + expect(texOffset3?.[0]).to.be.closeTo(0.625, 0.001); expect(texOffset3?.[1]).to.be.closeTo(0.5, 0.001); expect(texOffset3?.[2]).to.be.closeTo(0.125, 0.001); expect(texOffset3?.[3]).to.be.closeTo(0.125, 0.001); diff --git a/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-item.spec.ts b/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-item.spec.ts index 7534fc124..7f429ca2f 100644 --- a/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-item.spec.ts +++ b/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-item.spec.ts @@ -287,7 +287,7 @@ describe('core/plugins/sprite/item', () => { spriteItem?.setTexture(testTexture); const material = spriteItem?.material; - const texture = material?.getTexture('uSampler0'); + const texture = material?.getTexture('_MainTex'); expect(texture?.id).to.eql(testTexture.id, 'texture id'); }); diff --git a/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-renderder.spec.ts b/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-renderder.spec.ts index ca7adab69..14081d0e4 100644 --- a/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-renderder.spec.ts +++ b/web-packages/test/unit/src/effects-core/plugins/sprite/sprite-renderder.spec.ts @@ -202,7 +202,7 @@ describe('core/plugins/sprite/renderer', () => { const comp = await loadSceneAndPlay(player, JSON.parse(json), currentTime); const spriteItem = comp.getItemByName('sprite_1')?.getComponent(SpriteComponent); - spriteItem?.update(0.1); + spriteItem?.onUpdate(0.1); const transform = spriteItem?.item.transform; const startSize = transform?.size; const a = transform?.anchor.toArray(); diff --git a/web-packages/test/unit/src/effects-core/texture.spec.ts b/web-packages/test/unit/src/effects-core/texture.spec.ts index cc811ddb9..818be7ced 100644 --- a/web-packages/test/unit/src/effects-core/texture.spec.ts +++ b/web-packages/test/unit/src/effects-core/texture.spec.ts @@ -1,4 +1,4 @@ -import { Texture, Player } from '@galacean/effects'; +import { Texture, Player, glContext } from '@galacean/effects'; const { expect } = chai; @@ -30,4 +30,26 @@ describe('core/texture', () => { expect(testTexture.height).to.be.a('number'); expect(testTexture.id).to.be.a('string'); }); + + it('texture from image with options', async () => { + const testTexture = await Texture.fromImage('https://gw.alipayobjects.com/mdn/rms_2e421e/afts/img/A*fRtNTKrsq3YAAAAAAAAAAAAAARQnAQ', player.renderer.engine, + { + minFilter: glContext.LINEAR_MIPMAP_LINEAR, + magFilter: glContext.LINEAR, + wrapS: glContext.REPEAT, + wrapT: glContext.MIRRORED_REPEAT, + } + ); + const textureSource = testTexture.source; + + expect(textureSource.minFilter).to.equal(glContext.LINEAR_MIPMAP_LINEAR); + expect(textureSource.magFilter).to.equal(glContext.LINEAR); + expect(textureSource.wrapS).to.equal(glContext.REPEAT); + expect(textureSource.wrapT).to.equal(glContext.MIRRORED_REPEAT); + expect(textureSource.flipY).to.be.true; + expect(testTexture).to.be.an.instanceOf(Texture); + expect(testTexture.width).to.be.a('number'); + expect(testTexture.height).to.be.a('number'); + expect(testTexture.id).to.be.a('string'); + }); }); diff --git a/web-packages/test/unit/src/effects-webgl/gl-material.spec.ts b/web-packages/test/unit/src/effects-webgl/gl-material.spec.ts index 77bbd5dc6..b036b65f0 100644 --- a/web-packages/test/unit/src/effects-webgl/gl-material.spec.ts +++ b/web-packages/test/unit/src/effects-webgl/gl-material.spec.ts @@ -1687,7 +1687,7 @@ function generateGLMaterial ( ) { const material = new GLMaterial(engine, { shader }); - material.sampleAlphaToCoverage = !!(states.sampleAlphaToCoverage); + material.sampleAlphaToCoverage = !!states.sampleAlphaToCoverage; material.depthTest = states.depthTest; material.depthMask = states.depthMask; material.depthRange = states.depthRange; diff --git a/web-packages/test/unit/src/effects/scene-load.spec.ts b/web-packages/test/unit/src/effects/scene-load.spec.ts index 8850dce39..ba0543931 100644 --- a/web-packages/test/unit/src/effects/scene-load.spec.ts +++ b/web-packages/test/unit/src/effects/scene-load.spec.ts @@ -1,4 +1,6 @@ -import { Player, TextComponent } from '@galacean/effects'; +import type { Texture2DSourceOptionsVideo } from '@galacean/effects'; +import { AssetManager, Player, SpriteComponent, TextComponent, spec } from '@galacean/effects'; +import { cubeTexture1, cubeTexture2 } from '../../../assets/cube-texture'; const { expect } = chai; @@ -27,7 +29,7 @@ describe('player/scene-load', () => { await player.loadScene(json, { variables }); // @ts-expect-error - expect(player.assetManagers.find(d => d.baseUrl === json).options.variables).to.eql(variables); + expect(player.getAssetManager().find(d => d.baseUrl === json)?.options.variables).to.eql(variables); }); it('加载单个合成 JSONValue 并设置可选参数', async () => { @@ -38,8 +40,7 @@ describe('player/scene-load', () => { await player.loadScene(json, { variables }); - // @ts-expect-error - expect(player.assetManagers[1].options.variables).to.eql(variables); + expect(player.getAssetManager()[1].options.variables).to.eql(variables); }); it('加载多个合成链接并各自设置可选参数', async () => { @@ -53,7 +54,6 @@ describe('player/scene-load', () => { 'image1': 'https://mdn.alipayobjects.com/huamei_uj3n0k/afts/img/A*st1QSIvJEBcAAAAAAAAAAAAADt_KAQ/original', }; - // @ts-expect-error const [composition1, composition2] = await player.loadScene([{ url: json1, options: { @@ -68,9 +68,9 @@ describe('player/scene-load', () => { }]); // @ts-expect-error - expect(player.assetManagers.find(d => d.baseUrl === json1).options.variables).to.eql(variables1); + expect(player.getAssetManager().find(d => d.baseUrl === json1).options.variables).to.eql(variables1); // @ts-expect-error - expect(player.assetManagers.find(d => d.baseUrl === json2).options.variables).to.eql(variables2); + expect(player.getAssetManager().find(d => d.baseUrl === json2).options.variables).to.eql(variables2); expect(composition1.getSpeed()).to.eql(2); expect(composition2.getSpeed()).to.eql(1); }); @@ -79,7 +79,6 @@ describe('player/scene-load', () => { const json1 = 'https://mdn.alipayobjects.com/mars/afts/file/A*T1U4SqWhvioAAAAAAAAAAAAADlB4AQ'; const json2 = 'https://mdn.alipayobjects.com/mars/afts/file/A*de0NTrRAyzoAAAAAAAAAAAAADlB4AQ'; - // @ts-expect-error const [composition1, composition2] = await player.loadScene([{ url: json1, }, { @@ -178,177 +177,7 @@ describe('player/scene-load', () => { expect(textComponent?.text).to.eql('ttt'); }); - // TODO 未通过 先注释 - // it('load cube texture demo2', async () => { - // const json = { - // 'compositionId': 1, - // 'requires': [], - // 'compositions': [{ - // 'name': 'composition_1', - // 'id': 1, - // 'duration': 5, - // 'camera': { 'fov': 30, 'far': 20, 'near': 0.1, 'position': [0, 0, 8], 'clipMode': 1 }, - // 'items': [{ - // 'name': '11111', - // 'delay': 0, - // 'id': 2, - // 'type': '1', - // 'ro': 0.1, - // 'sprite': { - // 'options': { - // 'startLifetime': 2, - // 'startSize': 0.8355836885408324, - // 'sizeAspect': 1.1403508771929824, - // 'startColor': [8, [255, 255, 255]], - // 'duration': 2, - // 'gravityModifier': 1, - // 'renderLevel': 'B+', - // }, 'renderer': { 'renderMode': 1, 'anchor': [0.5, 0.5], 'texture': 0 }, - // }, - // }], - // 'meta': { 'previewSize': [0, 0] }, - // }], - // 'gltf': [], - // 'images': [], - // 'version': '0.9.0', - // 'shapes': [], - // 'plugins': [], - // 'bins': [ - // { 'url': 'https://gw.alipayobjects.com/os/gltf-asset/67210123752698/data0.bin' }, - // { 'url': 'https://gw.alipayobjects.com/os/gltf-asset/67210123752698/data1.bin' }, - // { 'url': 'https://gw.alipayobjects.com/os/gltf-asset/67210123752698/data2.bin' }, - // { 'url': 'https://gw.alipayobjects.com/os/gltf-asset/67210123752698/data3.bin' }, - // ], - // 'textures': [ - // { - // 'mipmaps': [ - // [ - // [20, [3, 0, 113649]], - // [20, [3, 113652, 103308]], - // [20, [3, 216960, 73885]], - // [20, [3, 290848, 115292]], - // [20, [3, 406140, 109199]], - // [20, [3, 515340, 102131]], - // ], - // [ - // [20, [3, 617472, 26164]], - // [20, [3, 643636, 23931]], - // [20, [3, 667568, 19089]], - // [20, [3, 686660, 24184]], - // [20, [3, 710844, 25232]], - // [20, [3, 736076, 23683]], - // ], - // [ - // [20, [3, 759760, 5543]], - // [20, [3, 765304, 4313]], - // [20, [3, 769620, 4236]], - // [20, [3, 773856, 3895]], - // [20, [3, 777752, 4664]], - // [20, [3, 782416, 4697]], - // ], - // [ - // [20, [3, 787116, 1550]], - // [20, [3, 788668, 1240]], - // [20, [3, 789908, 1230]], - // [20, [3, 791140, 1176]], - // [20, [3, 792316, 1322]], - // [20, [3, 793640, 1286]], - // ], - // [ - // [20, [3, 794928, 453]], - // [20, [3, 795384, 444]], - // [20, [3, 795828, 458]], - // [20, [3, 796288, 512]], - // [20, [3, 796800, 474]], - // [20, [3, 797276, 499]], - // ], - // ], - // 'sourceType': 7, - // 'target': 34067, - // }, - // ], - // 'type': 'mars', - // '_imgs': { '1': [] }, - // }; - // const scn = await player.loadScene(json); - // - // expect(scn.textures[0].mipmaps.length).to.eql(json.textures[0].mipmaps.length); - // expect(scn.textures[0].mipmaps.every(m => m.every(img => img instanceof HTMLImageElement || img instanceof ImageBitmap))).to.be.true; - // }); - // - // it('load cube textures fail', async () => { - // const a = { - // 'compositionId': 1, - // 'requires': [], - // 'compositions': [{ - // 'name': 'composition_1', - // 'id': 1, - // 'duration': 5, - // 'camera': { 'fov': 30, 'far': 20, 'near': 0.1, 'position': [0, 0, 8], 'clipMode': 1 }, - // 'items': [{ - // 'name': 'item_1', - // 'delay': 0, - // 'id': 1, - // 'type': '1', - // 'ro': 0.1, - // 'sprite': { - // 'options': { - // 'startLifetime': 2, - // 'startSize': 1.2, - // 'sizeAspect': 1.320754716981132, - // 'startColor': [8, [255, 255, 255]], - // 'duration': 2, - // 'gravityModifier': 1, - // 'renderLevel': 'B+', - // }, 'renderer': { 'renderMode': 1, 'anchor': [0.5, 0.5], 'texture': 0 }, - // }, - // }], - // 'meta': { 'previewSize': [750, 1624] }, - // }], - // 'gltf': [], - // images: [], - // 'textures': [{ - // 'minFilter': 9987, - // 'magFilter': 9729, - // 'wrapS': 33071, - // 'wrapT': 33071, - // 'target': 34067, - // 'format': 6408, - // 'internalFormat': 6408, - // 'type': 5121, - // 'mipmaps': [[ - // [20, [5, 0, 24661]], - // [20, [5, 24664, 26074]], - // [20, [5, 50740, 26845]], - // [20, [5, 77588, 24422]], - // [20, [5, 102012, 24461]], - // [20, [5, 126476, 27099]]], - // [[20, [5, 153576, 7699]], [20, [5, 161276, 7819]], [20, [5, 169096, 8919]], [20, [5, 178016, 7004]], [20, [5, 185020, 7657]], [20, [5, 192680, 8515]]], [[20, [5, 201196, 2305]], [20, [5, 203504, 2388]], [20, [5, 205892, 2789]], [20, [5, 208684, 2147]], [20, [5, 210832, 2351]], [20, [5, 213184, 2541]]], [[20, [5, 215728, 755]], [20, [5, 216484, 810]], [20, [5, 217296, 902]], [20, [5, 218200, 727]], [20, [5, 218928, 775]], [20, [5, 219704, 835]]], [[20, [5, 220540, 292]], [20, [5, 220832, 301]], [20, [5, 221136, 317]], [20, [5, 221456, 285]], [20, [5, 221744, 301]], [20, [5, 222048, 307]]], [[20, [5, 222356, 147]], [20, [5, 222504, 147]], [20, [5, 222652, 149]], [20, [5, 222804, 149]], [20, [5, 222956, 149]], [20, [5, 223108, 149]]], [[20, [5, 223260, 96]], [20, [5, 223356, 96]], [20, [5, 223452, 96]], [20, [5, 223548, 97]], [20, [5, 223648, 97]], [20, [5, 223748, 97]]], [[20, [5, 223848, 83]], [20, [5, 223932, 83]], [20, [5, 224016, 83]], [20, [5, 224100, 83]], [20, [5, 224184, 83]], [20, [5, 224268, 83]]]], - // 'sourceType': 7, - // }], - // 'bins': [ - // new ArrayBuffer(1), - // new ArrayBuffer(1), - // new ArrayBuffer(1), - // new ArrayBuffer(1), - // new ArrayBuffer(1), - // new ArrayBuffer(1)], - // 'version': '0.9.0', - // 'shapes': [], - // 'plugins': [], - // 'type': 'mars', - // }; - // const spy = chai.spy(); - // - // await player.loadScene(a).catch(ex => { - // expect(ex.message).to.eql('Error: load texture 0 fails'); - // spy(); - // }); - // expect(spy).to.has.been.called.once; - // }); - it('load scene with different text variables', async () => { - // @ts-expect-error const [composition1, composition2] = await player.loadScene([{ url: 'https://mdn.alipayobjects.com/mars/afts/file/A*_QkURKxu0TEAAAAAAAAAAAAADlB4AQ', options: { @@ -364,14 +193,150 @@ describe('player/scene-load', () => { }, }, }]); - const t1 = composition1.getItemByName('text_908').getComponent(TextComponent); - const t2 = composition2.getItemByName('text_908').getComponent(TextComponent); + const t1 = composition1.getItemByName('text_908')?.getComponent(TextComponent); + const t2 = composition2.getItemByName('text_908')?.getComponent(TextComponent); + + expect(t1?.text).to.eql('ttt'); + expect(t2?.text).to.eql('xxx'); + }); - expect(t1.text).to.eql('ttt'); - expect(t2.text).to.eql('xxx'); + it('success load json object by assetManager', async () => { + const url = 'https://mdn.alipayobjects.com/mars/afts/file/A*PubBSpHUbjYAAAAAAAAAAAAADlB4AQ'; + const [json] = await Promise.all([ + fetch(url).then(res => res.text()), + ]); + const data = JSON.parse(json); + const assetManager = new AssetManager(); + const scene = await assetManager.loadScene(data); + const spy = chai.spy(); + + try { + await player.loadScene(scene); + } catch (e: any) { + spy(); + } + expect(spy).not.to.have.been.called(); + }); + + it('success load multi-data type by assetManager', async () => { + const url1 = 'https://gw.alipayobjects.com/os/gltf-asset/mars-cli/SDNRPIJFENBK/-1998534768-39820.json'; + const url2 = 'https://mdn.alipayobjects.com/mars/afts/file/A*PubBSpHUbjYAAAAAAAAAAAAADlB4AQ'; + const image = 'https://mdn.alipayobjects.com/huamei_klifp9/afts/img/A*ySrfRJvfvfQAAAAAAAAAAAAADvV6AQ/original'; + const [json] = await Promise.all([ + fetch(url1).then(res => res.text()), + ]); + const data = JSON.parse(json); + const assetManager = new AssetManager({ + renderLevel: spec.RenderLevel.S, + variables: { + image, + }, + }); + const scene1 = await assetManager.loadScene(data); + const scene2 = await assetManager.loadScene(url2); + const spy = chai.spy(); + + expect(scene1.renderLevel).to.eql(spec.RenderLevel.S); + expect(scene2.renderLevel).to.eql(spec.RenderLevel.S); + + try { + const [composition1, composition2] = await player.loadScene([scene1, { url: scene2 }], { + speed: 2, + }); + const item = composition2.getItemByName('sprite_1'); + const spriteComponent = item?.getComponent(SpriteComponent); + + expect(spriteComponent?.getTextures()[0].sourceFrom).to.contains({ url: image }); + expect(composition1.getSpeed()).to.eql(2); + expect(composition2.getSpeed()).to.eql(2); + } catch (e: any) { + spy(); + } + expect(spy).not.to.have.been.called(); + }); + + it('load scene with template video', async () => { + const videoUrl = 'https://mdn.alipayobjects.com/huamei_p0cigc/afts/file/A*ZOgXRbmVlsIAAAAAAAAAAAAADoB5AQ'; + const images = [{ + 'id': 'test', + 'template': { + 'width': 126, + 'height': 130, + 'background': { + 'type': spec.BackgroundType.video, + 'name': 'test', + 'url': videoUrl, + }, + }, + 'url': 'https://mdn.alipayobjects.com/mars/afts/img/A*oKwARKdkWhEAAAAAAAAAAAAADlB4AQ/original', + 'webp': 'https://mdn.alipayobjects.com/mars/afts/img/A*eOLVQpT57FcAAAAAAAAAAAAADlB4AQ/original', + 'renderLevel': spec.RenderLevel.BPlus, + }]; + + const composition = await player.loadScene(getJSONWithImages(images)); + const textures = composition.textures; + const videoElement = (textures[0].source as Texture2DSourceOptionsVideo).video; + + expect(textures.length).to.deep.equals(1); + expect(textures[0].source).to.not.be.empty; + expect(videoElement).to.be.an.instanceOf(HTMLVideoElement); + expect(videoElement.src).to.be.equals(videoUrl); + }); + + it('load scene with template video on variables', async () => { + const videoUrl = 'https://gw.alipayobjects.com/v/huamei_p0cigc/afts/video/A*dftzSq2szUsAAAAAAAAAAAAADtN3AQ'; + const images = [{ + 'id': 'test', + 'template': { + 'width': 126, + 'height': 130, + 'background': { + 'type': spec.BackgroundType.video, + 'name': 'test', + 'url': 'https://mdn.alipayobjects.com/huamei_p0cigc/afts/file/A*ZOgXRbmVlsIAAAAAAAAAAAAADoB5AQ', + }, + }, + 'url': 'https://mdn.alipayobjects.com/mars/afts/img/A*oKwARKdkWhEAAAAAAAAAAAAADlB4AQ/original', + 'webp': 'https://mdn.alipayobjects.com/mars/afts/img/A*eOLVQpT57FcAAAAAAAAAAAAADlB4AQ/original', + 'renderLevel': spec.RenderLevel.BPlus, + }]; + + const composition = await player.loadScene(getJSONWithImages(images), { + variables: { + // 视频地址 + test: videoUrl, + }, + }); + const textures = composition.textures; + const videoElement = (textures[0].source as Texture2DSourceOptionsVideo).video; + + expect(videoElement).to.be.an.instanceOf(HTMLVideoElement); + expect(videoElement.src).to.be.equals(videoUrl); + }); + + it('load cube texture', async () => { + const scene = await player.loadScene(cubeTexture1); + const mipmaps = scene.textures[0].taggedProperties.mipmaps as (HTMLImageElement | ImageBitmap)[][]; + + expect(mipmaps.length).to.eql(cubeTexture1.textures[0].mipmaps.length); + expect(mipmaps.every(mipmap => mipmap.every(img => img instanceof HTMLImageElement || img instanceof ImageBitmap))).to.be.true; + }); + + it('load cube textures fail', async () => { + const spy = chai.spy(); + + await player.loadScene(cubeTexture2).catch(e => { + expect(e.message).to.include('Error: Load texture 0 fails'); + spy(); + }); + expect(spy).to.has.been.called.once; }); }); function getJSONWithImageURL (url: string, webp?: string) { return JSON.parse(`{"playerVersion":{"web":"1.3.0","native":"0.0.1.202311221223"},"images":[{"url":"${url}","webp":"${webp ?? url}","renderLevel":"B+"}],"fonts":[],"spines":[],"version":"2.2","shapes":[],"plugins":[],"type":"ge","compositions":[{"id":"2","name":"新建合成2","duration":5,"startTime":0,"endBehavior":1,"previewSize":[750,1624],"items":[{"id":"1","name":"sprite_1","duration":5,"type":"1","visible":true,"endBehavior":0,"delay":0,"renderLevel":"B+","content":{"options":{"startColor":[1,1,1,1]},"renderer":{"renderMode":1,"texture":0},"positionOverLifetime":{"direction":[0,0,0],"startSpeed":0,"gravity":[0,0,0],"gravityOverLifetime":[0,1]},"splits":[[0,0,0.5859375,0.7578125,0]]},"transform":{"position":[0,0,0],"rotation":[0,0,0],"scale":[3.6924,4.7756,1]}}],"camera":{"fov":60,"far":40,"near":0.1,"clipMode":1,"position":[0,0,8],"rotation":[0,0,0]}}],"requires":[],"compositionId":"2","bins":[],"textures":[{"source":0,"flipY":true}]}`); } + +function getJSONWithImages (images: spec.TemplateImage[]) { + return JSON.parse(`{"playerVersion":{"web":"1.2.1","native":"0.0.1.202311221223"},"images":${JSON.stringify(images)},"version":"2.2","type":"ge","compositions":[{"id":"1","name":"新建合成1","duration":5,"startTime":0,"endBehavior":0,"previewSize":[750,1624],"items":[{"id":"1","name":"sprite_1","duration":5,"type":"1","visible":true,"endBehavior":0,"delay":0,"renderLevel":"B+","content":{"options":{"startColor":[0.9529,1,0.0431,1]},"renderer":{"renderMode":1,"texture":0},"positionOverLifetime":{"path":[12,[[[0,0,0,2.3256],[0.43,1,2.3256,3.4483],[0.72,2,3.4483,0]],[[0,0,0],[0,7.79,0],[3.3269,7.79,0]],[[0,1.9475,0],[0,5.8425,0],[0.8317,7.79,0],[2.4952,7.79,0]]]],"direction":[0,0,0],"startSpeed":0,"gravity":[0,0,0],"gravityOverLifetime":[0,1]},"sizeOverLifetime":{"size":[6,[[0.126,1.2055,0,1.6835],[0.72,2.5395,1.6835,0]]]},"colorOverLifetime":{"opacity":[6,[[0,0,0,1.3889],[0.72,1,1.3889,0]]]}},"transform":{"position":[0,0,0],"rotation":[0,0,0],"scale":[1.5492,1.5984,1]}}],"camera":{"fov":60,"far":40,"near":0.1,"clipMode":1,"position":[0,0,8],"rotation":[0,0,0]}}],"requires":[],"compositionId":"1","bins":[],"textures":[{"source":0,"flipY":true}]}`); +}