From a68bb77f9f52094abad08148a3efe8f406f739ca Mon Sep 17 00:00:00 2001 From: hellmor Date: Tue, 9 May 2023 10:15:03 +0800 Subject: [PATCH] feat(sample): samples of animation (#115) Add samples to show how to use skeletal animation and property animation Add sample of how to control the morphtarget animation pdate shadow bias --- samples/animation/Sample_CurveAnimation.ts | 105 +++++++++++++++++ samples/animation/Sample_MorphTarget.ts | 93 ++++++++++++++++ samples/animation/Sample_Skeleton.ts | 75 +++++++++++++ samples/animation/Sample_Skeleton2.ts | 101 +++++++++++++++++ samples/animation/Sample_Skeleton3.ts | 124 +++++++++++++++++++++ src/util/Object3DUtil.ts | 2 +- 6 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 samples/animation/Sample_CurveAnimation.ts create mode 100644 samples/animation/Sample_MorphTarget.ts create mode 100644 samples/animation/Sample_Skeleton.ts create mode 100644 samples/animation/Sample_Skeleton2.ts create mode 100644 samples/animation/Sample_Skeleton3.ts diff --git a/samples/animation/Sample_CurveAnimation.ts b/samples/animation/Sample_CurveAnimation.ts new file mode 100644 index 00000000..3db6f356 --- /dev/null +++ b/samples/animation/Sample_CurveAnimation.ts @@ -0,0 +1,105 @@ +import { Object3D, Scene3D, AnimationCurve, Engine3D, AtmosphericComponent, CameraUtil, HoverCameraController, View3D, DirectLight, KelvinUtil, Keyframe, Object3DUtil, Time } from "@orillusion/core"; + +class Sample_AnimCurve { + lightObj3D: Object3D; + scene: Scene3D; + Duck: Object3D; + curve1: AnimationCurve; + curve2: AnimationCurve; + curve3: AnimationCurve; + curve4: AnimationCurve; + + async run() { + await Engine3D.init({ beforeRender: () => this.renderUpdate() }); + + Engine3D.setting.shadow.shadowBound = 100; + Engine3D.setting.shadow.shadowBias = 0.0001; + Engine3D.setting.shadow.pointShadowBias = 0.6; + Engine3D.setting.shadow.debug = true; + + Engine3D.setting.shadow.autoUpdate = true; + Engine3D.setting.shadow.updateFrameRate = 1; + Engine3D.setting.shadow.type = `HARD`; + + this.scene = new Scene3D(); + this.scene.addComponent(AtmosphericComponent); + + let camera = CameraUtil.createCamera3DObject(this.scene); + camera.perspective(60, Engine3D.aspect, 0.01, 5000.0); + + camera.object3D.addComponent(HoverCameraController).setCamera(-30, -25, 400); + + let view = new View3D(); + view.scene = this.scene; + view.camera = camera; + + Engine3D.startRenderView(view); + + await this.initScene(); + } + + async initScene() { + /******** light *******/ + { + this.lightObj3D = new Object3D(); + this.lightObj3D.rotationX = 15; + this.lightObj3D.rotationY = 110; + this.lightObj3D.rotationZ = 0; + let directLight = this.lightObj3D.addComponent(DirectLight); + directLight.lightColor = KelvinUtil.color_temperature_to_rgb(5355); + directLight.castShadow = true; + directLight.intensity = 10; + this.scene.addChild(this.lightObj3D); + + //create animation curve 1 + this.curve1 = new AnimationCurve(); + this.curve1.addKeyFrame(new Keyframe(0, 1)); + this.curve1.addKeyFrame(new Keyframe(0.5, 2)); + this.curve1.addKeyFrame(new Keyframe(0.7, 2)); + this.curve1.addKeyFrame(new Keyframe(1.0, 1)); + + //create animation curve 2 + + this.curve2 = new AnimationCurve(); + this.curve2.addKeyFrame(new Keyframe(0, 0)); + this.curve2.addKeyFrame(new Keyframe(1, 360)); + + //create animation curve 3 + this.curve3 = new AnimationCurve(); + this.curve3.addKeyFrame(new Keyframe(0, -5)); + this.curve3.addKeyFrame(new Keyframe(0.3, 3)); + this.curve3.addKeyFrame(new Keyframe(0.6, 8)); + this.curve3.addKeyFrame(new Keyframe(0.9, -2)); + this.curve3.addKeyFrame(new Keyframe(1.0, -5)); + + //create animation curve 4 + this.curve4 = new AnimationCurve(); + this.curve4.addKeyFrame(new Keyframe(0, 1)); + this.curve4.addKeyFrame(new Keyframe(0.3, -9)); + this.curve4.addKeyFrame(new Keyframe(0.6, -2)); + this.curve4.addKeyFrame(new Keyframe(0.9, 2)); + this.curve4.addKeyFrame(new Keyframe(1.0, 1)); + + } + + this.scene.addChild(Object3DUtil.GetSingleCube(300, 5, 300, 1, 1, 1)); + + // load a gltf model + this.Duck = (await Engine3D.res.loadGltf('PBR/Duck/Duck.gltf')) as Object3D; + this.Duck.scaleX = this.Duck.scaleY = this.Duck.scaleZ = 0.3; + this.scene.addChild(this.Duck); + } + + renderUpdate() { + //modify animation attribute values to the model + if (this.Duck) { + let time = (Time.time * 0.4 % (1000) / 1000); + this.Duck.y = this.curve1.getValue(time) * 5; + this.Duck.x = this.curve3.getValue(time) * 5; + this.Duck.z = this.curve4.getValue(time) * 5; + this.Duck.rotationY = this.curve2.getValue(time); + } + } +} + +new Sample_AnimCurve().run(); \ No newline at end of file diff --git a/samples/animation/Sample_MorphTarget.ts b/samples/animation/Sample_MorphTarget.ts new file mode 100644 index 00000000..b2b9c5d6 --- /dev/null +++ b/samples/animation/Sample_MorphTarget.ts @@ -0,0 +1,93 @@ +import { GUIHelp } from "@orillusion/debug/GUIHelp"; +import { Object3D, Scene3D, Engine3D, AtmosphericComponent, webGPUContext, HoverCameraController, View3D, DirectLight, KelvinUtil, Vector3, MorphTargetBlender, Entity, CameraUtil } from "@orillusion/core"; +import { GUIUtil } from "@samples/utils/GUIUtil"; + +// Sample of how to control the morphtarget animation +class Sample_MorphTarget { + lightObj3D: Object3D; + scene: Scene3D; + influenceData: { [key: string]: number } = {}; + + async run() { + Engine3D.setting.shadow.shadowBias = 0.0001; + + await Engine3D.init(); + + this.scene = new Scene3D(); + this.scene.addComponent(AtmosphericComponent); + + let camera = CameraUtil.createCamera3DObject(this.scene); + camera.perspective(60, webGPUContext.aspect, 1, 5000.0); + camera.object3D.addComponent(HoverCameraController).setCamera(0, 0, 150); + + this.initDirectLight(); + await this.initMorphModel(); + + let view = new View3D(); + view.scene = this.scene; + view.camera = camera; + + Engine3D.startRenderView(view); + } + + /******** light *******/ + initDirectLight() { + this.lightObj3D = new Object3D(); + this.lightObj3D.rotationX = 21; + this.lightObj3D.rotationY = 108; + this.lightObj3D.rotationZ = 10; + + let directLight = this.lightObj3D.addComponent(DirectLight); + directLight.lightColor = KelvinUtil.color_temperature_to_rgb(5355); + directLight.castShadow = true; + directLight.intensity = 25; + GUIUtil.renderDirLight(directLight, false); + this.scene.addChild(this.lightObj3D); + } + + private async initMorphModel() { + GUIHelp.init(); + + // load lion model + let model = await Engine3D.res.loadGltf('gltfs/glb/lion.glb'); + model.y = -80.0; + model.x = -30.0; + this.scene.addChild(model); + + GUIHelp.addFolder('morph controller'); + // register MorphTargetBlender component + let blendShapeComponent = model.addComponent(MorphTargetBlender); + let targetRenderers = blendShapeComponent.cloneMorphRenderers(); + + // bind influenceData to gui + for (let key in targetRenderers) { + this.influenceData[key] = 0.0; + GUIHelp.add(this.influenceData, key, 0, 1, 0.01).onChange((v) => { + this.influenceData[key] = v; + let list = blendShapeComponent.getMorphRenderersByKey(key); + for (let renderer of list) { + renderer.setMorphInfluence(key, v); + } + }); + } + + GUIHelp.open(); + GUIHelp.endFolder(); + + // print hierarchy + this.printHierarchy(model); + } + + + private printHierarchy(obj: Entity, path: string = '') { + console.log(path, obj.name); + + path += '== '; + for (let child of obj.entityChildren) { + this.printHierarchy(child, path); + } + } + +} + +new Sample_MorphTarget().run(); \ No newline at end of file diff --git a/samples/animation/Sample_Skeleton.ts b/samples/animation/Sample_Skeleton.ts new file mode 100644 index 00000000..565e7bec --- /dev/null +++ b/samples/animation/Sample_Skeleton.ts @@ -0,0 +1,75 @@ +import { GUIHelp } from "@orillusion/debug/GUIHelp"; +import { Object3D, Scene3D, Engine3D, AtmosphericComponent, CameraUtil, webGPUContext, HoverCameraController, View3D, LitMaterial, MeshRenderer, BoxGeometry, DirectLight, KelvinUtil, Object3DUtil } from "@orillusion/core"; +import { GUIUtil } from "@samples/utils/GUIUtil"; + +class Sample_Skeleton { + lightObj3D: Object3D; + scene: Scene3D; + async run() { + + Engine3D.setting.material.materialDebug = false; + Engine3D.setting.material.materialChannelDebug = true; + Engine3D.setting.shadow.autoUpdate = true; + Engine3D.setting.shadow.updateFrameRate = 2; + Engine3D.setting.shadow.shadowBound = 200; + Engine3D.setting.shadow.shadowBias = 0.0001; + + await Engine3D.init(); + + this.scene = new Scene3D(); + this.scene.addComponent(AtmosphericComponent); + + let camera = CameraUtil.createCamera3DObject(this.scene); + camera.perspective(60, Engine3D.aspect, 0.01, 5000.0); + + let ctrl = camera.object3D.addComponent(HoverCameraController); + ctrl.setCamera(0, -45, 100); + ctrl.maxDistance = 1000; + + let view = new View3D(); + view.scene = this.scene; + view.camera = camera; + + Engine3D.startRenderView(view); + + this.initScene(this.scene); + } + + + async initScene(scene: Scene3D) { + GUIHelp.init(); + { + // load model with skeleton animation + let man = await Engine3D.res.loadGltf('gltfs/CesiumMan/CesiumMan_compress.gltf'); + man.scaleX = 30; + man.scaleY = 30; + man.scaleZ = 30; + scene.addChild(man); + } + + /******** floor *******/ + this.scene.addChild(Object3DUtil.GetSingleCube(3000, 1, 3000, 0.5, 0.5, 0.5)); + + /******** light *******/ + { + this.lightObj3D = new Object3D(); + this.lightObj3D.x = 0; + this.lightObj3D.y = 30; + this.lightObj3D.z = -40; + this.lightObj3D.rotationX = 144; + this.lightObj3D.rotationY = 0; + this.lightObj3D.rotationZ = 0; + let directLight = this.lightObj3D.addComponent(DirectLight); + directLight.lightColor = KelvinUtil.color_temperature_to_rgb(5355); + directLight.castShadow = true; + directLight.intensity = 25; + GUIUtil.renderDirLight(directLight); + scene.addChild(this.lightObj3D); + } + + return true; + } + +} + +new Sample_Skeleton().run(); \ No newline at end of file diff --git a/samples/animation/Sample_Skeleton2.ts b/samples/animation/Sample_Skeleton2.ts new file mode 100644 index 00000000..eb22770e --- /dev/null +++ b/samples/animation/Sample_Skeleton2.ts @@ -0,0 +1,101 @@ +import { GUIHelp } from "@orillusion/debug/GUIHelp"; +import { Object3D, Scene3D, Engine3D, AtmosphericComponent, CameraUtil, webGPUContext, HoverCameraController, View3D, LitMaterial, MeshRenderer, BoxGeometry, DirectLight, KelvinUtil, Object3DUtil, SkeletonAnimationComponent } from "@orillusion/core"; +import { GUIUtil } from "@samples/utils/GUIUtil"; + +class Sample_Skeleton2 { + lightObj3D: Object3D; + scene: Scene3D; + + async run() { + + Engine3D.setting.shadow.autoUpdate = true; + Engine3D.setting.shadow.updateFrameRate = 2; + Engine3D.setting.shadow.shadowBound = 500; + Engine3D.setting.shadow.shadowBias = 0.0001; + + await Engine3D.init(); + + this.scene = new Scene3D(); + this.scene.addComponent(AtmosphericComponent); + this.scene.exposure = 1; + + let mainCamera = CameraUtil.createCamera3DObject(this.scene); + mainCamera.perspective(60, webGPUContext.aspect, 1, 3000.0); + + let hoverCameraController = mainCamera.object3D.addComponent(HoverCameraController); + hoverCameraController.setCamera(45, -30, 300); + hoverCameraController.maxDistance = 500.0; + + await this.initScene(this.scene); + + let view = new View3D(); + view.scene = this.scene; + view.camera = mainCamera; + + Engine3D.startRenderView(view); + } + + async initScene(scene: Scene3D) { + { + // load model with skeletion animation + let rootNode = await Engine3D.res.loadGltf('gltfs/glb/Soldier.glb'); + let character = rootNode.getObjectByName('Character') as Object3D; + character.scaleX = 0.3; + character.scaleY = 0.3; + character.scaleZ = 0.3; + character.rotationY = 180; + + // enum animation names + var animName = ['Idel', 'Walk', 'Run', 'TPose']; + let maxCount = 100; + let maxCol = 10; + let maxRow = Math.floor(maxCount / maxCol); + // Clone 100 players to play different animations + for (var i = 0; i < maxCount; i++) { + let cloneObj = character.clone(); + + let row = Math.floor(i / maxCol); + let col = Math.floor(i % maxCol); + + cloneObj.x = (maxCol * -0.5 + col) * 30; + cloneObj.z = (maxRow * -0.5 + row) * 30; + scene.addChild(cloneObj); + + let animation = cloneObj.getComponentsInChild(SkeletonAnimationComponent)[0]; + + if (i < animName.length) { + animation.play(animName[i]); + } else { + let animIndex = Math.floor(Math.random() * 100 % 3); + animation.play(animName[animIndex], -5 + Math.random() * 10); + } + } + } + + /******** floor *******/ + this.scene.addChild(Object3DUtil.GetSingleCube(3000, 1, 3000, 0.5, 0.5, 0.5)); + + /******** light *******/ + { + this.lightObj3D = new Object3D(); + this.lightObj3D.x = 0; + this.lightObj3D.y = 30; + this.lightObj3D.z = -40; + this.lightObj3D.rotationX = 144; + this.lightObj3D.rotationY = 0; + this.lightObj3D.rotationZ = 0; + let directLight = this.lightObj3D.addComponent(DirectLight); + directLight.lightColor = KelvinUtil.color_temperature_to_rgb(5355); + directLight.castShadow = true; + directLight.intensity = 40; + GUIHelp.init(); + GUIUtil.renderDirLight(directLight); + scene.addChild(this.lightObj3D); + } + + return true; + } + +} + +new Sample_Skeleton2().run(); \ No newline at end of file diff --git a/samples/animation/Sample_Skeleton3.ts b/samples/animation/Sample_Skeleton3.ts new file mode 100644 index 00000000..4321b1f9 --- /dev/null +++ b/samples/animation/Sample_Skeleton3.ts @@ -0,0 +1,124 @@ +import { GUIHelp } from "@orillusion/debug/GUIHelp"; +import { Object3D, Scene3D, Engine3D, AtmosphericComponent, CameraUtil, webGPUContext, HoverCameraController, View3D, SkeletonAnimationComponent, LitMaterial, MeshRenderer, BoxGeometry, DirectLight, KelvinUtil, Time, Object3DUtil } from "@orillusion/core"; +import { GUIUtil } from "@samples/utils/GUIUtil"; + +// Sample to use SkeletonAnimationComponent +class Sample_Skeleton3 { + lightObj3D: Object3D; + scene: Scene3D; + + async run() { + Engine3D.setting.shadow.autoUpdate = true; + Engine3D.setting.shadow.updateFrameRate = 1; + Engine3D.setting.shadow.shadowBound = 350; + Engine3D.setting.shadow.shadowBias = 0.0001; + await Engine3D.init({ + renderLoop: () => this.onRenderLoop(), + }); + + GUIHelp.init(); + + this.scene = new Scene3D(); + this.scene.addComponent(AtmosphericComponent); + + let mainCamera = CameraUtil.createCamera3DObject(this.scene); + mainCamera.perspective(60, webGPUContext.aspect, 1, 3000.0); + + let ctrl = mainCamera.object3D.addComponent(HoverCameraController); + ctrl.setCamera(45, -30, 150); + ctrl.maxDistance = 500.0; + + await this.initScene(this.scene); + + let view = new View3D(); + view.scene = this.scene; + view.camera = mainCamera; + + Engine3D.startRenderView(view); + } + + async initScene(scene: Scene3D) { + { + let rootNode = await Engine3D.res.loadGltf('gltfs/glb/Soldier_draco.glb'); + let character = rootNode.getObjectByName('Character') as Object3D; + character.scaleX = 0.3; + character.scaleY = 0.3; + character.scaleZ = 0.3; + character.rotationY = 180; + scene.addChild(character); + + let animation = character.getComponentsInChild(SkeletonAnimationComponent)[0]; + + const runClip = animation.getAnimationClip("Run"); + runClip.addEvent("Begin", 0); + runClip.addEvent("Mid", runClip.totalTime / 2); + runClip.addEvent("End", runClip.totalTime); + + animation.eventDispatcher.addEventListener("Begin", (e: any /*AnimationEvent*/) => { + console.log("Run-Begin", e.skeletonAnimation.getAnimationClipState('Run').time) + }, this); + animation.eventDispatcher.addEventListener("Mid", (e: any /*AnimationEvent*/) => { + console.log("Run-Mid", e.skeletonAnimation.getAnimationClipState('Run').time) + }, this); + animation.eventDispatcher.addEventListener("End", (e: any /*AnimationEvent*/) => { + console.log("Run-End:", e.skeletonAnimation.getAnimationClipState('Run').time) + }, this); + + // change speed + GUIHelp.addFolder("Animation-speed").open(); + GUIHelp.add(animation, 'timeScale', -6, 6, 0.01); + GUIHelp.endFolder(); + + // change animation weight + GUIHelp.addFolder("Animation-weight").open(); + animation.getAnimationClipStates().forEach((clipState, _) => { + GUIHelp.add(clipState, 'weight', 0, 1.0, 0.01).name(clipState.name); + }); + GUIHelp.endFolder(); + + // toggle play/stop + GUIHelp.addFolder("Animation-play").open(); + animation.getAnimationClipStates().forEach((clipState, _) => { + GUIHelp.addButton(clipState.name, () => animation.play(clipState.name)); + }); + GUIHelp.endFolder(); + + // cross fade animation + GUIHelp.addFolder("Animation-crossFade").open(); + animation.getAnimationClipStates().forEach((clipState, _) => { + GUIHelp.addButton('crossFade(' + clipState.name + ')', () => animation.crossFade(clipState.name, 0.3)); + }); + GUIHelp.endFolder(); + } + + /******** floor *******/ + this.scene.addChild(Object3DUtil.GetSingleCube(3000, 1, 3000, 0.5, 0.5, 0.5)); + + /******** light *******/ + { + this.lightObj3D = new Object3D(); + this.lightObj3D.x = 0; + this.lightObj3D.y = 30; + this.lightObj3D.z = -40; + this.lightObj3D.rotationX = 45; + this.lightObj3D.rotationY = 0; + this.lightObj3D.rotationZ = 0; + let directLight = this.lightObj3D.addComponent(DirectLight); + directLight.lightColor = KelvinUtil.color_temperature_to_rgb(5355); + directLight.castShadow = true; + directLight.intensity = 40; + GUIUtil.renderDirLight(directLight); + scene.addChild(this.lightObj3D); + } + + return true; + } + + public onRenderLoop() { + if (this.lightObj3D) { + this.lightObj3D.rotationY += Time.delta * 0.01 * 2; + } + } +} + +new Sample_Skeleton3().run(); \ No newline at end of file diff --git a/src/util/Object3DUtil.ts b/src/util/Object3DUtil.ts index 61dae2c5..52d54283 100644 --- a/src/util/Object3DUtil.ts +++ b/src/util/Object3DUtil.ts @@ -121,7 +121,7 @@ export class Object3DUtil { return obj; } - public static getSinglepCube(mat: MaterialBase, size: number = 10) { + public static GetSingleCube2(mat: MaterialBase, size: number = 10) { this.initHeap(); let obj = new Object3D();