Optimize TrailRenderer texture scale and remove widthMultiplier (#2889)

* feat: update trail texture scaling to support separate X and Y scales
This commit is contained in:
ChenMo
2026-02-24 15:53:58 +08:00
committed by GitHub
parent 67ce4eeb1c
commit 5f77293d7e
5 changed files with 49 additions and 60 deletions

View File

@@ -211,12 +211,12 @@ WebGLEngine.create({
material.emissiveColor.copyFrom(config.emissive);
trail.setMaterial(material);
trail.time = config.time;
trail.width = config.width;
trail.minVertexDistance = 0.15;
trailMaterials.push(material);
// Tapered width curve
trail.widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(0.8, 0.3), new CurveKey(1, 0));
// Tapered width curve (width baked into curve values)
const w = config.width;
trail.widthCurve = new ParticleCurve(new CurveKey(0, w), new CurveKey(0.8, 0.3 * w), new CurveKey(1, 0));
// Color gradient
const gradient = new ParticleGradient(

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b51b7d04cf5e0c04fe6f5bb333b96a1dc0b4fa903cb74730ccf10fe76e4c1804
size 61042
oid sha256:3e1e20c0df21d8aacf8b23d89f3fc61c2c6f13b025397f07ddfdfad6c3571e1c
size 61033

View File

@@ -2,7 +2,7 @@ attribute vec4 a_PositionBirthTime; // xyz: World position, w: Birth time (used
attribute vec4 a_CornerTangent; // x: Corner (-1 or 1), yzw: Tangent direction
attribute float a_Distance; // Absolute cumulative distance (written once per point)
uniform vec4 renderer_TrailParams; // x: Width, y: TextureMode (0: Stretch, 1: Tile), z: TextureScale
uniform vec4 renderer_TrailParams; // x: TextureMode (0: Stretch, 1: Tile), y: TextureScaleX, z: TextureScaleY
uniform vec2 renderer_DistanceParams; // x: HeadDistance, y: TailDistance
uniform vec3 camera_Position;
uniform mat4 camera_ViewMat;
@@ -41,17 +41,15 @@ void main() {
}
right = right * inversesqrt(rightLenSq);
float widthMultiplier = evaluateParticleCurve(renderer_WidthCurve, min(relativePos, renderer_CurveMaxTime.z));
float width = renderer_TrailParams.x * widthMultiplier;
float width = evaluateParticleCurve(renderer_WidthCurve, min(relativePos, renderer_CurveMaxTime.z));
vec3 worldPosition = position + right * width * 0.5 * corner;
gl_Position = camera_ProjMat * camera_ViewMat * vec4(worldPosition, 1.0);
// UV: u=corner side, v=position along trail
float u = corner * 0.5 + 0.5;
// Stretch: normalize to 0-1, Tile: use world distance directly
float v = renderer_TrailParams.y == 0.0 ? relativePos : distFromHead;
v_uv = vec2(u, v * renderer_TrailParams.z);
// u = position along trail (affected by textureMode), v = corner side.
float u = renderer_TrailParams.x == 0.0 ? relativePos : distFromHead;
float v = corner * 0.5 + 0.5;
v_uv = vec2(u * renderer_TrailParams.y, v * renderer_TrailParams.z);
v_color = evaluateParticleGradient(renderer_ColorKeys, renderer_CurveMaxTime.x, renderer_AlphaKeys, renderer_CurveMaxTime.y, relativePos);
}

View File

@@ -60,7 +60,9 @@ export class TrailRenderer extends Renderer {
// Shader parameters
@deepClone
private _trailParams = new Vector4(1.0, TrailTextureMode.Stretch, 1.0, 0); // x: width, y: textureMode, z: textureScale
private _trailParams = new Vector4(TrailTextureMode.Stretch, 1.0, 1.0, 0); // x: textureMode, y: textureScaleX, z: textureScaleY
@deepClone
private _textureScale = new Vector2(1.0, 1.0);
@ignoreClone
private _distanceParams = new Vector2(); // x: headDistance, y: tailDistance
@ignoreClone
@@ -120,36 +122,26 @@ export class TrailRenderer extends Renderer {
}
/**
* The width of the trail.
* The texture mapping mode for the trail.
*/
get width(): number {
get textureMode(): TrailTextureMode {
return this._trailParams.x;
}
set width(value: number) {
set textureMode(value: TrailTextureMode) {
this._trailParams.x = value;
}
/**
* The texture mapping mode for the trail.
* Scale of the UV coordinates.
* x scales the coordinate along the trail, y scales the coordinate across the trail.
*/
get textureMode(): TrailTextureMode {
return this._trailParams.y;
get textureScale(): Vector2 {
return this._textureScale;
}
set textureMode(value: TrailTextureMode) {
this._trailParams.y = value;
}
/**
* The texture scale when using Tile texture mode.
*/
get textureScale(): number {
return this._trailParams.z;
}
set textureScale(value: number) {
this._trailParams.z = value;
set textureScale(value: Vector2) {
value !== this._textureScale && this._textureScale.copyFrom(value);
}
/**
@@ -157,6 +149,9 @@ export class TrailRenderer extends Renderer {
*/
constructor(entity: Entity) {
super(entity);
// @ts-ignore
this._textureScale._onValueChanged = this._onTextureScaleChanged.bind(this);
this._onTextureScaleChanged();
this._initGeometry();
}
@@ -299,16 +294,16 @@ export class TrailRenderer extends Renderer {
// Only expand by half width when there's actual/upcoming trail geometry
if (hasTrailGeometry) {
// Find max width multiplier from widthCurve
let maxWidthMultiplier = 0;
// Find max width from widthCurve
let maxWidth = 0;
const widthKeys = this.widthCurve.keys;
for (let i = 0, n = widthKeys.length; i < n; i++) {
const value = widthKeys[i].value;
if (value > maxWidthMultiplier) {
maxWidthMultiplier = value;
if (value > maxWidth) {
maxWidth = value;
}
}
const halfWidth = this.width * maxWidthMultiplier * 0.5;
const halfWidth = maxWidth * 0.5;
min.set(min.x - halfWidth, min.y - halfWidth, min.z - halfWidth);
max.set(max.x + halfWidth, max.y + halfWidth, max.z + halfWidth);
}
@@ -606,4 +601,10 @@ export class TrailRenderer extends Renderer {
subRenderElement.set(this, material, this._primitive, subPrimitive);
renderElement.addSubRenderElement(subRenderElement);
}
@ignoreClone
private _onTextureScaleChanged(): void {
this._trailParams.y = this._textureScale.x;
this._trailParams.z = this._textureScale.y;
}
}

View File

@@ -11,7 +11,7 @@ import {
BlendMode,
Camera
} from "@galacean/engine-core";
import { Color, Vector3 } from "@galacean/engine-math";
import { Color, Vector2, Vector3 } from "@galacean/engine-math";
import { describe, it, expect, beforeEach } from "vitest";
describe("Trail", async () => {
@@ -38,9 +38,9 @@ describe("Trail", async () => {
expect(trailRenderer.emitting).to.eq(true);
expect(trailRenderer.minVertexDistance).to.eq(0.1);
expect(trailRenderer.time).to.eq(5.0);
expect(trailRenderer.width).to.eq(1.0);
expect(trailRenderer.textureMode).to.eq(TrailTextureMode.Stretch);
expect(trailRenderer.textureScale).to.eq(1.0);
expect(trailRenderer.textureScale.x).to.eq(1.0);
expect(trailRenderer.textureScale.y).to.eq(1.0);
});
it("set emitting", () => {
@@ -79,18 +79,6 @@ describe("Trail", async () => {
expect(trailRenderer.time).to.eq(10.0);
});
it("set width", () => {
const rootEntity = scene.getRootEntity();
const trailEntity = rootEntity.createChild("trail");
const trailRenderer = trailEntity.addComponent(TrailRenderer);
trailRenderer.width = 0.5;
expect(trailRenderer.width).to.eq(0.5);
trailRenderer.width = 2.0;
expect(trailRenderer.width).to.eq(2.0);
});
it("set textureMode", () => {
const rootEntity = scene.getRootEntity();
const trailEntity = rootEntity.createChild("trail");
@@ -108,11 +96,13 @@ describe("Trail", async () => {
const trailEntity = rootEntity.createChild("trail");
const trailRenderer = trailEntity.addComponent(TrailRenderer);
trailRenderer.textureScale = 2.0;
expect(trailRenderer.textureScale).to.eq(2.0);
trailRenderer.textureScale = new Vector2(2.0, 0.5);
expect(trailRenderer.textureScale.x).to.eq(2.0);
expect(trailRenderer.textureScale.y).to.eq(0.5);
trailRenderer.textureScale = 0.5;
expect(trailRenderer.textureScale).to.eq(0.5);
trailRenderer.textureScale.set(0.5, 3.0);
expect(trailRenderer.textureScale.x).to.eq(0.5);
expect(trailRenderer.textureScale.y).to.eq(3.0);
});
it("set widthCurve", () => {
@@ -180,10 +170,10 @@ describe("Trail", async () => {
const trailEntity = rootEntity.createChild("trail");
const trailRenderer = trailEntity.addComponent(TrailRenderer);
trailRenderer.setMaterial(new TrailMaterial(engine));
trailRenderer.width = 2.0;
trailRenderer.widthCurve = new ParticleCurve(new CurveKey(0, 2), new CurveKey(1, 2));
trailRenderer.minVertexDistance = 0.1;
const halfWidth = trailRenderer.width * 0.5; // 1.0
const halfWidth = 2.0 * 0.5; // 1.0
// Initial bounds is (0,0,0) because dirty flag is not set initially
expect(trailRenderer.bounds.min).to.deep.include({ x: 0, y: 0, z: 0 });
@@ -214,7 +204,7 @@ describe("Trail", async () => {
expect(trailRenderer.bounds.max.z).to.closeTo(halfWidth, 0.01);
// Test width change affects bounds
trailRenderer.width = 4.0;
trailRenderer.widthCurve = new ParticleCurve(new CurveKey(0, 4), new CurveKey(1, 4));
const newHalfWidth = 2.0;
trailEntity.transform.position = new Vector3(5, 4, 0);