Merge branch 'main' into dev/1.2

* main:
  Delete ignore file (#1917)
  Ignore git-lfs husky sh (#1916)
  "v1.1.0-beta.30"
  Fix component disable error when  do `enabled = false` in `onAwake` method (#1915)
  Add e2e test (#1656)
  "v1.1.0-beta.29"
  Fix text renderer wrap error (#1914)
  "v1.1.0-beta.28"
  feat(material): temporary resolution of submesh rendering order (#1910)
  fix: unit test (#1909)
  Animator events support be added in real time (#1906)
  editor: parse layer (#1907)

# Conflicts:
#	package.json
#	packages/core/package.json
#	packages/design/package.json
#	packages/draco/package.json
#	packages/galacean/package.json
#	packages/loader/package.json
#	packages/math/package.json
#	packages/physics-lite/package.json
#	packages/physics-physx/package.json
#	packages/rhi-webgl/package.json
#	packages/shader-lab/package.json
#	pnpm-lock.yaml
#	tests/src/core/Animator.test.ts
This commit is contained in:
GuoLei1990
2023-12-27 17:25:09 +08:00
51 changed files with 2634 additions and 74 deletions

3
.gitattributes vendored
View File

@@ -1,3 +1,4 @@
# We'll let Git's auto-detection algorithm infer if a file is text. If it is,
# enforce LF line endings regardless of OS or git configurations.
* text=auto eol=lf
* text=auto eol=lf
e2e/fixtures/** filter=lfs diff=lfs merge=lfs -text

View File

@@ -48,6 +48,8 @@ The following is a set of guidelines for contributing to Galacean. Please spend
- Clone the Galacean playground repository and write a demo for your change.
- Write an uint test in the Galacean repository and run `npm run test` to execute the uint test.
- [Write an e2e test](https://github.com/galacean/runtime/wiki/How-to-write-an-e2e-Test-for-runtime) in the Galacean repository and run `npm run e2e` to execute the e2e test.
### Submitting a Pull Request
@@ -142,4 +144,4 @@ git pull --ff upstream master
## Credits
<br />Thank you to all the people who have already contributed to Galacean!<br />
<br />// WIP: Contributors
<br />// WIP: Contributors

View File

@@ -73,3 +73,53 @@ jobs:
- name: Upload coverage to Codecov
run: ./node_modules/.bin/codecov
- run: curl -s https://codecov.io/bash
e2e:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
with:
lfs: true
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
- name: Install dependencies
run: pnpm i
- name: Build
run: npm run build
- name: Run Cypress Tests
uses: cypress-io/github-action@v5
with:
start: npm run e2e:case
wait-on: 'http://localhost:5175'
wait-on-timeout: 120
browser: chrome
- name: Upload Diff
if: failure()
uses: actions/upload-artifact@v3
with:
name: cypress-diff
path: e2e/diff/
- name: Upload Origin
if: failure()
uses: actions/upload-artifact@v3
with:
name: cypress-origin
path: e2e/fixtures/originImage
- name: Upload Screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: cypress-screenshots
path: e2e/screenshots/

9
.gitignore vendored
View File

@@ -7,6 +7,7 @@ tmp
/packages/*/types
/packages/*/doc
/tests/node_modules
/e2e/node_modules
/playground/node_modules
types
/packages/*/test/fixtures/**/node_modules
@@ -25,3 +26,11 @@ stats.html
tsconfig.tsbuildinfo
api
yarn.lock
e2e/videos/*
e2e/screenshots/*
e2e/diff/*
e2e/.dev/mpa
.husky/post-checkout
.husky/post-commit
.husky/pre-push
.husky/post-merge

View File

@@ -75,6 +75,16 @@ Everyone is welcome to join us! Whether you find a bug, have a great feature req
Make sure to read the [Contributing Guide](.github/HOW_TO_CONTRIBUTE.md) / [贡献指南](https://github.com/galacean/engine/wiki/%E5%A6%82%E4%BD%95%E4%B8%8E%E6%88%91%E4%BB%AC%E5%85%B1%E5%BB%BA-Oasis-%E5%BC%80%E6%BA%90%E4%BA%92%E5%8A%A8%E5%BC%95%E6%93%8E) before submitting changes.
## Clone
Prerequisites:
- [git-lfs](https://git-lfs.com/) (Install by official website)
Clone this repository:
```sh
git clone git@github.com:galacean/runtime.git
```
## Build
Prerequisites:

71
cypress.config.ts Normal file
View File

@@ -0,0 +1,71 @@
import { defineConfig } from "cypress";
import { compare } from "odiff-bin";
const path = require("path");
const fs = require("fs-extra");
const downloadDirectory = path.join(__dirname, "e2e/downloads");
let isRunningInCommandLine = false;
export default defineConfig({
e2e: {
viewportWidth: 1200,
viewportHeight: 800,
baseUrl: "http://localhost:5175",
defaultCommandTimeout: 60000,
fileServerFolder: "e2e",
supportFile: "e2e/support/e2e.ts",
fixturesFolder: "e2e/fixtures",
screenshotsFolder: "e2e/screenshots",
videosFolder: "e2e/videos",
specPattern: "e2e/tests/*.cy.ts",
setupNodeEvents(on, config) {
// implement node event listeners here
on("before:browser:launch", (browser, launchOptions) => {
console.log("launching browser %s is headless? %s", browser.name, browser.isHeadless);
// supply the absolute path to an unpacked extension's folder
// NOTE: extensions cannot be loaded in headless Chrome
if (fs.existsSync("e2e/diff")) {
fs.rmdirSync("e2e/diff", { recursive: true });
}
if (browser.name === "chrome") {
launchOptions.preferences.default["download"] = {
default_directory: downloadDirectory
};
}
if (browser.isHeadless) {
isRunningInCommandLine = true;
}
launchOptions.args.push("--force-device-scale-factor=1");
return launchOptions;
}),
on("task", {
async compare({ fileName, options }) {
fileName += ".png";
const baseFolder = "e2e/fixtures/originImage/";
const newFolder = path.join("e2e/screenshots", isRunningInCommandLine ? options.specFolder : "");
const diffFolder = path.join("e2e/diff", options.specFolder);
if (!fs.existsSync(diffFolder)) {
fs.mkdirSync(diffFolder, { recursive: true });
}
const baseImage = path.join(baseFolder, fileName);
const newImage = path.join(newFolder, fileName);
const diffImage = path.join(diffFolder, fileName);
console.log("comparing base image %s to the new image %s", baseImage, newImage);
if (options) {
console.log("odiff options %o", options);
}
const started = +new Date();
const result = await compare(baseImage, newImage, diffImage, options);
const finished = +new Date();
const elapsed = finished - started;
console.log("odiff took %dms", elapsed);
console.log(result);
return result;
}
});
}
},
chromeWebSecurity: false
});

BIN
e2e/.dev/AlibabaSans.ttf Normal file

Binary file not shown.

39
e2e/.dev/index.html Normal file
View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Galacean Case</title>
</head>
<body>
<div class="container">
<div class="header">
<a class="logo" href="https://galacean.antgroup.com/" target="Home">
<img
src="https://mdn.alipayobjects.com/huamei_qbugvr/afts/img/A*ppQsSphM7uUAAAAAAAAAAAAADtKFAQ/original"
alt=""
/>
<span> Galacean </span>
</a>
</div>
<input
placeholder="search..."
class="search-bar"
id="searchBar"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
<div class="nav-left">
<div class="item-list" id="itemList"></div>
</div>
<div class="nav-right">
<iframe id="iframe" allowfullscreen src="" frameborder="0"></iframe>
</div>
</div>
<script type="module" src="./index.js"></script>
</body>
</html>

76
e2e/.dev/index.js Normal file
View File

@@ -0,0 +1,76 @@
import './index.sass';
import demoList from './mpa/.demoList.json';
const itemListDOM = document.getElementById('itemList');
const searchBarDOM = document.getElementById('searchBar');
const iframe = document.getElementById('iframe');
const items = []; // itemDOM,label
Object.keys(demoList).forEach((group) => {
const demos = demoList[group];
const groupDOM = document.createElement('div');
const demosDOM = document.createElement('ul');
itemListDOM.appendChild(groupDOM);
groupDOM.appendChild(demosDOM);
demos.forEach((item) => {
const { label, src } = item;
const itemDOM = document.createElement('a');
itemDOM.innerHTML = src;
itemDOM.title = `${src}`;
itemDOM.onclick = function () {
clickItem(itemDOM);
};
demosDOM.appendChild(itemDOM);
items.push({
itemDOM,
label,
src,
});
});
});
searchBarDOM.oninput = () => {
updateFilter(searchBar.value);
};
function updateFilter(value) {
const reg = new RegExp(value, 'i');
items.forEach(({ itemDOM, label, src }) => {
reg.lastIndex = 0;
if (reg.test(label) || reg.test(src)) {
itemDOM.classList.remove('hide');
} else {
itemDOM.classList.add('hide');
}
});
}
function clickItem(itemDOM) {
window.location.hash = `#mpa/${itemDOM.title}`;
}
function onHashChange() {
const hashPath = window.location.hash.split('#')[1];
if (!hashPath) {
clickItem(items[0].itemDOM);
return;
}
iframe.src = hashPath + '.html';
items.forEach(({ itemDOM }) => {
const itemPath = `mpa/${itemDOM.title}`;
if (itemPath === hashPath) {
itemDOM.classList.add('active');
} else {
itemDOM.classList.remove('active');
}
});
}
window.onhashchange = onHashChange;
// init
onHashChange();

97
e2e/.dev/index.sass Normal file
View File

@@ -0,0 +1,97 @@
$header-height: 3.5rem
$header-padding: 1rem
$container-gap: 2rem
$transition-duration: 0.5s
*
box-sizing: border-box
html, body
margin: 0
width: 100%
height: 100%
overflow: hidden
body
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif
font-size: 16px
a
text-decoration: none
color: inherit
display: inline-block
.container
display: flex
height: 100%
user-select: none
.header
position: absolute
top: 0
left: 0
height: $header-height
padding: $header-padding $container-gap
.logo
height: $header-height - 2 * $header-padding
display: flex
align-items: center
img
height: 100%
span
font-weight: bold
padding-left: 10px
font-size: 1.2em
.search-bar
position: absolute
top: $header-height
left: 1rem
padding: $header-padding $container-gap
background: #fff url("//gw-office.alipayobjects.com/basement_prod/225a73b9-1281-4388-9555-1e671899bc25.svg") .6rem .55rem no-repeat
cursor: text
width: 10rem
height: 2rem
color: #666
display: inline-block
border: 1px solid #ccc
border-radius: 2rem
font-size: .9rem
line-height: 2rem
outline: none
.nav-left
display: flex
flex-direction: column
overflow: auto
padding: 1rem $container-gap
margin-top: $header-height + 2rem
.item-list
.hide
height: 0
ul
padding-left: 20px
margin: 0
.title
font-weight: bold
cursor: default
font-size: 1.2em
a
height: 2rem
line-height: 2rem
cursor: pointer
transition-property: color,height
transition-duration: $transition-duration
overflow: hidden
display: block
&.active
text-decoration: underline
color: #096dd9
&:hover
color: #096dd9
.nav-right
flex: 1
box-shadow: rgb(125 121 121) -2px 0px 8px
iframe
width: 100%
height: 100%

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title %></title>
<style>
::-webkit-scrollbar {
width: 0px; /* For vertical scrollbars */
height: 0px; /* For horizontal scrollbars */
}
@font-face {
font-family: "AlibabaSans";
src: url("../AlibabaSans.ttf");
}
html,
body,
canvas {
width: 1200px;
height: 800px;
padding: 0;
margin: 0;
overflow: hidden;
background: black;
border: none;
}
.dg.main > ul {
overflow: initial;
}
div {
font-family: "AlibabaSans";
font-size: 20px;
color: white;
position: absolute;
top: 0;
left: 0;
z-index: 100;
}
</style>
<link rel="preload" href="../AlibabaSans.ttf" as="font" type="font/ttf" crossorigin="anonymous">
</head>
<body>
<canvas id="canvas"></canvas>
<script type="module" src="<%= url %>"></script>
<script>
document.oncontextmenu = (e) => {
e.preventDefault();
};
</script>
</body>
</html>

76
e2e/.dev/vite.config.js Normal file
View File

@@ -0,0 +1,76 @@
const path = require("path");
const fs = require("fs-extra");
const OUT_PATH = "mpa";
const templateStr = fs.readFileSync(path.join(__dirname, "template/iframe.ejs"), "utf8");
// 替换 ejs 模版格式的字符串,如 <%= title %>: templateStr.replaceEJS("title","replaced title");
String.prototype.replaceEJS = function (regStr, replaceStr) {
return this.replace(new RegExp(`<%=\\s*${regStr}\\s*%>`, "g"), replaceStr);
};
// clear mpa
fs.emptyDirSync(path.resolve(__dirname, OUT_PATH));
// create mpa
const demoList = fs
.readdirSync(path.join(__dirname, "../case"))
.filter((name) => /.ts$/.test(name) && name.indexOf(".") !== 0)
.map((name) => {
return {
file: name.split(".ts")[0]
};
});
demoList.forEach(({ file }) => {
const ejs = templateStr.replaceEJS("url", `./${file}.ts`);
fs.outputFileSync(path.resolve(__dirname, OUT_PATH, file + ".ts"), `import "../../case/${file}"`);
fs.outputFileSync(path.resolve(__dirname, OUT_PATH, file + ".html"), ejs);
});
// output demolist
const demoSorted = {};
demoList.forEach(({ file }) => {
if (!demoSorted[file]) {
demoSorted[file] = [];
}
demoSorted[file].push({
src: file
});
});
fs.outputJSONSync(path.join(__dirname, OUT_PATH, ".demoList.json"), demoSorted);
module.exports = {
server: {
open: true,
host: "0.0.0.0",
port: 5175
},
resolve: {
dedupe: ["@galacean/engine"]
},
optimizeDeps: {
exclude: [
"@galacean/engine",
"@galacean/engine-draco",
"@galacean/engine-lottie",
"@galacean/engine-spine",
"@galacean/tools-baker",
"@galacean/engine-toolkit",
"@galacean/engine-toolkit-auxiliary-lines",
"@galacean/engine-toolkit-controls",
"@galacean/engine-toolkit-framebuffer-picker",
"@galacean/engine-toolkit-gizmo",
"@galacean/engine-toolkit-lines",
"@galacean/engine-toolkit-outline",
"@galacean/engine-toolkit-planar-shadow-material",
"@galacean/engine-toolkit-skeleton-viewer",
"@galacean/engine-toolkit-grid-material",
"@galacean/engine-toolkit-navigation-gizmo",
"@galacean/engine-toolkit-geometry-sketch",
"@galacean/engine-toolkit-stats",
"@galacean/engine-toolkit-input-logger"
]
}
};

20
e2e/case/.mockForE2E.ts Normal file
View File

@@ -0,0 +1,20 @@
export const updateForE2E = (engine, deltaTime = 100) => {
engine._vSyncCount = Infinity;
engine._time._lastSystemTime = 0;
let times = 0;
performance.now = function () {
times++;
return times * deltaTime;
};
for (let i = 0; i < 10; ++i) {
engine.update();
}
};
export const e2eReady = () => {
setTimeout(() => {
const text = document.createElement("div");
text.className = "cypressReady";
document.body.appendChild(text);
}, 1000);
}

View File

@@ -0,0 +1,68 @@
/**
* @title Animation Additive
* @category Animation
*/
import {
Animator,
AnimatorControllerLayer,
AnimatorLayerBlendingMode,
AnimatorStateMachine,
Camera,
DirectLight,
GLTFResource,
Logger,
SystemInfo,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { OrbitControl } from "@galacean/engine-toolkit";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb")
.then((gltfResource) => {
const { animations = [], defaultSceneRoot } = gltfResource;
const animator = defaultSceneRoot.getComponent(Animator);
const { animatorController } = animator;
const animatorStateMachine = new AnimatorStateMachine();
const additiveLayer = new AnimatorControllerLayer("additiveLayer");
additiveLayer.stateMachine = animatorStateMachine;
additiveLayer.blendingMode = AnimatorLayerBlendingMode.Additive;
animatorController.addLayer(additiveLayer);
const additivePoseNames = animations.filter((clip) => clip.name.includes("pose")).map((clip) => clip.name);
additivePoseNames.forEach((name) => {
const clip = animator.findAnimatorState(name).clip;
const newState = animatorStateMachine.addState(name);
newState.clipStartTime = 1;
newState.clip = clip;
});
rootEntity.addChild(defaultSceneRoot);
animator.play("walk", 0);
animator.play("sad_pose", 1);
updateForE2E(engine);
e2eReady();
});
});

View File

@@ -0,0 +1,49 @@
/**
* @title Animation BlendShape
* @category Animation
*/
import { OrbitControl } from "@galacean/engine-toolkit";
import {
Animator,
Camera,
DirectLight,
Logger,
SkinnedMeshRenderer,
SystemInfo,
Vector3,
WebGLEngine,
GLTFResource
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 1.0;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(-45, -135, 0));
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/746da3e3-fdc9-4155-8fee-0e2a97de4e72.glb")
.then((asset) => {
const { defaultSceneRoot } = asset;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
const skinMeshRenderer = defaultSceneRoot.getComponent(SkinnedMeshRenderer);
skinMeshRenderer.blendShapeWeights[0] = 1.0;
animator.play("TheWave");
updateForE2E(engine);
e2eReady();
});
});

View File

@@ -0,0 +1,47 @@
/**
* @title Animation Play
* @category Animation
*/
import { OrbitControl } from "@galacean/engine-toolkit";
import {
Animator,
Camera,
DirectLight,
Logger,
SystemInfo,
Vector3,
WebGLEngine,
GLTFResource
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb")
.then((gltfResource) => {
const { animations = [], defaultSceneRoot } = gltfResource;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
animator.play("agree");
updateForE2E(engine, 30);
animator.crossFade("walk", 0.5, 0, 0);
updateForE2E(engine, 30);
e2eReady();
});
});

View File

@@ -0,0 +1,188 @@
/**
* @title Animation CustomAnimationClip
* @category Animation
*/
import {
AnimationClip,
AnimationColorCurve,
AnimationFloatCurve,
AnimationVector3Curve,
Animator,
AnimatorController,
AnimatorControllerLayer,
AnimatorStateMachine,
Camera,
Color,
DirectLight,
GLTFResource,
Keyframe,
Logger,
SpotLight,
SystemInfo,
Transform,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraWrap = rootEntity.createChild("camera_wrap");
const cameraEntity = cameraWrap.createChild("camera");
cameraEntity.transform.position = new Vector3(0, 3, 8);
cameraEntity.transform.rotation = new Vector3(-2, 0, 0);
cameraEntity.addComponent(Camera);
const lightWrap = rootEntity.createChild("light_wrap");
const directLightEntity = lightWrap.createChild("light_node");
const directLight = directLightEntity.addComponent(DirectLight);
directLight.intensity = 0.6;
directLightEntity.transform.lookAt(new Vector3(0, 0, 1));
directLightEntity.transform.rotate(new Vector3(0, 90, 0));
const spotLightEntity = lightWrap.createChild("spotLight1");
const spotLightEntity2 = spotLightEntity.clone();
spotLightEntity2.name = "spotLight2";
spotLightEntity2.transform.setRotation(-120, 0, 0);
lightWrap.addChild(spotLightEntity2);
const spotLight = spotLightEntity.addComponent(SpotLight);
spotLight.angle = Math.PI / 60;
spotLightEntity.transform.setPosition(0, 8, 0);
spotLightEntity.transform.setRotation(-60, 0, 0);
const spotLight2 = spotLightEntity2.addComponent(SpotLight);
spotLight2.angle = Math.PI / 60;
spotLightEntity2.transform.setPosition(0, 8, 0);
spotLightEntity2.transform.setRotation(-60, 0, 0);
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/OasisHub/244228a7-361c-4c63-a790-dd9e19d12e78/data.gltf")
.then((gltfResource) => {
const { defaultSceneRoot, animations = [] } = gltfResource;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
const sceneAnimator = rootEntity.addComponent(Animator);
sceneAnimator.animatorController = new AnimatorController();
const layer = new AnimatorControllerLayer("base");
sceneAnimator.animatorController.addLayer(layer);
const stateMachine = (layer.stateMachine = new AnimatorStateMachine());
const sceneState = stateMachine.addState("sceneAnim");
const sceneClip = (sceneState.clip = new AnimationClip("sceneAnim"));
//custom rotate curve
const rotateCurve = new AnimationVector3Curve();
const key1 = new Keyframe<Vector3>();
key1.time = 0;
key1.value = new Vector3(0, 0, 0);
const key2 = new Keyframe<Vector3>();
key2.time = 15;
key2.value = new Vector3(0, 360, 0);
rotateCurve.addKey(key1);
rotateCurve.addKey(key2);
//custom color curve
const colorCurve = new AnimationColorCurve();
const key3 = new Keyframe<Color>();
key3.time = 0;
key3.value = new Color(1, 0, 0, 1);
const key4 = new Keyframe<Color>();
key4.time = 5;
key4.value = new Color(0, 1, 0, 1);
const key5 = new Keyframe<Color>();
key5.time = 10;
key5.value = new Color(0, 0, 1, 1);
const key6 = new Keyframe<Color>();
key6.time = 15;
key6.value = new Color(1, 0, 0, 1);
colorCurve.addKey(key3);
colorCurve.addKey(key4);
colorCurve.addKey(key5);
colorCurve.addKey(key6);
const color2Curve = new AnimationColorCurve();
const key16 = new Keyframe<Color>();
key16.time = 0;
key16.value = new Color(0, 0, 1, 1);
const key17 = new Keyframe<Color>();
key17.time = 5;
key17.value = new Color(0, 1, 0, 1);
const key18 = new Keyframe<Color>();
key18.time = 10;
key18.value = new Color(1, 0, 0, 1);
const key19 = new Keyframe<Color>();
key19.time = 15;
key19.value = new Color(0, 0, 1, 1);
color2Curve.addKey(key16);
color2Curve.addKey(key17);
color2Curve.addKey(key18);
color2Curve.addKey(key19);
//custom fov curve
const fovCurve = new AnimationFloatCurve();
const key7 = new Keyframe<number>();
key7.time = 0;
key7.value = 45;
const key8 = new Keyframe<number>();
key8.time = 8;
key8.value = 80;
const key9 = new Keyframe<number>();
key9.time = 15;
key9.value = 45;
fovCurve.addKey(key7);
fovCurve.addKey(key8);
fovCurve.addKey(key9);
//custom spotLight1 rotate curve
const spotLight1RotateCurve = new AnimationVector3Curve();
const key10 = new Keyframe<Vector3>();
key10.time = 0;
key10.value = new Vector3(-60, 0, 0);
const key11 = new Keyframe<Vector3>();
key11.time = 10;
key11.value = new Vector3(-120, 0, 0);
const key12 = new Keyframe<Vector3>();
key12.time = 15;
key12.value = new Vector3(-60, 0, 0);
spotLight1RotateCurve.addKey(key10);
spotLight1RotateCurve.addKey(key11);
spotLight1RotateCurve.addKey(key12);
//custom spotLight2 rotate curve
const spotLight2RotateCurve = new AnimationVector3Curve();
const key13 = new Keyframe<Vector3>();
key13.time = 0;
key13.value = new Vector3(-120, 0, 0);
const key14 = new Keyframe<Vector3>();
key14.time = 10;
key14.value = new Vector3(-60, 0, 0);
const key15 = new Keyframe<Vector3>();
key15.time = 15;
key15.value = new Vector3(-120, 0, 0);
spotLight2RotateCurve.addKey(key13);
spotLight2RotateCurve.addKey(key14);
spotLight2RotateCurve.addKey(key15);
sceneClip.addCurveBinding("/light_wrap/spotLight1", SpotLight, "color", colorCurve);
sceneClip.addCurveBinding("/light_wrap/spotLight1", Transform, "rotation", spotLight1RotateCurve);
sceneClip.addCurveBinding("/light_wrap/spotLight2", Transform, "rotation", spotLight2RotateCurve);
sceneClip.addCurveBinding("/light_wrap/spotLight2", SpotLight, "color", color2Curve);
sceneClip.addCurveBinding("/light_wrap", Transform, "rotation", rotateCurve);
// curve can be reused
sceneClip.addCurveBinding("/camera_wrap", Transform, "rotation", rotateCurve);
sceneClip.addCurveBinding("/camera_wrap/camera", Camera, "fieldOfView", fovCurve);
sceneAnimator.play("sceneAnim", 0);
animator.play(animations[0].name, 0);
updateForE2E(engine, 500);
e2eReady();
});
});

View File

@@ -0,0 +1,98 @@
/**
* @title Animation CustomBlendShape
* @category Animation
*/
import { OrbitControl } from "@galacean/engine-toolkit";
import {
AnimationClip,
AnimationFloatArrayCurve,
Animator,
AnimatorController,
AnimatorControllerLayer,
AnimatorStateMachine,
BlendShape,
Camera,
Keyframe,
Logger,
ModelMesh,
SkinnedMeshRenderer,
SystemInfo,
UnlitMaterial,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("cameraNode");
cameraEntity.transform.position = new Vector3(0, 0, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl);
const meshEntity = rootEntity.createChild("meshEntity");
const skinnedMeshRenderer = meshEntity.addComponent(SkinnedMeshRenderer);
const modelMesh = new ModelMesh(engine);
// Set vertices data.
const positions = [
new Vector3(-1.0, -1.0, 1.0),
new Vector3(1.0, -1.0, 1.0),
new Vector3(1.0, 1.0, 1.0),
new Vector3(1.0, 1.0, 1.0),
new Vector3(-1.0, 1.0, 1.0),
new Vector3(-1.0, -1.0, 1.0)
];
modelMesh.setPositions(positions);
// Add SubMesh.
modelMesh.addSubMesh(0, 6);
// Add BlendShape.
const deltaPositions = [
new Vector3(0.0, 0.0, 0.0),
new Vector3(0.0, 0.0, 0.0),
new Vector3(-1.0, 0.0, 0.0),
new Vector3(-1.0, 0.0, 0.0),
new Vector3(1.0, 0.0, 0.0),
new Vector3(0.0, 0.0, 0.0)
];
const blendShape = new BlendShape("BlendShapeA");
blendShape.addFrame(1.0, deltaPositions);
modelMesh.addBlendShape(blendShape);
skinnedMeshRenderer.mesh = modelMesh;
skinnedMeshRenderer.setMaterial(new UnlitMaterial(engine));
// Upload data.
modelMesh.uploadData(false);
const animator = meshEntity.addComponent(Animator);
animator.animatorController = new AnimatorController();
const layer = new AnimatorControllerLayer("base");
animator.animatorController.addLayer(layer);
const stateMachine = (layer.stateMachine = new AnimatorStateMachine());
const state = stateMachine.addState("blendShape");
const clip = (state.clip = new AnimationClip("blendShape"));
//custom blendShape curve
const blendShapeCurve = new AnimationFloatArrayCurve();
const key1 = new Keyframe<Float32Array>();
key1.time = 0;
key1.value = new Float32Array([0]);
const key2 = new Keyframe<Float32Array>();
key2.time = 5;
key2.value = new Float32Array([1]);
blendShapeCurve.addKey(key1);
blendShapeCurve.addKey(key2);
clip.addCurveBinding("", SkinnedMeshRenderer, "blendShapeWeights", blendShapeCurve);
animator.play("blendShape");
updateForE2E(engine, 1000);
e2eReady();
});

View File

@@ -0,0 +1,83 @@
/**
* @title Animation Event
* @category Animation
*/
import { OrbitControl } from "@galacean/engine-toolkit";
import * as dat from "dat.gui";
import {
AnimationEvent,
Animator,
Camera,
DirectLight,
Font,
FontStyle,
GLTFResource,
Script,
SystemInfo,
TextRenderer,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
const engine = await WebGLEngine.create({ canvas: "canvas" });
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
console.log(99, SystemInfo.devicePixelRatio);
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
// initText
const textEntity = rootEntity.createChild("text");
const textRenderer = textEntity.addComponent(TextRenderer);
textEntity.transform.setPosition(0, 2, 0);
textRenderer.fontSize = 12;
textRenderer.font = Font.createFromOS(engine, "AlibabaSans");
textRenderer.text = "";
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb")
.then((gltfResource) => {
const { defaultSceneRoot, animations } = gltfResource;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
const state = animator.findAnimatorState("walk");
const clip = state.clip;
const event0 = new AnimationEvent();
event0.functionName = "event0";
event0.time = 0.5;
clip.addEvent(event0);
const event1 = new AnimationEvent();
event1.functionName = "event1";
event1.time = clip.length;
clip.addEvent(event1);
defaultSceneRoot.addComponent(
class extends Script {
event0(): void {
textRenderer.text = "0";
}
event1(): void {
textRenderer.text = "1";
}
}
);
animator.play("walk", 0);
updateForE2E(engine, 500);
e2eReady();
});

46
e2e/case/animator-play.ts Normal file
View File

@@ -0,0 +1,46 @@
/**
* @title Animation Play
* @category Animation
*/
import {
Animator,
Camera,
DirectLight,
GLTFResource,
Logger,
SystemInfo,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { OrbitControl } from "@galacean/engine-toolkit";
import { updateForE2E, e2eReady } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb")
.then((gltfResource) => {
const { defaultSceneRoot } = gltfResource;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
animator.play("agree");
updateForE2E(engine);
e2eReady();
});
});

View File

@@ -0,0 +1,69 @@
/**
* @title Animation Reuse
* @category Animation
*/
import {
Animator,
AssetPromise,
Camera,
DirectLight,
GLTFResource,
Logger,
SystemInfo,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { OrbitControl } from "@galacean/engine-toolkit";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
const promises: AssetPromise<GLTFResource>[] = [];
// origin model
promises.push(
engine.resourceManager.load<GLTFResource>(
"https://gw.alipayobjects.com/os/OasisHub/6f5b1918-1380-4641-a57a-7507503a524c/data.gltf"
)
);
// animation
promises.push(
engine.resourceManager.load<GLTFResource>(
"https://gw.alipayobjects.com/os/OasisHub/9ef53086-67d4-4be6-bff8-449a8074a5bd/data.gltf"
)
);
Promise.all(promises).then((resArr) => {
const modelGLTF = resArr[0];
const animationGLTF = resArr[1];
const { animations: originAnimations = [] } = modelGLTF;
const { animations = [] } = animationGLTF;
const { defaultSceneRoot } = modelGLTF;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
const danceState = animator.animatorController.layers[0].stateMachine.addState("dance");
danceState.clip = animations[0];
animator.play("dance");
const animationNames = originAnimations.map((clip) => clip.name);
animationNames.push("dance");
updateForE2E(engine);
e2eReady();
});
});

View File

@@ -0,0 +1,82 @@
/**
* @title AnimatorStateScript
* @category Animation
*/
import { OrbitControl } from "@galacean/engine-toolkit";
import {
Animator,
AnimatorState,
Camera,
DirectLight,
Font,
FontStyle,
GLTFResource,
Logger,
StateMachineScript,
SystemInfo,
TextRenderer,
Vector3,
WebGLEngine
} from "@galacean/engine";
import { e2eReady, updateForE2E } from "./.mockForE2E";
Logger.enable();
WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize(2);
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 1, 5);
cameraEntity.addComponent(Camera);
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0);
const lightNode = rootEntity.createChild("light_node");
lightNode.addComponent(DirectLight).intensity = 0.6;
lightNode.transform.lookAt(new Vector3(0, 0, 1));
lightNode.transform.rotate(new Vector3(0, 90, 0));
// initText
const textEntity = rootEntity.createChild("text");
const textRenderer = textEntity.addComponent(TextRenderer);
textEntity.transform.setPosition(0, 2, 0);
textRenderer.fontSize = 12;
textRenderer.font = Font.createFromOS(engine, "AlibabaSans");
textRenderer.text = "";
engine.resourceManager
.load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb")
.then((gltfResource) => {
const { animations = [], defaultSceneRoot } = gltfResource;
rootEntity.addChild(defaultSceneRoot);
const animator = defaultSceneRoot.getComponent(Animator);
const state = animator.findAnimatorState("walk");
state.addStateMachineScript(
class extends StateMachineScript {
onStateEnter(animator: Animator, animatorState: AnimatorState, layerIndex: number): void {
textRenderer.text = "0";
console.log("onStateEnter: ", animatorState);
}
onStateUpdate(animator: Animator, animatorState: AnimatorState, layerIndex: number): void {
console.log("onStateUpdate: ", animatorState);
}
onStateExit(animator: Animator, animatorState: AnimatorState, layerIndex: number): void {
textRenderer.text = "1";
console.log("onStateExit: ", animatorState);
}
}
);
animator.play("walk");
updateForE2E(engine, 30);
animator.crossFade("run", 0.5, 0, 0);
updateForE2E(engine, 100);
e2eReady();
});
});

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c9f55ccf7d9ba3eeae64d80dec8a0f1d07a28708b6864200402611c5d70fe2f3
size 31060

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6247265cc5a322be8c5870243db59fb945651e56f046a390991130aa20171a8e
size 72841

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c053b0cc1ac48e4e764d5c16b0ca2d46b5f72e4d07b74ff3b779974af22f40a0
size 30566

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b2f8c4351d39e95152af5604858512e94157b58e74800ec5be8cc9c0344cabe
size 14836

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f816564ea3b61a8f6ec6bb6ab55c4526278f15c3167493bd6e7ae7a8d96b9538
size 9894

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46d72de6ff304523602e6d651bd19de111d8a92c9ecac5854fcd8ae3686262f8
size 30210

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2ae9948fa4082b03e9a2b6e847b2e301497045a1e8a51aed55238dc67fcbb31
size 31219

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9a7e5ed546549e84343f7a590eb7d01eed5a8716833b25e305d8356ab174aee6
size 28309

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:92beef2371cf9b1cb897902ef47c58646b30133d8ebd418c39820931d226a8f0
size 28203

25
e2e/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "@galacean/engine-e2e",
"private": true,
"version": "1.0.0-alpha.6",
"license": "MIT",
"scripts": {
"case": "vite serve .dev --config .dev/vite.config.js",
"b:types": "echo hi"
},
"files": [
],
"dependencies": {
"@galacean/engine-toolkit": "^1.0.0-beta.1",
"@galacean/engine": "workspace:*",
"@galacean/engine-core": "workspace:*",
"@galacean/engine-loader": "workspace:*",
"@galacean/engine-design": "workspace:*",
"@galacean/engine-math": "workspace:*",
"@galacean/engine-rhi-webgl": "workspace:*",
"@galacean/engine-physics-lite": "workspace:*",
"dat.gui": "^0.7.9",
"vite": "^3.1.6",
"sass": "^1.55.0"
}
}

67
e2e/support/commands.ts Normal file
View File

@@ -0,0 +1,67 @@
import { recurse } from "cypress-recurse";
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
declare global {
namespace Cypress {
interface Chainable {
screenshotWithThreshold(category: string, name: string, threshold?: number): Chainable<Element>;
}
}
}
Cypress.Commands.add("screenshotWithThreshold", (category, name, threshold = 0) => {
cy.visit(`/mpa/${name}.html`);
cy.get(".cypressReady").then(() => {
return new Promise((resolve) => {
const imageName = `${category}_${name}`;
resolve(
recurse(
() => {
return cy
.get("#canvas")
.screenshot(imageName, { overwrite: true, capture: "viewport" })
.then(() => {
return cy.task("compare", {
fileName: imageName,
options: {
specFolder: Cypress.spec.name,
threshold
}
});
});
},
({ match }) => match,
{
limit: 3
}
)
);
});
});
});

20
e2e/support/e2e.ts Normal file
View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')

37
e2e/tests/animator.cy.ts Normal file
View File

@@ -0,0 +1,37 @@
describe("Animator", () => {
it("Animator Play", () => {
cy.screenshotWithThreshold("Animator", "animator-play", 0.3);
});
it("Animator Crossfade", () => {
cy.screenshotWithThreshold("Animator", "animator-crossfade", 0.3);
});
it("Animation Additive", () => {
cy.screenshotWithThreshold("Animator", "animator-additive", 0.3);
});
it("Animator Reuse", () => {
cy.screenshotWithThreshold("Animator", "animator-reuse", 0.3);
});
it("Animation BlendShape", () => {
cy.screenshotWithThreshold("Animator", "animator-blendShape", 0.3);
});
it("Animator CustomBlendShape", () => {
cy.screenshotWithThreshold("Animator", "animator-customBlendShape", 0.3);
});
it("Animator stateMachineScript", () => {
cy.screenshotWithThreshold("Animator", "animator-stateMachineScript", 0.38);
});
it("Animator event", () => {
cy.screenshotWithThreshold("Animator", "animator-event", 0.38);
});
it("Animator CustomAnimationClip", () => {
cy.screenshotWithThreshold("Animator", "animator-customAnimationClip", 0.3);
});
});

8
e2e/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
}

View File

@@ -18,6 +18,9 @@
"b:miniprogram": "cross-env BUILD_TYPE=MINI rollup -c",
"b:all": "npm run b:types && cross-env BUILD_TYPE=ALL rollup -c",
"clean": "pnpm -r exec rm -rf dist && pnpm -r exec rm -rf types",
"e2e:case": "pnpm -C ./e2e run case",
"e2e": "cypress run --browser chrome --headless",
"e2e:debug": "cypress open",
"prepare": "husky install"
},
"devDependencies": {
@@ -54,7 +57,11 @@
"ts-node": "^10",
"typescript": "^5.1.6",
"@types/webxr": "latest",
"husky": "^8.0.0"
"husky": "^8.0.0",
"fs-extra": "^10.1.0",
"cypress": "^12.17.1",
"cypress-recurse": "^1.23.0",
"odiff-bin": "^2.5.0"
},
"lint-staged": {
"*.{ts}": [

View File

@@ -215,8 +215,8 @@ export class TextUtils {
} else {
word += char;
wordWidth += charInfo.xAdvance;
wordMaxAscent = lineMaxAscent = Math.max(wordMaxAscent, ascent);
wordMaxDescent = lineMaxDescent = Math.max(wordMaxDescent, descent);
wordMaxAscent = Math.max(wordMaxAscent, ascent);
wordMaxDescent = Math.max(wordMaxDescent, descent);
}
}
}

View File

@@ -36,20 +36,28 @@ export class Component extends EngineObject {
this._enabled = value;
if (this._entity._isActiveInScene) {
if (value) {
this._phasedActiveInScene = true;
this._onEnableInScene();
if (!this._phasedActiveInScene) {
this._phasedActiveInScene = true;
this._onEnableInScene();
}
} else {
this._phasedActiveInScene = false;
this._onDisableInScene();
if (this._phasedActiveInScene) {
this._phasedActiveInScene = false;
this._onDisableInScene();
}
}
}
if (this._entity.isActiveInHierarchy) {
if (value) {
this._phasedActive = true;
this._onEnable();
if (!this._phasedActive) {
this._phasedActive = true;
this._onEnable();
}
} else {
this._phasedActive = false;
this._onDisable();
if (this._phasedActive) {
this._phasedActive = false;
this._onDisable();
}
}
}
}

View File

@@ -18,6 +18,7 @@ export class RenderQueue {
static _compareFromNearToFar(a: RenderElement, b: RenderElement): number {
return (
a.data.component.priority - b.data.component.priority ||
a.data.material._priority - b.data.material._priority ||
a.data.component._distanceForSort - b.data.component._distanceForSort
);
}
@@ -28,6 +29,7 @@ export class RenderQueue {
static _compareFromFarToNear(a: RenderElement, b: RenderElement): number {
return (
a.data.component.priority - b.data.component.priority ||
a.data.material._priority - b.data.material._priority ||
b.data.component._distanceForSort - a.data.component._distanceForSort
);
}

View File

@@ -1,6 +1,7 @@
import { EngineObject } from "../base/EngineObject";
import { Component } from "../Component";
import { Entity } from "../Entity";
import { UpdateFlagManager } from "../UpdateFlagManager";
import { AnimationClipCurveBinding } from "./AnimationClipCurveBinding";
import { AnimationCurve } from "./animationCurve/AnimationCurve";
import { AnimationEvent } from "./AnimationEvent";
@@ -14,6 +15,9 @@ export class AnimationClip extends EngineObject {
/** @internal */
_curveBindings: AnimationClipCurveBinding[] = [];
/** @internal */
_updateFlagManager: UpdateFlagManager = new UpdateFlagManager();
private _length: number = 0;
private _events: AnimationEvent[] = [];
@@ -81,6 +85,8 @@ export class AnimationClip extends EngineObject {
while (--index >= 0 && eventTime < events[index].time);
events.splice(index + 1, 0, newEvent);
}
this._updateFlagManager.dispatch();
}
/**
@@ -88,6 +94,7 @@ export class AnimationClip extends EngineObject {
*/
clearEvents(): void {
this._events.length = 0;
this._updateFlagManager.dispatch();
}
/**

View File

@@ -312,7 +312,6 @@ export class Animator extends Component {
const { property } = curve;
const { instanceId } = targetEntity;
// Get owner
const propertyOwners = (curveOwnerPool[instanceId] ||= Object.create(null));
const owner = (propertyOwners[property] ||= curve._createCurveOwner(targetEntity, component));
@@ -336,26 +335,30 @@ export class Animator extends Component {
private _saveAnimatorEventHandlers(state: AnimatorState, animatorStateData: AnimatorStateData): void {
const eventHandlerPool = this._animationEventHandlerPool;
const scripts = [];
this._entity.getComponents(Script, scripts);
const scriptCount = scripts.length;
const { eventHandlers } = animatorStateData;
const { events } = state.clip;
eventHandlers.length = 0;
for (let i = 0, n = events.length; i < n; i++) {
const event = events[i];
const eventHandler = eventHandlerPool.getFromPool();
const funcName = event.functionName;
const { handlers } = eventHandler;
const clipChangedListener = () => {
this._entity.getComponents(Script, scripts);
const scriptCount = scripts.length;
const { events } = state.clip;
eventHandlers.length = 0;
for (let i = 0, n = events.length; i < n; i++) {
const event = events[i];
const eventHandler = eventHandlerPool.getFromPool();
const funcName = event.functionName;
const { handlers } = eventHandler;
eventHandler.event = event;
handlers.length = 0;
for (let j = scriptCount - 1; j >= 0; j--) {
const handler = <Function>scripts[j][funcName];
handler && handlers.push(handler);
eventHandler.event = event;
handlers.length = 0;
for (let j = scriptCount - 1; j >= 0; j--) {
const handler = <Function>scripts[j][funcName];
handler && handlers.push(handler);
}
eventHandlers.push(eventHandler);
}
eventHandlers.push(eventHandler);
}
};
clipChangedListener();
state._updateFlagManager.addListener(clipChangedListener);
}
private _clearCrossData(animatorLayerData: AnimatorLayerData): void {

View File

@@ -1,3 +1,4 @@
import { UpdateFlagManager } from "../UpdateFlagManager";
import { AnimationClip } from "./AnimationClip";
import { AnimatorStateTransition } from "./AnimatorTransition";
import { WrapMode } from "./enums/WrapMode";
@@ -18,6 +19,8 @@ export class AnimatorState {
_onStateUpdateScripts: StateMachineScript[] = [];
/** @internal */
_onStateExitScripts: StateMachineScript[] = [];
/** @internal */
_updateFlagManager: UpdateFlagManager = new UpdateFlagManager();
private _clipStartTime: number = 0;
private _clipEndTime: number = 1;
@@ -39,8 +42,21 @@ export class AnimatorState {
}
set clip(clip: AnimationClip) {
const lastClip = this._clip;
if (lastClip === clip) {
return;
}
if (lastClip) {
lastClip._updateFlagManager.removeListener(this._onClipChanged);
}
this._clip = clip;
this._clipEndTime = Math.min(this._clipEndTime, 1);
this._onClipChanged();
clip._updateFlagManager.addListener(this._onClipChanged);
}
/**
@@ -68,7 +84,9 @@ export class AnimatorState {
/**
* @param name - The state's name
*/
constructor(public readonly name: string) {}
constructor(public readonly name: string) {
this._onClipChanged = this._onClipChanged.bind(this);
}
/**
* Add an outgoing transition to the destination state.
@@ -154,4 +172,11 @@ export class AnimatorState {
index !== -1 && this._onStateExitScripts.splice(index, 1);
}
}
/**
* @internal
*/
_onClipChanged(): void {
this._updateFlagManager.dispatch();
}
}

View File

@@ -18,6 +18,8 @@ export class Material extends ReferResource implements IClone {
_shader: Shader;
/** @internal */
_renderStates: RenderState[] = []; // todo: later will as a part of shaderData when shader effect frame is OK, that is more powerful and flexible.
/** @internal */
_priority: number = 0; // todo: temporary resolution of submesh rendering order issue.
private _shaderData: ShaderData = new ShaderData(ShaderDataGroup.Material);

View File

@@ -18,6 +18,7 @@ export class ReflectionParser {
if (position) entity.transform.position.copyFrom(position);
if (rotation) entity.transform.rotation.copyFrom(rotation);
if (scale) entity.transform.scale.copyFrom(scale);
entity.layer = entityConfig.layer ?? entity.layer;
return entity;
});
}

View File

@@ -1,3 +1,5 @@
import { Layer } from "@galacean/engine-core";
export interface IVector3 {
x: number;
y: number;
@@ -38,6 +40,7 @@ export interface IBasicEntity {
scale?: IVector3;
children?: Array<string>;
parent?: string;
layer?: Layer;
}
export type IEntity = IBasicEntity | IRefEntity;

1090
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,4 +2,5 @@ packages:
# all packages in direct subdirs of packages/
- 'packages/*'
# all packages in subdirs of components/
- 'tests'
- 'tests'
- 'e2e'

View File

@@ -8,8 +8,12 @@ describe("TextUtils", () => {
let scene: Scene;
let textEntity1: Entity;
let textEntity2: Entity;
let textEntity3: Entity;
let textEntity4: Entity;
let textRendererTruncate: TextRenderer;
let textRendererOverflow: TextRenderer;
let wrap1TextRenderer: TextRenderer;
let wrap2TextRenderer: TextRenderer;
before(async function () {
engine = await WebGLEngine.create({
@@ -29,6 +33,8 @@ describe("TextUtils", () => {
textEntity1 = rootEntity.createChild("text1");
textEntity2 = rootEntity.createChild("text2");
textEntity3 = rootEntity.createChild("text3");
textEntity4 = rootEntity.createChild("text4");
textRendererTruncate = textEntity1.addComponent(TextRenderer);
textRendererTruncate.font = Font.createFromOS(engine, "Arial");
@@ -36,6 +42,11 @@ describe("TextUtils", () => {
textRendererOverflow = textEntity2.addComponent(TextRenderer);
textRendererOverflow.font = Font.createFromOS(engine, "Arial");
wrap1TextRenderer = textEntity3.addComponent(TextRenderer);
wrap1TextRenderer.font = Font.createFromOS(engine, "Arial");
wrap2TextRenderer = textEntity4.addComponent(TextRenderer);
wrap2TextRenderer.font = Font.createFromOS(engine, "Arial");
engine.run();
});
@@ -200,6 +211,18 @@ describe("TextUtils", () => {
expect(result.height).to.be.equal(162);
expect(result.lines).to.be.deep.equal([" ", " ", "W", "o", "rl", "d"]);
expect(result.lineHeight).to.be.equal(27);
wrap1TextRenderer.enableWrapping = true;
wrap1TextRenderer.width = 5;
wrap1TextRenderer.fontSize = 60;
wrap1TextRenderer.text = "测试";
const text1Metrics = TextUtils.measureTextWithWrap(wrap1TextRenderer);
wrap2TextRenderer.enableWrapping = true;
wrap2TextRenderer.width = 5;
wrap2TextRenderer.fontSize = 60;
wrap2TextRenderer.text = "测试。";
const text2Metrics = TextUtils.measureTextWithWrap(wrap2TextRenderer);
expect(text1Metrics.lineMaxSizes[0].size).to.be.equal(text2Metrics.lineMaxSizes[0].size);
});
it("measureTextWithoutWrap", () => {

View File

@@ -1,15 +1,17 @@
import { WebGLEngine } from "@galacean/engine-rhi-webgl";
import {
AnimationEvent,
Animator,
AnimatorControllerLayer,
AnimatorLayerBlendingMode,
AnimatorLayerMask,
AnimatorStateMachine,
AnimatorStateTransition,
Camera
Camera,
Script
} from "@galacean/engine-core";
import { Quaternion } from "@galacean/engine-math";
import { GLTFResource } from "@galacean/engine-loader";
import { Quaternion } from "@galacean/engine-math";
import { WebGLEngine } from "@galacean/engine-rhi-webgl";
import chai, { expect } from "chai";
import spies from "chai-spies";
import { glbResource } from "./model/fox";
@@ -272,4 +274,23 @@ describe("Animator test", function () {
expect(parentLayerCurveOwner.isActive).to.eq(true);
expect(childLayerCurveOwner.isActive).to.eq(false);
});
it("animation event", () => {
animator.play("Walk");
class TestScript extends Script {
event0(): void {}
}
TestScript.prototype.event0 = chai.spy(TestScript.prototype.event0);
animator.entity.addComponent(TestScript);
const event0 = new AnimationEvent();
event0.functionName = "event0";
event0.time = 0;
const state = animator.findAnimatorState("Walk");
state.clip.addEvent(event0);
animator.update(10);
expect(TestScript.prototype.event0).to.have.been.called.exactly(1);
});
});

View File

@@ -287,6 +287,31 @@ describe("Script", () => {
}, 1000);
});
it("Enable Disable components", async () => {
const engine = await WebGLEngine.create({ canvas: document.createElement("canvas") });
class KKK extends Script {
cmdStr = "";
onEnable(): void {
this.cmdStr += "A";
}
onDisable(): void {
this.cmdStr += "D";
}
}
const root = new Entity(engine);
const kkk = root.addComponent(KKK);
root.addComponent(
class extends Script {
onEnable(): void {
kkk.enabled = false;
kkk.enabled = true;
}
}
);
engine.sceneManager.activeScene.addRootEntity(root);
expect(kkk.cmdStr[0]).to.eql("A");
});
it("Dependent components", async () => {
@dependentComponents(Camera, DependentMode.CheckOnly)
class CheckScript extends Script {}