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:
chenmo.gl
2026-04-20 15:47:30 +08:00
41 changed files with 734 additions and 95 deletions

View File

@@ -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.

View File

@@ -44,6 +44,6 @@ label: Graphics/Particle
scalingMode 有以下几种模式:
- **Local**:粒子会继承粒子生成器的局部变换,即粒子的变换是在生成器的本地坐标系中进行的
- **World**:粒子会继承粒子生成器的全局变换,即粒子的变换是在世界坐标系中进行的
- **Hierarchy**:粒子会继承整个变换层级中的变换,即粒子会考虑到生成器的父级及更上级的变换
- **World**:使用世界缩放(包含所有父级变换)来缩放粒子的发射位置和大小
- **Local**:仅使用自身 Transform 的缩放来缩放粒子的发射位置和大小,忽略父级缩放
- **Shape**:仅缩放发射形状区域,粒子本身的大小不受影响

View File

@@ -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;

View 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();
}

View File

@@ -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: {

View File

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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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"

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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)
);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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);
}
/**

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"
},

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -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"

View File

@@ -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;

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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",

View File

@@ -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(

View 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;
});
});
});

View File

@@ -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));

View File

@@ -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);

View 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;
}
}
}
}