mirror of
https://github.com/galacean/engine.git
synced 2026-06-01 16:21:19 +08:00
Merge remote-tracking branch 'origin/dev/2.0' into fix/shaderlab-texture-generic-return
# Conflicts: # tests/src/shader-lab/ShaderLab.test.ts
This commit is contained in:
@@ -44,6 +44,6 @@ You can debug each property in the provided example to better understand and con
|
||||
|
||||
The scalingMode has the following options:
|
||||
|
||||
- **Local**: Particles inherit the local transformation of the particle generator, meaning the particle transformation occurs in the generator's local coordinate system.
|
||||
- **World**: Particles inherit the global transformation of the particle generator, meaning the particle transformation occurs in the world coordinate system.
|
||||
- **Hierarchy**: Particles inherit transformations from the entire transformation hierarchy, meaning particles consider transformations of the generator's parent and higher-level transformations.
|
||||
- **World**: Scale particle emission position and size using the world scale, including all parent transforms.
|
||||
- **Local**: Scale particle emission position and size using only its own transform scale, ignoring parent scale.
|
||||
- **Shape**: Only scale the emission shape area, particles themselves are not affected.
|
||||
@@ -44,6 +44,6 @@ label: Graphics/Particle
|
||||
|
||||
scalingMode 有以下几种模式:
|
||||
|
||||
- **Local**:粒子会继承粒子生成器的局部变换,即粒子的变换是在生成器的本地坐标系中进行的
|
||||
- **World**:粒子会继承粒子生成器的全局变换,即粒子的变换是在世界坐标系中进行的
|
||||
- **Hierarchy**:粒子会继承整个变换层级中的变换,即粒子会考虑到生成器的父级及更上级的变换
|
||||
- **World**:使用世界缩放(包含所有父级变换)来缩放粒子的发射位置和大小
|
||||
- **Local**:仅使用自身 Transform 的缩放来缩放粒子的发射位置和大小,忽略父级缩放
|
||||
- **Shape**:仅缩放发射形状区域,粒子本身的大小不受影响
|
||||
|
||||
@@ -44,7 +44,7 @@ WebGLEngine.create({
|
||||
|
||||
// Create camera
|
||||
const cameraEntity = rootEntity.createChild("camera_entity");
|
||||
cameraEntity.transform.position = new Vector3(-10, 1, 3);// -10 can test bounds transform
|
||||
cameraEntity.transform.position = new Vector3(-10, 1, 3); // -10 can test bounds transform
|
||||
const camera = cameraEntity.addComponent(Camera);
|
||||
camera.fieldOfView = 60;
|
||||
|
||||
@@ -199,7 +199,7 @@ function createFireGlowParticle(fireEntity: Entity, texture: Texture2D): void {
|
||||
|
||||
main.simulationSpace = ParticleSimulationSpace.World;
|
||||
|
||||
main.scalingMode = ParticleScaleMode.Hierarchy;
|
||||
main.scalingMode = ParticleScaleMode.World;
|
||||
|
||||
// Emission module
|
||||
emission.rateOverTime.constant = 20;
|
||||
@@ -270,7 +270,7 @@ function createFireSmokeParticle(fireEntity: Entity, texture: Texture2D): void {
|
||||
|
||||
main.simulationSpace = ParticleSimulationSpace.World;
|
||||
|
||||
main.scalingMode = ParticleScaleMode.Hierarchy;
|
||||
main.scalingMode = ParticleScaleMode.World;
|
||||
|
||||
// Emission module
|
||||
emission.rateOverTime.constant = 25;
|
||||
@@ -353,7 +353,7 @@ function createFireEmbersParticle(fireEntity: Entity, texture: Texture2D): void
|
||||
|
||||
main.simulationSpace = ParticleSimulationSpace.World;
|
||||
|
||||
main.scalingMode = ParticleScaleMode.Hierarchy;
|
||||
main.scalingMode = ParticleScaleMode.World;
|
||||
|
||||
// Emission module
|
||||
emission.rateOverTime.constant = 65;
|
||||
|
||||
89
e2e/case/particleRenderer-shape-transform.ts
Normal file
89
e2e/case/particleRenderer-shape-transform.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @title Particle Shape Transform
|
||||
* @category Particle
|
||||
*/
|
||||
import {
|
||||
Camera,
|
||||
Color,
|
||||
ConeShape,
|
||||
BoxShape,
|
||||
Engine,
|
||||
Entity,
|
||||
Logger,
|
||||
ParticleMaterial,
|
||||
ParticleRenderer,
|
||||
ParticleSimulationSpace,
|
||||
Vector3,
|
||||
WebGLEngine
|
||||
} from "@galacean/engine";
|
||||
import { initScreenshot, updateForE2E } from "./.mockForE2E";
|
||||
|
||||
WebGLEngine.create({
|
||||
canvas: "canvas"
|
||||
}).then((engine) => {
|
||||
Logger.enable();
|
||||
engine.canvas.resizeByClientSize();
|
||||
|
||||
const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root");
|
||||
|
||||
const cameraEntity = rootEntity.createChild("Camera");
|
||||
cameraEntity.transform.position = new Vector3(0, 0, 30);
|
||||
const camera = cameraEntity.addComponent(Camera);
|
||||
camera.fieldOfView = 60;
|
||||
camera.nearClipPlane = 0.3;
|
||||
camera.farClipPlane = 1000;
|
||||
|
||||
// Cone with shape position offset
|
||||
createParticle(rootEntity, engine, -6, () => {
|
||||
const shape = new ConeShape();
|
||||
shape.position.set(0, 3, 0);
|
||||
return shape;
|
||||
});
|
||||
|
||||
// Cone with shape rotation
|
||||
createParticle(rootEntity, engine, -2, () => {
|
||||
const shape = new ConeShape();
|
||||
shape.rotation.set(0, 0, 90);
|
||||
return shape;
|
||||
});
|
||||
|
||||
// Box with shape scale
|
||||
createParticle(rootEntity, engine, 2, () => {
|
||||
const shape = new BoxShape();
|
||||
shape.scale.set(3, 1, 1);
|
||||
return shape;
|
||||
});
|
||||
|
||||
// Cone with combined transform
|
||||
createParticle(rootEntity, engine, 6, () => {
|
||||
const shape = new ConeShape();
|
||||
shape.position.set(0, 2, 0);
|
||||
shape.rotation.set(0, 0, 45);
|
||||
shape.scale.set(2, 1, 1);
|
||||
return shape;
|
||||
});
|
||||
|
||||
updateForE2E(engine, 500);
|
||||
initScreenshot(engine, camera);
|
||||
});
|
||||
|
||||
function createParticle(rootEntity: Entity, engine: Engine, xPos: number, createShape: () => any): void {
|
||||
const particleEntity = rootEntity.createChild("Particle");
|
||||
particleEntity.transform.position.set(xPos, 0, 0);
|
||||
|
||||
const particleRenderer = particleEntity.addComponent(ParticleRenderer);
|
||||
|
||||
const material = new ParticleMaterial(engine);
|
||||
material.baseColor = new Color(1.0, 1.0, 1.0, 1.0);
|
||||
particleRenderer.setMaterial(material);
|
||||
|
||||
const generator = particleRenderer.generator;
|
||||
generator.useAutoRandomSeed = false;
|
||||
|
||||
const { main, emission } = generator;
|
||||
main.startSpeed.constant = 3;
|
||||
main.startSize.constant = 0.15;
|
||||
main.simulationSpace = ParticleSimulationSpace.Local;
|
||||
|
||||
emission.shape = createShape();
|
||||
}
|
||||
@@ -436,6 +436,12 @@ export const E2E_CONFIG = {
|
||||
caseFileName: "particleRenderer-noise",
|
||||
threshold: 0,
|
||||
diffPercentage: 0
|
||||
},
|
||||
shapeTransform: {
|
||||
category: "Particle",
|
||||
caseFileName: "particleRenderer-shape-transform",
|
||||
threshold: 0,
|
||||
diffPercentage: 0.334
|
||||
}
|
||||
},
|
||||
PostProcess: {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aa7a4bc8c57d966ad7ca49f6ea4f7177bb5d4e9c4c666f3c92ecc600c08b28f5
|
||||
size 23645
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@galacean/engine-e2e",
|
||||
"private": true,
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"case": "vite serve .dev --config .dev/vite.config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-examples",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"main": "dist/main.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-root",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"packageManager": "pnpm@9.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-core",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1228,7 +1228,7 @@ export class ParticleGenerator {
|
||||
// StartSpeed's impact
|
||||
const { shape } = this.emission;
|
||||
if (shape?.enabled) {
|
||||
shape._getPositionRange(min, max);
|
||||
shape._getPositionRange(bounds);
|
||||
shape._getDirectionRange(directionMin, directionMax);
|
||||
} else {
|
||||
min.set(0, 0, 0);
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* Control how Particle Generator apply transform scale.
|
||||
*/
|
||||
export enum ParticleScaleMode {
|
||||
/** Scale the Particle Generator using the entire transform hierarchy. */
|
||||
Hierarchy,
|
||||
/** Scale the Particle Generator using only its own transform scale. (Ignores parent scale). */
|
||||
/** Scale the Particle Generator using the world scale, including all parent transforms. */
|
||||
World,
|
||||
/** Scale the Particle Generator using only its own transform scale, ignoring parent scale. */
|
||||
Local,
|
||||
/** Only apply transform scale to the shape component, which controls where particles are spawned, but does not affect their size or movement. */
|
||||
World
|
||||
/** Scale only the emitter shape positions; particle size and movement are unaffected. */
|
||||
Shape
|
||||
}
|
||||
|
||||
@@ -277,8 +277,8 @@ export class MainModule implements ICustomClone {
|
||||
_getPositionScale(): Vector3 {
|
||||
const transform = this._generator._renderer.entity.transform;
|
||||
switch (this.scalingMode) {
|
||||
case ParticleScaleMode.Hierarchy:
|
||||
case ParticleScaleMode.World:
|
||||
case ParticleScaleMode.Shape:
|
||||
return transform.lossyWorldScale;
|
||||
case ParticleScaleMode.Local:
|
||||
return transform.scale;
|
||||
@@ -306,7 +306,7 @@ export class MainModule implements ICustomClone {
|
||||
}
|
||||
|
||||
switch (this.scalingMode) {
|
||||
case ParticleScaleMode.Hierarchy:
|
||||
case ParticleScaleMode.World:
|
||||
var scale = transform.lossyWorldScale;
|
||||
shaderData.setVector3(MainModule._positionScale, scale);
|
||||
shaderData.setVector3(MainModule._sizeScale, scale);
|
||||
@@ -316,7 +316,7 @@ export class MainModule implements ICustomClone {
|
||||
shaderData.setVector3(MainModule._positionScale, scale);
|
||||
shaderData.setVector3(MainModule._sizeScale, scale);
|
||||
break;
|
||||
case ParticleScaleMode.World:
|
||||
case ParticleScaleMode.Shape:
|
||||
shaderData.setVector3(MainModule._positionScale, transform.lossyWorldScale);
|
||||
shaderData.setVector3(MainModule._sizeScale, MainModule._vector3One);
|
||||
break;
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { Rand, Vector3 } from "@galacean/engine-math";
|
||||
import { BoundingBox, MathUtil, Matrix, Quaternion, Rand, Vector2, Vector3 } from "@galacean/engine-math";
|
||||
import { ParticleShapeType } from "./enums/ParticleShapeType";
|
||||
import { UpdateFlagManager } from "../../../UpdateFlagManager";
|
||||
import { ignoreClone } from "../../../clone/CloneManager";
|
||||
import { deepClone, ignoreClone } from "../../../clone/CloneManager";
|
||||
|
||||
/**
|
||||
* Base class for all particle shapes.
|
||||
*/
|
||||
export abstract class BaseShape {
|
||||
/** @internal */
|
||||
static _tempVector20 = new Vector2();
|
||||
/** @internal */
|
||||
static _tempVector21 = new Vector2();
|
||||
/** @internal */
|
||||
static _tempVector30 = new Vector3();
|
||||
/** @internal */
|
||||
static _tempVector31 = new Vector3();
|
||||
private static _tempQuaternion = new Quaternion();
|
||||
/** The type of shape to emit particles from. */
|
||||
abstract readonly shapeType: ParticleShapeType;
|
||||
|
||||
@@ -16,6 +25,19 @@ export abstract class BaseShape {
|
||||
private _enabled = true;
|
||||
private _randomDirectionAmount = 0;
|
||||
|
||||
@deepClone
|
||||
private _position = new Vector3(0, 0, 0);
|
||||
@deepClone
|
||||
private _rotation = new Vector3(0, 0, 0);
|
||||
@deepClone
|
||||
private _scale = new Vector3(1, 1, 1);
|
||||
@ignoreClone
|
||||
private _matrix = new Matrix();
|
||||
@ignoreClone
|
||||
private _transformDirty = false;
|
||||
@ignoreClone
|
||||
private _hasShapeTransform = false;
|
||||
|
||||
/**
|
||||
* Specifies whether the ShapeModule is enabled or disabled.
|
||||
*/
|
||||
@@ -44,6 +66,54 @@ export abstract class BaseShape {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a local position offset to the shape.
|
||||
*/
|
||||
get position(): Vector3 {
|
||||
return this._position;
|
||||
}
|
||||
|
||||
set position(value: Vector3) {
|
||||
if (value !== this._position) {
|
||||
this._position.copyFrom(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a local rotation to the shape, specified as euler angles in degrees.
|
||||
*/
|
||||
get rotation(): Vector3 {
|
||||
return this._rotation;
|
||||
}
|
||||
|
||||
set rotation(value: Vector3) {
|
||||
if (value !== this._rotation) {
|
||||
this._rotation.copyFrom(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a local scale to the shape.
|
||||
*/
|
||||
get scale(): Vector3 {
|
||||
return this._scale;
|
||||
}
|
||||
|
||||
set scale(value: Vector3) {
|
||||
if (value !== this._scale) {
|
||||
this._scale.copyFrom(value);
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// @ts-ignore
|
||||
this._position._onValueChanged = this._onTransformChanged;
|
||||
// @ts-ignore
|
||||
this._rotation._onValueChanged = this._onTransformChanged;
|
||||
// @ts-ignore
|
||||
this._scale._onValueChanged = this._onTransformChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -61,15 +131,92 @@ export abstract class BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract _generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void;
|
||||
_generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
this._generateLocalPositionAndDirection(rand, emitTime, position, direction);
|
||||
if (this._hasShapeTransform) {
|
||||
const matrix = this._getMatrix();
|
||||
Vector3.transformToVec3(position, matrix, position);
|
||||
Vector3.transformNormal(direction, matrix, direction);
|
||||
direction.normalize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract _getDirectionRange(outMin: Vector3, outMax: Vector3): void;
|
||||
_getPositionRange(bounds: BoundingBox): void {
|
||||
this._getLocalPositionRange(bounds.min, bounds.max);
|
||||
if (this._hasShapeTransform) {
|
||||
BoundingBox.transform(bounds, this._getMatrix(), bounds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract _getPositionRange(outMin: Vector3, outMax: Vector3): void;
|
||||
_getDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
this._getLocalDirectionRange(outMin, outMax);
|
||||
if (this._hasShapeTransform) {
|
||||
this._transformDirectionRange(outMin, outMax);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract _generateLocalPositionAndDirection(
|
||||
rand: Rand,
|
||||
emitTime: number,
|
||||
position: Vector3,
|
||||
direction: Vector3
|
||||
): void;
|
||||
|
||||
protected abstract _getLocalPositionRange(outMin: Vector3, outMax: Vector3): void;
|
||||
|
||||
protected abstract _getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void;
|
||||
|
||||
@ignoreClone
|
||||
protected _onTransformChanged = (): void => {
|
||||
this._transformDirty = true;
|
||||
const { _position: p, _rotation: r, _scale: s } = this;
|
||||
this._hasShapeTransform =
|
||||
p.x !== 0 || p.y !== 0 || p.z !== 0 || r.x !== 0 || r.y !== 0 || r.z !== 0 || s.x !== 1 || s.y !== 1 || s.z !== 1;
|
||||
this._updateManager.dispatch();
|
||||
};
|
||||
|
||||
private _getMatrix(): Matrix {
|
||||
if (this._transformDirty) {
|
||||
const { _rotation: r } = this;
|
||||
const q = BaseShape._tempQuaternion;
|
||||
Quaternion.rotationEuler(
|
||||
MathUtil.degreeToRadian(r.x),
|
||||
MathUtil.degreeToRadian(r.y),
|
||||
MathUtil.degreeToRadian(r.z),
|
||||
q
|
||||
);
|
||||
Matrix.affineTransformation(this._scale, q, this._position, this._matrix);
|
||||
this._transformDirty = false;
|
||||
}
|
||||
return this._matrix;
|
||||
}
|
||||
|
||||
// Arvo min/max method without translation, only apply RS part of the matrix
|
||||
private _transformDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
const e = this._getMatrix().elements;
|
||||
const { x: minX, y: minY, z: minZ } = outMin;
|
||||
const { x: maxX, y: maxY, z: maxZ } = outMax;
|
||||
// prettier-ignore
|
||||
const e0 = e[0], e1 = e[1], e2 = e[2],
|
||||
e4 = e[4], e5 = e[5], e6 = e[6],
|
||||
e8 = e[8], e9 = e[9], e10 = e[10];
|
||||
|
||||
outMin.set(
|
||||
(e0 > 0 ? e0 * minX : e0 * maxX) + (e4 > 0 ? e4 * minY : e4 * maxY) + (e8 > 0 ? e8 * minZ : e8 * maxZ),
|
||||
(e1 > 0 ? e1 * minX : e1 * maxX) + (e5 > 0 ? e5 * minY : e5 * maxY) + (e9 > 0 ? e9 * minZ : e9 * maxZ),
|
||||
(e2 > 0 ? e2 * minX : e2 * maxX) + (e6 > 0 ? e6 * minY : e6 * maxY) + (e10 > 0 ? e10 * minZ : e10 * maxZ)
|
||||
);
|
||||
|
||||
outMax.set(
|
||||
(e0 > 0 ? e0 * maxX : e0 * minX) + (e4 > 0 ? e4 * maxY : e4 * minY) + (e8 > 0 ? e8 * maxZ : e8 * minZ),
|
||||
(e1 > 0 ? e1 * maxX : e1 * minX) + (e5 > 0 ? e5 * maxY : e5 * minY) + (e9 > 0 ? e9 * maxZ : e9 * minZ),
|
||||
(e2 > 0 ? e2 * maxX : e2 * minX) + (e6 > 0 ? e6 * maxY : e6 * minY) + (e10 > 0 ? e10 * maxZ : e10 * minZ)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import { ParticleShapeType } from "./enums/ParticleShapeType";
|
||||
* Particle shape that emits particles from a box.
|
||||
*/
|
||||
export class BoxShape extends BaseShape {
|
||||
private static _tempVector30 = new Vector3();
|
||||
|
||||
readonly shapeType = ParticleShapeType.Box;
|
||||
|
||||
@deepClone
|
||||
@@ -37,11 +35,11 @@ export class BoxShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
_generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
ShapeUtils._randomPointInsideHalfUnitBox(position, rand);
|
||||
position.multiply(this.size);
|
||||
|
||||
const defaultDirection = BoxShape._tempVector30;
|
||||
const defaultDirection = BaseShape._tempVector30;
|
||||
defaultDirection.set(0.0, 0.0, -1.0);
|
||||
ShapeUtils._randomPointUnitSphere(direction, rand);
|
||||
Vector3.lerp(defaultDirection, direction, this.randomDirectionAmount, direction);
|
||||
@@ -50,7 +48,7 @@ export class BoxShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
const radian = Math.PI * this.randomDirectionAmount;
|
||||
|
||||
if (this.randomDirectionAmount < 0.5) {
|
||||
@@ -67,7 +65,7 @@ export class BoxShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
const { x, y, z } = this._size;
|
||||
outMin.set(-x * 0.5, -y * 0.5, -z * 0.5);
|
||||
outMax.set(x * 0.5, y * 0.5, z * 0.5);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MathUtil, Rand, Vector2, Vector3 } from "@galacean/engine-math";
|
||||
import { MathUtil, Rand, Vector3 } from "@galacean/engine-math";
|
||||
import { BaseShape } from "./BaseShape";
|
||||
import { ShapeUtils } from "./ShapeUtils";
|
||||
import { ParticleShapeArcMode } from "./enums/ParticleShapeArcMode";
|
||||
@@ -8,8 +8,6 @@ import { ParticleShapeType } from "./enums/ParticleShapeType";
|
||||
* Particle shape that emits particles from a circle.
|
||||
*/
|
||||
export class CircleShape extends BaseShape {
|
||||
private static _tempPositionPoint = new Vector2();
|
||||
|
||||
readonly shapeType = ParticleShapeType.Circle;
|
||||
|
||||
private _radius = 1.0;
|
||||
@@ -76,8 +74,8 @@ export class CircleShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
const positionPoint = CircleShape._tempPositionPoint;
|
||||
_generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
const positionPoint = BaseShape._tempVector20;
|
||||
|
||||
switch (this.arcMode) {
|
||||
case ParticleShapeArcMode.Loop:
|
||||
@@ -101,7 +99,7 @@ export class CircleShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
const randomDirZ = this.randomDirectionAmount > 0.5 ? 1 : Math.sin(this.randomDirectionAmount * Math.PI);
|
||||
const randomDegreeOnXY = 0.5 * (360 - this._arc) * this.randomDirectionAmount;
|
||||
const randomDirY = randomDegreeOnXY > 90 ? -1 : -Math.sin(randomDegreeOnXY);
|
||||
@@ -111,7 +109,7 @@ export class CircleShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
this._getUnitArcRange(this._arc, outMin, outMax, 0, 0);
|
||||
outMin.scale(this._radius);
|
||||
outMax.scale(this._radius);
|
||||
|
||||
@@ -7,11 +7,6 @@ import { ParticleShapeType } from "./enums/ParticleShapeType";
|
||||
* Cone shape.
|
||||
*/
|
||||
export class ConeShape extends BaseShape {
|
||||
private static _tempVector20 = new Vector2();
|
||||
private static _tempVector21 = new Vector2();
|
||||
private static _tempVector30 = new Vector3();
|
||||
private static _tempVector31 = new Vector3();
|
||||
|
||||
readonly shapeType = ParticleShapeType.Cone;
|
||||
|
||||
private _angle = 25.0;
|
||||
@@ -78,8 +73,8 @@ export class ConeShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
const unitPosition = ConeShape._tempVector20;
|
||||
_generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
const unitPosition = BaseShape._tempVector20;
|
||||
const radian = MathUtil.degreeToRadian(this.angle);
|
||||
const dirSinA = Math.sin(radian);
|
||||
const dirCosA = Math.cos(radian);
|
||||
@@ -89,7 +84,7 @@ export class ConeShape extends BaseShape {
|
||||
ShapeUtils.randomPointInsideUnitCircle(unitPosition, rand);
|
||||
position.set(unitPosition.x * this.radius, unitPosition.y * this.radius, 0);
|
||||
|
||||
const unitDirection = ConeShape._tempVector21;
|
||||
const unitDirection = BaseShape._tempVector21;
|
||||
ShapeUtils.randomPointInsideUnitCircle(unitDirection, rand);
|
||||
Vector2.lerp(unitPosition, unitDirection, this.randomDirectionAmount, unitDirection);
|
||||
direction.set(unitDirection.x * dirSinA, unitDirection.y * dirSinA, -dirCosA);
|
||||
@@ -101,11 +96,11 @@ export class ConeShape extends BaseShape {
|
||||
direction.set(unitPosition.x * dirSinA, unitPosition.y * dirSinA, -dirCosA);
|
||||
direction.normalize();
|
||||
|
||||
const distance = ConeShape._tempVector30;
|
||||
const distance = BaseShape._tempVector30;
|
||||
Vector3.scale(direction, this.length * rand.random(), distance);
|
||||
position.add(distance);
|
||||
|
||||
const randomDirection = ConeShape._tempVector31;
|
||||
const randomDirection = BaseShape._tempVector31;
|
||||
ShapeUtils._randomPointUnitSphere(randomDirection, rand);
|
||||
Vector3.lerp(direction, randomDirection, this.randomDirectionAmount, direction);
|
||||
break;
|
||||
@@ -115,7 +110,7 @@ export class ConeShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
let radian = 0;
|
||||
switch (this.emitType) {
|
||||
case ConeEmitType.Base:
|
||||
@@ -135,7 +130,7 @@ export class ConeShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
const { radius } = this;
|
||||
|
||||
switch (this.emitType) {
|
||||
|
||||
@@ -28,7 +28,7 @@ export class HemisphereShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
_generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
ShapeUtils._randomPointInsideUnitSphere(position, rand);
|
||||
position.scale(this.radius);
|
||||
|
||||
@@ -42,7 +42,7 @@ export class HemisphereShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
const randomDir = Math.sin(0.5 * this.randomDirectionAmount * Math.PI);
|
||||
outMin.set(-1, -1, -1);
|
||||
outMax.set(1, 1, randomDir);
|
||||
@@ -51,7 +51,7 @@ export class HemisphereShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
const radius = this._radius;
|
||||
outMin.set(-radius, -radius, -radius);
|
||||
outMax.set(radius, radius, 0);
|
||||
|
||||
@@ -54,7 +54,7 @@ export class MeshShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
_generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
const {
|
||||
_positionBuffer: positions,
|
||||
_positionElementInfo: positionInfo,
|
||||
@@ -78,7 +78,7 @@ export class MeshShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
const { bounds } = this._mesh;
|
||||
bounds.min.copyTo(outMin);
|
||||
bounds.max.copyTo(outMax);
|
||||
@@ -87,7 +87,7 @@ export class MeshShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
// @todo: Should use min and max of normal, use bounds is worst, but we can't get the min and max of normal by fast way.
|
||||
const { bounds } = this._mesh;
|
||||
bounds.min.copyTo(outMin);
|
||||
|
||||
@@ -28,7 +28,7 @@ export class SphereShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_generatePositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
_generateLocalPositionAndDirection(rand: Rand, emitTime: number, position: Vector3, direction: Vector3): void {
|
||||
ShapeUtils._randomPointInsideUnitSphere(position, rand);
|
||||
position.scale(this.radius);
|
||||
|
||||
@@ -39,7 +39,7 @@ export class SphereShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
outMin.set(-1, -1, -1);
|
||||
outMax.set(1, 1, 1);
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export class SphereShape extends BaseShape {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
_getLocalPositionRange(outMin: Vector3, outMax: Vector3): void {
|
||||
const radius = this._radius;
|
||||
outMin.set(-radius, -radius, -radius);
|
||||
outMax.set(radius, radius, radius);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-design",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-loader",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-math",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -8,9 +8,6 @@ import { Vector3, Vector3Like } from "./Vector3";
|
||||
* Axis Aligned Bound Box (AABB).
|
||||
*/
|
||||
export class BoundingBox implements IClone<BoundingBox>, ICopy<BoundingBox, BoundingBox> {
|
||||
private static _tempVec30: Vector3 = new Vector3();
|
||||
private static _tempVec31: Vector3 = new Vector3();
|
||||
|
||||
/**
|
||||
* Calculate a bounding box from the center point and the extent of the bounding box.
|
||||
* @param center - The center point
|
||||
@@ -67,26 +64,46 @@ export class BoundingBox implements IClone<BoundingBox>, ICopy<BoundingBox, Boun
|
||||
* @param out - The transformed bounding box
|
||||
*/
|
||||
static transform(source: BoundingBox, matrix: Matrix, out: BoundingBox): void {
|
||||
// https://zeux.io/2010/10/17/aabb-from-obb-with-component-wise-abs/
|
||||
const center = BoundingBox._tempVec30;
|
||||
const extent = BoundingBox._tempVec31;
|
||||
source.getCenter(center);
|
||||
source.getExtent(extent);
|
||||
Vector3.transformCoordinate(center, matrix, center);
|
||||
const { x, y, z } = extent;
|
||||
// Arvo's min/max method: for each matrix element, positive values multiply min for new min (max for new max),
|
||||
// negative values multiply max for new min (min for new max), then add translation.
|
||||
// Zero check avoids 0 * Infinity = NaN
|
||||
const { x: minX, y: minY, z: minZ } = source.min;
|
||||
const { x: maxX, y: maxY, z: maxZ } = source.max;
|
||||
const e = matrix.elements;
|
||||
// prettier-ignore
|
||||
const e0 = e[0], e1 = e[1], e2 = e[2],
|
||||
e4 = e[4], e5 = e[5], e6 = e[6],
|
||||
e8 = e[8], e9 = e[9], e10 = e[10];
|
||||
extent.set(
|
||||
(e0 === 0 ? 0 : Math.abs(x * e0)) + (e4 === 0 ? 0 : Math.abs(y * e4)) + (e8 === 0 ? 0 : Math.abs(z * e8)),
|
||||
(e1 === 0 ? 0 : Math.abs(x * e1)) + (e5 === 0 ? 0 : Math.abs(y * e5)) + (e9 === 0 ? 0 : Math.abs(z * e9)),
|
||||
(e2 === 0 ? 0 : Math.abs(x * e2)) + (e6 === 0 ? 0 : Math.abs(y * e6)) + (e10 === 0 ? 0 : Math.abs(z * e10))
|
||||
|
||||
out.min.set(
|
||||
(e0 > 0 ? e0 * minX : e0 < 0 ? e0 * maxX : 0) +
|
||||
(e4 > 0 ? e4 * minY : e4 < 0 ? e4 * maxY : 0) +
|
||||
(e8 > 0 ? e8 * minZ : e8 < 0 ? e8 * maxZ : 0) +
|
||||
e[12],
|
||||
(e1 > 0 ? e1 * minX : e1 < 0 ? e1 * maxX : 0) +
|
||||
(e5 > 0 ? e5 * minY : e5 < 0 ? e5 * maxY : 0) +
|
||||
(e9 > 0 ? e9 * minZ : e9 < 0 ? e9 * maxZ : 0) +
|
||||
e[13],
|
||||
(e2 > 0 ? e2 * minX : e2 < 0 ? e2 * maxX : 0) +
|
||||
(e6 > 0 ? e6 * minY : e6 < 0 ? e6 * maxY : 0) +
|
||||
(e10 > 0 ? e10 * minZ : e10 < 0 ? e10 * maxZ : 0) +
|
||||
e[14]
|
||||
);
|
||||
|
||||
out.max.set(
|
||||
(e0 > 0 ? e0 * maxX : e0 < 0 ? e0 * minX : 0) +
|
||||
(e4 > 0 ? e4 * maxY : e4 < 0 ? e4 * minY : 0) +
|
||||
(e8 > 0 ? e8 * maxZ : e8 < 0 ? e8 * minZ : 0) +
|
||||
e[12],
|
||||
(e1 > 0 ? e1 * maxX : e1 < 0 ? e1 * minX : 0) +
|
||||
(e5 > 0 ? e5 * maxY : e5 < 0 ? e5 * minY : 0) +
|
||||
(e9 > 0 ? e9 * maxZ : e9 < 0 ? e9 * minZ : 0) +
|
||||
e[13],
|
||||
(e2 > 0 ? e2 * maxX : e2 < 0 ? e2 * minX : 0) +
|
||||
(e6 > 0 ? e6 * maxY : e6 < 0 ? e6 * minY : 0) +
|
||||
(e10 > 0 ? e10 * maxZ : e10 < 0 ? e10 * minZ : 0) +
|
||||
e[14]
|
||||
);
|
||||
// set min、max
|
||||
Vector3.subtract(center, extent, out.min);
|
||||
Vector3.add(center, extent, out.max);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-physics-lite",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-physics-physx",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-rhi-webgl",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"repository": {
|
||||
"url": "https://github.com/galacean/engine.git"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-shaderlab",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -47,7 +47,7 @@ export class GLES100Visitor extends GLESVisitor {
|
||||
return "";
|
||||
}
|
||||
const expression = node.children[1] as ASTNode.Expression;
|
||||
return `gl_FragColor = ${expression.codeGen(this)}`;
|
||||
return `gl_FragColor = ${expression.codeGen(this)};`;
|
||||
}
|
||||
return super.visitJumpStatement(node);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-shader",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -5,6 +5,7 @@ mat4 renderer_LocalMat;
|
||||
mat4 renderer_ModelMat;
|
||||
mat4 camera_ViewMat;
|
||||
mat4 camera_ProjMat;
|
||||
mat4 camera_VPMat;
|
||||
mat4 renderer_MVMat;
|
||||
mat4 renderer_MVPMat;
|
||||
mat4 renderer_NormalMat;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-ui",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-xr-webxr",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@galacean/engine-xr",
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@galacean/engine-tests",
|
||||
"private": true,
|
||||
"version": "2.0.0-alpha.27",
|
||||
"version": "2.0.0-alpha.29",
|
||||
"license": "MIT",
|
||||
"main": "dist/main.js",
|
||||
"module": "dist/module.js",
|
||||
|
||||
@@ -404,6 +404,73 @@ describe("ParticleBoundingBox", function () {
|
||||
);
|
||||
});
|
||||
|
||||
it("ShapeTransform-Position", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.position.set(5, 0, 0);
|
||||
particleRenderer.generator.emission.shape = shape;
|
||||
|
||||
// Same as default BoxShape bounds shifted by (5,0,0)
|
||||
testParticleRendererBounds(
|
||||
engine,
|
||||
particleRenderer,
|
||||
{ x: 3.086, y: -1.914, z: -26.914 },
|
||||
{ x: 6.914, y: 1.914, z: 1.914 },
|
||||
delta
|
||||
);
|
||||
});
|
||||
|
||||
it("ShapeTransform-Rotation", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.size.set(1, 2, 1);
|
||||
shape.rotation.set(0, 0, 90);
|
||||
particleRenderer.generator.emission.shape = shape;
|
||||
|
||||
// size(1,2,1): local pos range (-0.5,-1,-0.5)~(0.5,1,0.5)
|
||||
// rotated 90 Z: x<->y swapped -> (-1,-0.5,-0.5)~(1,0.5,0.5)
|
||||
testParticleRendererBounds(
|
||||
engine,
|
||||
particleRenderer,
|
||||
{ x: -2.414, y: -1.914, z: -26.914 },
|
||||
{ x: 2.414, y: 1.914, z: 1.914 },
|
||||
delta
|
||||
);
|
||||
});
|
||||
|
||||
it("ShapeTransform-Scale", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.scale.set(3, 1, 1);
|
||||
particleRenderer.generator.emission.shape = shape;
|
||||
|
||||
// Default box pos range (-0.5,-0.5,-0.5)~(0.5,0.5,0.5), X scaled 3x -> (-1.5,...)~(1.5,...)
|
||||
testParticleRendererBounds(
|
||||
engine,
|
||||
particleRenderer,
|
||||
{ x: -2.914, y: -1.914, z: -26.914 },
|
||||
{ x: 2.914, y: 1.914, z: 1.914 },
|
||||
delta
|
||||
);
|
||||
});
|
||||
|
||||
it("ShapeTransform-Combined", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.position.set(0, 0, 5);
|
||||
shape.rotation.set(0, 0, 90);
|
||||
shape.scale.set(2, 1, 1);
|
||||
particleRenderer.generator.emission.shape = shape;
|
||||
|
||||
// Default size(1,1,1): local (-0.5,-0.5,-0.5)~(0.5,0.5,0.5)
|
||||
// scale(2,1,1) -> (-1,-0.5,-0.5)~(1,0.5,0.5)
|
||||
// rotate 90 Z: x<->y -> (-0.5,-1,-0.5)~(0.5,1,0.5)
|
||||
// + position(0,0,5) -> (-0.5,-1,4.5)~(0.5,1,5.5)
|
||||
testParticleRendererBounds(
|
||||
engine,
|
||||
particleRenderer,
|
||||
{ x: -1.914, y: -2.414, z: -21.914 },
|
||||
{ x: 1.914, y: 2.414, z: 6.914 },
|
||||
delta
|
||||
);
|
||||
});
|
||||
|
||||
it("Transform", function () {
|
||||
entity.transform.position.set(1, 2, 3);
|
||||
testParticleRendererBounds(
|
||||
|
||||
270
tests/src/core/particle/ParticleShapeTransform.test.ts
Normal file
270
tests/src/core/particle/ParticleShapeTransform.test.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { BoxShape, SphereShape, ConeShape } from "@galacean/engine-core";
|
||||
import { BoundingBox, Rand, Vector3 } from "@galacean/engine-math";
|
||||
import { describe, beforeEach, expect, it } from "vitest";
|
||||
|
||||
describe("ParticleShapeTransform", function () {
|
||||
const position = new Vector3();
|
||||
const direction = new Vector3();
|
||||
const rand = new Rand(0, 1234);
|
||||
const epsilon = 1e-5;
|
||||
|
||||
describe("Position offset", function () {
|
||||
it("should offset generated position by shape position", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.size.set(0, 0, 0);
|
||||
shape.position.set(3, 5, 7);
|
||||
|
||||
shape._generatePositionAndDirection(rand, 0, position, direction);
|
||||
|
||||
expect(position.x).to.be.closeTo(3, epsilon);
|
||||
expect(position.y).to.be.closeTo(5, epsilon);
|
||||
expect(position.z).to.be.closeTo(7, epsilon);
|
||||
});
|
||||
|
||||
it("should offset position range by shape position", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.size.set(2, 2, 2);
|
||||
shape.position.set(10, 0, 0);
|
||||
|
||||
const bounds = new BoundingBox();
|
||||
shape._getPositionRange(bounds);
|
||||
|
||||
expect(bounds.min.x).to.be.closeTo(9, epsilon);
|
||||
expect(bounds.max.x).to.be.closeTo(11, epsilon);
|
||||
expect(bounds.min.y).to.be.closeTo(-1, epsilon);
|
||||
expect(bounds.max.y).to.be.closeTo(1, epsilon);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rotation", function () {
|
||||
it("should rotate position range by shape rotation", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.size.set(2, 0, 0);
|
||||
shape.rotation.set(0, 0, 90);
|
||||
|
||||
const bounds = new BoundingBox();
|
||||
shape._getPositionRange(bounds);
|
||||
|
||||
// Local range: (-1,0,0) to (1,0,0), rotated 90 around Z -> (0,-1,0) to (0,1,0)
|
||||
expect(bounds.min.x).to.be.closeTo(0, epsilon);
|
||||
expect(bounds.max.x).to.be.closeTo(0, epsilon);
|
||||
expect(bounds.min.y).to.be.closeTo(-1, epsilon);
|
||||
expect(bounds.max.y).to.be.closeTo(1, epsilon);
|
||||
});
|
||||
|
||||
it("should rotate box position range", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.size.set(2, 4, 2);
|
||||
shape.rotation.set(0, 0, 90);
|
||||
|
||||
const bounds = new BoundingBox();
|
||||
shape._getPositionRange(bounds);
|
||||
|
||||
// Original range: (-1,-2,-1) to (1,2,1)
|
||||
// After 90 Z rotation: x<->y swapped
|
||||
expect(bounds.min.x).to.be.closeTo(-2, epsilon);
|
||||
expect(bounds.max.x).to.be.closeTo(2, epsilon);
|
||||
expect(bounds.min.y).to.be.closeTo(-1, epsilon);
|
||||
expect(bounds.max.y).to.be.closeTo(1, epsilon);
|
||||
});
|
||||
|
||||
it("sphere bounds should be conservative after rotation", function () {
|
||||
const shape = new SphereShape();
|
||||
shape.radius = 2;
|
||||
|
||||
const boundsBefore = new BoundingBox();
|
||||
shape._getPositionRange(boundsBefore);
|
||||
const minBefore = new Vector3();
|
||||
const maxBefore = new Vector3();
|
||||
minBefore.copyFrom(boundsBefore.min);
|
||||
maxBefore.copyFrom(boundsBefore.max);
|
||||
|
||||
shape.rotation.set(45, 30, 60);
|
||||
const boundsAfter = new BoundingBox();
|
||||
shape._getPositionRange(boundsAfter);
|
||||
|
||||
// Arvo rotates the AABB (cube), which expands it. Bounds should be >= original.
|
||||
expect(boundsAfter.min.x).to.be.lessThanOrEqual(minBefore.x + epsilon);
|
||||
expect(boundsAfter.min.y).to.be.lessThanOrEqual(minBefore.y + epsilon);
|
||||
expect(boundsAfter.min.z).to.be.lessThanOrEqual(minBefore.z + epsilon);
|
||||
expect(boundsAfter.max.x).to.be.greaterThanOrEqual(maxBefore.x - epsilon);
|
||||
expect(boundsAfter.max.y).to.be.greaterThanOrEqual(maxBefore.y - epsilon);
|
||||
expect(boundsAfter.max.z).to.be.greaterThanOrEqual(maxBefore.z - epsilon);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scale", function () {
|
||||
it("should scale generated position", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.size.set(0, 0, 0);
|
||||
shape.position.set(1, 0, 0);
|
||||
shape.scale.set(3, 1, 1);
|
||||
|
||||
shape._generatePositionAndDirection(rand, 0, position, direction);
|
||||
|
||||
// position (0,0,0) scaled then rotated then + position offset (1,0,0)
|
||||
expect(position.x).to.be.closeTo(1, epsilon);
|
||||
expect(position.y).to.be.closeTo(0, epsilon);
|
||||
});
|
||||
|
||||
it("should scale position range", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.size.set(2, 2, 2);
|
||||
shape.scale.set(3, 1, 1);
|
||||
|
||||
const bounds = new BoundingBox();
|
||||
shape._getPositionRange(bounds);
|
||||
|
||||
// Original: (-1,-1,-1) to (1,1,1), scaled X by 3
|
||||
expect(bounds.min.x).to.be.closeTo(-3, epsilon);
|
||||
expect(bounds.max.x).to.be.closeTo(3, epsilon);
|
||||
expect(bounds.min.y).to.be.closeTo(-1, epsilon);
|
||||
expect(bounds.max.y).to.be.closeTo(1, epsilon);
|
||||
});
|
||||
|
||||
it("negative scale should flip and reorder min/max", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.size.set(2, 2, 2);
|
||||
shape.scale.set(-1, 1, 1);
|
||||
|
||||
const bounds = new BoundingBox();
|
||||
shape._getPositionRange(bounds);
|
||||
|
||||
// After negative X scale, reorder ensures min < max
|
||||
expect(bounds.min.x).to.be.closeTo(-1, epsilon);
|
||||
expect(bounds.max.x).to.be.closeTo(1, epsilon);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Combined transform", function () {
|
||||
it("should apply scale then rotation then position", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.size.set(0, 0, 0);
|
||||
shape.position.set(0, 0, 5);
|
||||
shape.rotation.set(0, 90, 0);
|
||||
shape.scale.set(2, 1, 1);
|
||||
|
||||
// Generate from zero-size box at origin
|
||||
shape._generatePositionAndDirection(rand, 0, position, direction);
|
||||
|
||||
// Local pos (0,0,0) -> scale -> (0,0,0) -> rotate -> (0,0,0) -> + offset (0,0,5)
|
||||
expect(position.x).to.be.closeTo(0, epsilon);
|
||||
expect(position.y).to.be.closeTo(0, epsilon);
|
||||
expect(position.z).to.be.closeTo(5, epsilon);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Default transform (no-op fast path)", function () {
|
||||
it("should produce identical results when no transform set", function () {
|
||||
const shape1 = new BoxShape();
|
||||
const shape2 = new BoxShape();
|
||||
shape1.size.set(2, 3, 4);
|
||||
shape2.size.set(2, 3, 4);
|
||||
|
||||
const bounds1 = new BoundingBox();
|
||||
const bounds2 = new BoundingBox();
|
||||
|
||||
shape1._getPositionRange(bounds1);
|
||||
shape2._getPositionRange(bounds2);
|
||||
|
||||
expect(bounds1.min.x).to.be.closeTo(bounds2.min.x, epsilon);
|
||||
expect(bounds1.min.y).to.be.closeTo(bounds2.min.y, epsilon);
|
||||
expect(bounds1.min.z).to.be.closeTo(bounds2.min.z, epsilon);
|
||||
expect(bounds1.max.x).to.be.closeTo(bounds2.max.x, epsilon);
|
||||
expect(bounds1.max.y).to.be.closeTo(bounds2.max.y, epsilon);
|
||||
expect(bounds1.max.z).to.be.closeTo(bounds2.max.z, epsilon);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Direction range", function () {
|
||||
it("should rotate direction range by shape rotation", function () {
|
||||
const shape = new BoxShape();
|
||||
// Default direction range: min=(0,0,-1), max=(0,0,0)
|
||||
shape.rotation.set(90, 0, 0);
|
||||
|
||||
const min = new Vector3();
|
||||
const max = new Vector3();
|
||||
shape._getDirectionRange(min, max);
|
||||
|
||||
// (0,0,-1) rotated 90 around X -> (0,1,0)
|
||||
// Rotated AABB: min=(0,0,0), max=(0,1,0)
|
||||
expect(min.x).to.be.closeTo(0, epsilon);
|
||||
expect(min.y).to.be.closeTo(0, epsilon);
|
||||
expect(min.z).to.be.closeTo(0, epsilon);
|
||||
expect(max.y).to.be.closeTo(1, epsilon);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Clone", function () {
|
||||
// Simulate CloneManager: deepClone calls copyFrom, then _cloneTo
|
||||
function simulateClone(source: BoxShape): BoxShape {
|
||||
const clone = new BoxShape();
|
||||
// @deepClone step: copyFrom on existing Vector3 (preserves constructor-bound callbacks)
|
||||
clone.position.copyFrom(source.position);
|
||||
clone.rotation.copyFrom(source.rotation);
|
||||
clone.scale.copyFrom(source.scale);
|
||||
return clone;
|
||||
}
|
||||
|
||||
it("cloned shape should have correct transform values", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.position.set(1, 2, 3);
|
||||
shape.rotation.set(45, 0, 0);
|
||||
shape.scale.set(2, 2, 2);
|
||||
|
||||
const clone = simulateClone(shape);
|
||||
|
||||
expect(clone.position.x).to.be.closeTo(1, epsilon);
|
||||
expect(clone.position.y).to.be.closeTo(2, epsilon);
|
||||
expect(clone.position.z).to.be.closeTo(3, epsilon);
|
||||
expect(clone.rotation.x).to.be.closeTo(45, epsilon);
|
||||
expect(clone.scale.x).to.be.closeTo(2, epsilon);
|
||||
});
|
||||
|
||||
it("cloned shape should rebuild matrix correctly", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.size.set(2, 0, 0);
|
||||
shape.rotation.set(0, 0, 90);
|
||||
|
||||
const clone = simulateClone(shape);
|
||||
clone.size.set(2, 0, 0);
|
||||
|
||||
const boundsOrig = new BoundingBox();
|
||||
shape._getPositionRange(boundsOrig);
|
||||
|
||||
const boundsClone = new BoundingBox();
|
||||
clone._getPositionRange(boundsClone);
|
||||
|
||||
// Both should have: local (-1,0,0)~(1,0,0) rotated 90Z -> (0,-1,0)~(0,1,0)
|
||||
expect(boundsClone.min.x).to.be.closeTo(boundsOrig.min.x, epsilon);
|
||||
expect(boundsClone.min.y).to.be.closeTo(boundsOrig.min.y, epsilon);
|
||||
expect(boundsClone.max.x).to.be.closeTo(boundsOrig.max.x, epsilon);
|
||||
expect(boundsClone.max.y).to.be.closeTo(boundsOrig.max.y, epsilon);
|
||||
});
|
||||
|
||||
it("modifying clone should not affect original", function () {
|
||||
const shape = new BoxShape();
|
||||
shape.position.set(1, 2, 3);
|
||||
|
||||
const clone = simulateClone(shape);
|
||||
clone.position.set(10, 20, 30);
|
||||
|
||||
expect(shape.position.x).to.be.closeTo(1, epsilon);
|
||||
expect(shape.position.y).to.be.closeTo(2, epsilon);
|
||||
expect(shape.position.z).to.be.closeTo(3, epsilon);
|
||||
});
|
||||
|
||||
it("clone callback should trigger on cloned shape", function () {
|
||||
const shape = new BoxShape();
|
||||
const clone = simulateClone(shape);
|
||||
|
||||
let notified = false;
|
||||
clone._registerOnValueChanged(() => {
|
||||
notified = true;
|
||||
});
|
||||
|
||||
clone.position.x = 10;
|
||||
expect(notified).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -63,8 +63,9 @@ describe("BoundingBox test", () => {
|
||||
new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE)
|
||||
);
|
||||
BoundingBox.transform(maxValueBox, matrixWithoutScale, newBox);
|
||||
expect(newBox.min).to.deep.eq(compare.set(-Infinity, -Infinity, -Infinity));
|
||||
expect(newBox.max).to.deep.eq(compare.set(Infinity, Infinity, Infinity));
|
||||
// Identity rotation * MAX_VALUE = MAX_VALUE, adding small translation doesn't overflow
|
||||
expect(Math.abs(newBox.min.x)).eq(Number.MAX_VALUE);
|
||||
expect(Math.abs(newBox.max.x)).eq(Number.MAX_VALUE);
|
||||
BoundingBox.transform(maxValueBox, matrixWithScale, newBox);
|
||||
expect(newBox.min).to.deep.eq(compare.set(-Infinity, -Infinity, -Infinity));
|
||||
expect(newBox.max).to.deep.eq(compare.set(Infinity, Infinity, Infinity));
|
||||
|
||||
@@ -258,6 +258,12 @@ describe("ShaderLab", async () => {
|
||||
glslValidate(engine, shaderSource, shaderLabRelease);
|
||||
});
|
||||
|
||||
it("frag-return-vec4 (Cocos pattern: fragment entry returns vec4 instead of void)", async () => {
|
||||
const shaderSource = await readFile("./shaders/frag-return-vec4.shader");
|
||||
glslValidate(engine, shaderSource, shaderLabRelease);
|
||||
glslValidate(engine, shaderSource, shaderLabVerbose);
|
||||
});
|
||||
|
||||
it("texture-generic (GVec4 → vec4 resolve)", async () => {
|
||||
const shaderSource = await readFile("./shaders/texture-generic.shader");
|
||||
glslValidate(engine, shaderSource, shaderLabRelease);
|
||||
|
||||
41
tests/src/shader-lab/shaders/frag-return-vec4.shader
Normal file
41
tests/src/shader-lab/shaders/frag-return-vec4.shader
Normal file
@@ -0,0 +1,41 @@
|
||||
Shader "frag-return-vec4-test" {
|
||||
SubShader "Default" {
|
||||
Pass "Forward" {
|
||||
mat4 renderer_MVPMat;
|
||||
|
||||
struct Attributes { vec3 POSITION; vec3 NORMAL; vec2 TEXCOORD_0; };
|
||||
struct Varyings { vec3 v_worldPos; vec4 v_normal; vec2 v_uv; };
|
||||
|
||||
VertexShader = vert;
|
||||
FragmentShader = frag;
|
||||
|
||||
sampler2D u_texture;
|
||||
vec3 u_lightDir;
|
||||
|
||||
vec3 SRGBToLinear(vec3 gamma) { return gamma * gamma; }
|
||||
vec3 LinearToSRGB(vec3 linear) { return sqrt(linear); }
|
||||
|
||||
vec4 SurfacesFragmentModifyBaseColorAndTransparency(Varyings input) {
|
||||
vec4 color = texture2D(u_texture, input.v_uv);
|
||||
color.rgb = SRGBToLinear(color.rgb);
|
||||
return color;
|
||||
}
|
||||
|
||||
Varyings vert(Attributes attr) {
|
||||
Varyings o;
|
||||
vec4 pos = renderer_MVPMat * vec4(attr.POSITION, 1.0);
|
||||
o.v_worldPos = pos.xyz;
|
||||
o.v_normal = vec4(attr.NORMAL, 1.0);
|
||||
o.v_uv = attr.TEXCOORD_0;
|
||||
gl_Position = pos;
|
||||
return o;
|
||||
}
|
||||
|
||||
vec4 frag(Varyings input) {
|
||||
vec4 color = SurfacesFragmentModifyBaseColorAndTransparency(input);
|
||||
color.rgb = LinearToSRGB(color.rgb);
|
||||
return color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user