From 1c376d4f4c8fe37dc9ae0b4b7c591ef237a6539d Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 27 Dec 2025 23:45:12 +0800 Subject: [PATCH 01/85] feat: init trail render --- e2e/case/trailRenderer-basic.ts | 94 +++ packages/core/src/trail/TrailMaterial.ts | 76 +- packages/core/src/trail/TrailRenderer.ts | 694 +++++++++++++----- .../core/src/trail/enums/TrailTextureMode.ts | 10 + packages/core/src/trail/index.ts | 2 + packages/core/src/trail/trail.fs.glsl | 18 +- packages/core/src/trail/trail.vs.glsl | 149 +++- 7 files changed, 851 insertions(+), 192 deletions(-) create mode 100644 e2e/case/trailRenderer-basic.ts create mode 100644 packages/core/src/trail/enums/TrailTextureMode.ts diff --git a/e2e/case/trailRenderer-basic.ts b/e2e/case/trailRenderer-basic.ts new file mode 100644 index 0000000000..6e9d10cf23 --- /dev/null +++ b/e2e/case/trailRenderer-basic.ts @@ -0,0 +1,94 @@ +/** + * @title Trail Renderer Basic + * @category Trail + */ +import { + Camera, + Color, + CurveKey, + GradientAlphaKey, + GradientColorKey, + Logger, + ParticleCompositeCurve, + ParticleCurve, + ParticleGradient, + Script, + TrailRenderer, + Vector3, + WebGLEngine +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ + canvas: "canvas" +}).then((engine) => { + Logger.enable(); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + scene.background.solidColor = new Color(0.1, 0.1, 0.15, 1); + + // Create camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.position = new Vector3(0, 5, 15); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + const camera = cameraEntity.addComponent(Camera); + camera.fieldOfView = 60; + + // Create trail entity + const trailEntity = rootEntity.createChild("trail"); + trailEntity.transform.position = new Vector3(0, 0, 0); + + // Add TrailRenderer component + const trail = trailEntity.addComponent(TrailRenderer); + trail.time = 2.0; + trail.width = 0.5; + trail.minVertexDistance = 0.05; + trail.color.set(1, 0.5, 0, 1); + + // Setup width curve (taper from head to tail) + trail.widthCurve = new ParticleCompositeCurve( + new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 0)) + ); + + // Setup color gradient (orange to blue with fade out) + const gradient = new ParticleGradient( + [ + new GradientColorKey(0, new Color(1, 0.5, 0, 1)), + new GradientColorKey(0.5, new Color(1, 0, 0.5, 1)), + new GradientColorKey(1, new Color(0, 0.5, 1, 1)) + ], + [ + new GradientAlphaKey(0, 1), + new GradientAlphaKey(0.7, 0.8), + new GradientAlphaKey(1, 0) + ] + ); + trail.colorGradient = gradient; + + // Add movement script + class MoveScript extends Script { + private _time = 0; + private _radius = 4; + private _speed = 2; + private _verticalSpeed = 1.5; + + onUpdate(deltaTime: number): void { + this._time += deltaTime; + const t = this._time * this._speed; + + // Lissajous curve movement + const x = Math.sin(t) * this._radius; + const y = Math.sin(t * this._verticalSpeed) * 2; + const z = Math.cos(t * 0.7) * this._radius; + + this.entity.transform.position.set(x, y, z); + } + } + + trailEntity.addComponent(MoveScript); + + engine.run(); +}); + diff --git a/packages/core/src/trail/TrailMaterial.ts b/packages/core/src/trail/TrailMaterial.ts index 86ec16f929..94cc0dbdfc 100644 --- a/packages/core/src/trail/TrailMaterial.ts +++ b/packages/core/src/trail/TrailMaterial.ts @@ -1,20 +1,86 @@ +import { Color } from "@galacean/engine-math"; import { Engine } from "../Engine"; import { Material } from "../material/Material"; -import { BlendFactor, Shader } from "../shader"; +import { BlendFactor, CullMode, Shader, ShaderPass, SubShader } from "../shader"; +import { Texture2D } from "../texture"; +import { ShaderMacro } from "../shader/ShaderMacro"; +import { ShaderProperty } from "../shader/ShaderProperty"; import FRAG_SHADER from "./trail.fs.glsl"; import VERT_SHADER from "./trail.vs.glsl"; -Shader.create("trail", VERT_SHADER, FRAG_SHADER); - +/** + * Trail material. + */ export class TrailMaterial extends Material { + private static _baseTextureMacro: ShaderMacro = ShaderMacro.getByName("MATERIAL_HAS_BASETEXTURE"); + private static _baseColorProp: ShaderProperty = ShaderProperty.getByName("material_BaseColor"); + private static _baseTextureProp: ShaderProperty = ShaderProperty.getByName("material_BaseTexture"); + + private static _isShaderCreated = false; + + private static _createShader(): void { + if (TrailMaterial._isShaderCreated) return; + + const shaderPass = new ShaderPass(VERT_SHADER, FRAG_SHADER); + const subShader = new SubShader("default", [shaderPass]); + Shader.create("trail", [subShader]); + TrailMaterial._isShaderCreated = true; + } + + /** + * Base color. + */ + get baseColor(): Color { + return this.shaderData.getColor(TrailMaterial._baseColorProp); + } + + set baseColor(value: Color) { + const baseColor = this.shaderData.getColor(TrailMaterial._baseColorProp); + if (value !== baseColor) { + baseColor.copyFrom(value); + } + } + + /** + * Base texture. + */ + get baseTexture(): Texture2D { + return this.shaderData.getTexture(TrailMaterial._baseTextureProp); + } + + set baseTexture(value: Texture2D) { + this.shaderData.setTexture(TrailMaterial._baseTextureProp, value); + if (value) { + this.shaderData.enableMacro(TrailMaterial._baseTextureMacro); + } else { + this.shaderData.disableMacro(TrailMaterial._baseTextureMacro); + } + } + + /** + * Create a trail material instance. + * @param engine - Engine to which the material belongs + */ constructor(engine: Engine) { + TrailMaterial._createShader(); super(engine, Shader.find("trail")); + const shaderData = this.shaderData; + shaderData.setColor(TrailMaterial._baseColorProp, new Color(1, 1, 1, 1)); + + // Default blend state for additive blending const target = this.renderState.blendState.targetBlendState; target.enabled = true; - target.sourceColorBlendFactor = target.sourceAlphaBlendFactor = BlendFactor.SourceAlpha; - target.destinationColorBlendFactor = target.destinationAlphaBlendFactor = BlendFactor.One; + target.sourceColorBlendFactor = BlendFactor.SourceAlpha; + target.destinationColorBlendFactor = BlendFactor.One; + target.sourceAlphaBlendFactor = BlendFactor.SourceAlpha; + target.destinationAlphaBlendFactor = BlendFactor.One; + // Disable depth write for transparent rendering this.renderState.depthState.writeEnabled = false; + + // Disable culling for double-sided rendering + this.renderState.rasterState.cullMode = CullMode.Off; } } + diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index c17c9afa87..89a2af0eb8 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -1,250 +1,588 @@ -import { Matrix, Quaternion, Vector3 } from "@galacean/engine-math"; +import { BoundingBox, Color, Vector3 } from "@galacean/engine-math"; import { Entity } from "../Entity"; +import { Renderer } from "../Renderer"; import { RenderContext } from "../RenderPipeline/RenderContext"; import { Buffer } from "../graphic/Buffer"; +import { IndexBufferBinding } from "../graphic/IndexBufferBinding"; +import { Primitive } from "../graphic/Primitive"; +import { SubPrimitive } from "../graphic/SubPrimitive"; +import { VertexBufferBinding } from "../graphic/VertexBufferBinding"; import { VertexElement } from "../graphic/VertexElement"; +import { BufferBindFlag } from "../graphic/enums/BufferBindFlag"; import { BufferUsage } from "../graphic/enums/BufferUsage"; +import { IndexFormat } from "../graphic/enums/IndexFormat"; import { MeshTopology } from "../graphic/enums/MeshTopology"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; -import { BufferMesh } from "../mesh/BufferMesh"; -import { MeshRenderer } from "../mesh/MeshRenderer"; -import { Texture2D } from "../texture"; +import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { ShaderProperty } from "../shader/ShaderProperty"; import { TrailMaterial } from "./TrailMaterial"; - -const _tempVector3 = new Vector3(); +import { ParticleCompositeCurve } from "../particle/modules/ParticleCompositeCurve"; +import { ParticleGradient } from "../particle/modules/ParticleGradient"; +import { TrailTextureMode } from "./enums/TrailTextureMode"; /** - * @deprecated + * Trail Renderer Component. + * Renders a trail behind a moving object. */ -export class TrailRenderer extends MeshRenderer { - private _vertexStride: number; - private _vertices: Float32Array; +export class TrailRenderer extends Renderer { + // Shader properties + private static _currentTimeProp = ShaderProperty.getByName("renderer_CurrentTime"); + private static _lifetimeProp = ShaderProperty.getByName("renderer_Lifetime"); + private static _widthProp = ShaderProperty.getByName("renderer_Width"); + private static _textureModeProp = ShaderProperty.getByName("renderer_TextureMode"); + private static _tileScaleProp = ShaderProperty.getByName("renderer_TileScale"); + + // Width curve shader properties + private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); + private static _widthCurveCountProp = ShaderProperty.getByName("renderer_WidthCurveCount"); + + // Color gradient shader properties + private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); + private static _colorKeyCountProp = ShaderProperty.getByName("renderer_ColorKeyCount"); + private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); + private static _alphaKeyCountProp = ShaderProperty.getByName("renderer_AlphaKeyCount"); + + // Vertex layout constants + private static readonly VERTEX_STRIDE = 52; // bytes per vertex + private static readonly VERTEX_FLOAT_STRIDE = 13; // floats per vertex + + // Temp variables + private static _tempVector3 = new Vector3(); + + /** How long the trail points last (in seconds). */ + time: number = 5.0; + + /** The width of the trail. */ + width: number = 1.0; + + /** The minimum distance between trail points (in world units). */ + minVertexDistance: number = 0.1; + + /** Controls how the texture is applied to the trail. */ + textureMode: TrailTextureMode = TrailTextureMode.Stretch; + + /** The tile scale for Tile texture mode. */ + tileScale: number = 1.0; + + /** Trail color (used when colorGradient is not set). */ + @deepClone + color: Color = new Color(1, 1, 1, 1); + + /** + * Width curve over lifetime. + * The curve is evaluated based on normalizedAge (0 = head, 1 = tail). + * Default is a constant curve of 1.0. + */ + @deepClone + widthCurve: ParticleCompositeCurve = new ParticleCompositeCurve(1.0); + + /** + * Color gradient over lifetime. + * The gradient is evaluated based on normalizedAge (0 = head, 1 = tail). + * If not set (null), the color property is used. + */ + @deepClone + colorGradient: ParticleGradient = null; + + /** Whether the trail is currently emitting new points. */ + emitting: boolean = true; + + // Internal state + @ignoreClone + private _primitive: Primitive; + @ignoreClone + private _subPrimitive: SubPrimitive; + @ignoreClone private _vertexBuffer: Buffer; - private _stroke; - private _minSeg; - private _lifetime; - private _maxPointNum; - private _points: Array; - private _pointStates: Array; - private _strapPoints: Array; - private _curPointNum; - private _prePointsNum; + @ignoreClone + private _vertexBufferBinding: VertexBufferBinding; + @ignoreClone + private _vertices: Float32Array; + @ignoreClone + private _indexBuffer: Buffer; + @ignoreClone + private _indices: Uint16Array; + + // Ring buffer pointers + @ignoreClone + private _firstActiveElement: number = 0; + @ignoreClone + private _firstFreeElement: number = 0; + @ignoreClone + private _currentPointCount: number = 0; + @ignoreClone + private _maxPointCount: number = 256; + + // Last recorded position + @ignoreClone + private _lastPosition: Vector3 = new Vector3(); + @ignoreClone + private _hasLastPosition: boolean = false; + + // Playback time + @ignoreClone + private _playTime: number = 0; + + // Shader data cache + @ignoreClone + private _widthCurveData: Float32Array; + @ignoreClone + private _colorKeysData: Float32Array; + @ignoreClone + private _alphaKeysData: Float32Array; + /** - * @deprecated + * @internal */ - constructor(entity: Entity, props: any) { + constructor(entity: Entity) { super(entity); + this._initGeometry(); - this._stroke = props.stroke || 0.2; - this._minSeg = props.minSeg || 0.02; - this._lifetime = props.lifetime || 1000; - this._maxPointNum = (this._lifetime / 1000.0) * entity.engine.targetFrameRate; + // Set default material + this.setMaterial(new TrailMaterial(this.engine)); + } - this._points = []; - this._pointStates = []; - this._strapPoints = []; - for (let i = 0; i < this._maxPointNum; i++) { - this._points.push(new Vector3()); - this._pointStates.push(this._lifetime); + /** + * Clear all trail points. + */ + clear(): void { + this._firstActiveElement = 0; + this._firstFreeElement = 0; + this._currentPointCount = 0; + this._hasLastPosition = false; + } - this._strapPoints.push(new Vector3()); - this._strapPoints.push(new Vector3()); + /** + * @internal + */ + protected override _update(context: RenderContext): void { + super._update(context); + + const deltaTime = this.engine.time.deltaTime; + this._playTime += deltaTime; + + // Retire old points + this._retireActivePoints(); + + // Add new point if emitting and moved enough + if (this.emitting) { + this._tryAddNewPoint(); } - this._curPointNum = 0; - const mtl = props.material || new TrailMaterial(this.engine); - this.setMaterial(mtl); + // Update shader uniforms + const shaderData = this.shaderData; + shaderData.setFloat(TrailRenderer._currentTimeProp, this._playTime); + shaderData.setFloat(TrailRenderer._lifetimeProp, this.time); + shaderData.setFloat(TrailRenderer._widthProp, this.width); + shaderData.setInt(TrailRenderer._textureModeProp, this.textureMode); + shaderData.setFloat(TrailRenderer._tileScaleProp, this.tileScale); - this.setTexture(props.texture); - this._initGeometry(); + // Update width curve + this._updateWidthCurve(shaderData); + + // Update color gradient + this._updateColorGradient(shaderData); } /** * @internal */ - override update(deltaTime: number) { - let mov = 0, - newIdx = 0; - for (let i = 0; i < this._curPointNum; i++) { - this._pointStates[i] -= deltaTime; - if (this._pointStates[i] < 0) { - mov++; - } else if (mov > 0) { - newIdx = i - mov; - - // Move data - this._pointStates[newIdx] = this._pointStates[i]; - - // Move point - this._points[newIdx].copyFrom(this._points[i]); - } - } - this._curPointNum -= mov; - - let appendNewPoint = true; - if (this._curPointNum === this._maxPointNum) { - appendNewPoint = false; - } else if (this._curPointNum > 0) { - const lastPoint = this._points[this._points.length - 1]; - if (Vector3.distance(this.entity.transform.worldPosition, lastPoint) < this._minSeg) { - appendNewPoint = false; - } else { - // debugger - } + protected override _render(context: RenderContext): void { + const activeCount = this._getActivePointCount(); + if (activeCount < 2) { + return; // Need at least 2 points to form a segment } - if (appendNewPoint) { - this._pointStates[this._curPointNum] = this._lifetime; - this._points[this._curPointNum].copyFrom(this.entity.transform.worldPosition); + // Update vertex buffer with new points + this._updateVertexBuffer(); + + // Update index buffer to handle ring buffer wrap-around + const indexCount = this._updateIndexBuffer(activeCount); + this._subPrimitive.count = indexCount; - this._curPointNum++; + let material = this.getMaterial(); + if (!material) { + return; } + + if (material.destroyed || material.shader.destroyed) { + return; + } + + const engine = this._engine; + const renderElement = engine._renderElementPool.get(); + renderElement.set(this.priority, this._distanceForSort); + const subRenderElement = engine._subRenderElementPool.get(); + subRenderElement.set(this, material, this._primitive, this._subPrimitive); + renderElement.addSubRenderElement(subRenderElement); + context.camera._renderPipeline.pushRenderElement(context, renderElement); } /** - * @deprecated - * Set trail texture. - * @param texture + * @internal */ - setTexture(texture: Texture2D) { - if (texture) { - this.getMaterial().shaderData.setTexture("u_texture", texture); + protected override _updateBounds(worldBounds: BoundingBox): void { + const vertices = this._vertices; + const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const halfWidth = this.width * 0.5; + + if (this._currentPointCount === 0) { + // No active points, use current entity position + const worldPosition = this.entity.transform.worldPosition; + worldBounds.min.set( + worldPosition.x - halfWidth, + worldPosition.y - halfWidth, + worldPosition.z - halfWidth + ); + worldBounds.max.set( + worldPosition.x + halfWidth, + worldPosition.y + halfWidth, + worldPosition.z + halfWidth + ); + return; + } + + // Initialize with first active point + const firstOffset = this._firstActiveElement * 2 * floatStride; + let minX = vertices[firstOffset]; + let minY = vertices[firstOffset + 1]; + let minZ = vertices[firstOffset + 2]; + let maxX = minX; + let maxY = minY; + let maxZ = minZ; + + // Iterate through all active points + let idx = this._firstActiveElement; + for (let i = 0; i < this._currentPointCount; i++) { + const offset = idx * 2 * floatStride; + const x = vertices[offset]; + const y = vertices[offset + 1]; + const z = vertices[offset + 2]; + + if (x < minX) minX = x; + if (y < minY) minY = y; + if (z < minZ) minZ = z; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + if (z > maxZ) maxZ = z; + + idx++; + if (idx >= this._maxPointCount) idx = 0; } + + // Expand bounds by half width (for billboard offset) + worldBounds.min.set(minX - halfWidth, minY - halfWidth, minZ - halfWidth); + worldBounds.max.set(maxX + halfWidth, maxY + halfWidth, maxZ + halfWidth); } /** * @internal */ - protected override _render(context: RenderContext): void { - this._updateStrapVertices(context.camera, this._points); - this._updateStrapCoords(); - this._vertexBuffer.setData(this._vertices); - - super._render(context); + protected override _onDestroy(): void { + super._onDestroy(); + this._vertexBuffer?.destroy(); + this._indexBuffer?.destroy(); + this._primitive?.destroy(); } - private _initGeometry() { - const mesh = new BufferMesh(this._entity.engine); - - const vertexStride = 20; - const vertexCount = this._maxPointNum * 2; - const vertexFloatCount = vertexCount * vertexStride; - const vertices = new Float32Array(vertexFloatCount); - const vertexElements = [ - new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0), - new VertexElement("TEXCOORD_0", 12, VertexElementFormat.Vector2, 0) - ]; - const vertexBuffer = new Buffer(this.engine, vertexFloatCount * 4, BufferUsage.Dynamic); - - mesh.setVertexBufferBinding(vertexBuffer, vertexStride); - mesh.setVertexElements(vertexElements); - mesh.addSubMesh(0, vertexCount, MeshTopology.TriangleStrip); - - this._vertexBuffer = vertexBuffer; - this._vertexStride = vertexStride; - this._vertices = vertices; - this.mesh = mesh; + private _initGeometry(): void { + const engine = this.engine; + const maxPoints = this._maxPointCount; + + // Each point generates 2 vertices (top and bottom of the trail strip) + const vertexCount = maxPoints * 2; + const byteLength = vertexCount * TrailRenderer.VERTEX_STRIDE; + + // Create vertex buffer + this._vertexBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + byteLength, + BufferUsage.Dynamic, + false + ); + + // Create CPU-side vertex array + this._vertices = new Float32Array(vertexCount * TrailRenderer.VERTEX_FLOAT_STRIDE); + + // Create vertex buffer binding + this._vertexBufferBinding = new VertexBufferBinding( + this._vertexBuffer, + TrailRenderer.VERTEX_STRIDE + ); + + // Create index buffer (max indices = maxPoints * 2 for triangle strip) + const maxIndices = maxPoints * 2; + this._indexBuffer = new Buffer( + engine, + BufferBindFlag.IndexBuffer, + maxIndices * 2, // Uint16 = 2 bytes + BufferUsage.Dynamic, + false + ); + this._indices = new Uint16Array(maxIndices); + + // Create primitive + this._primitive = new Primitive(engine); + this._primitive.vertexBufferBindings.push(this._vertexBufferBinding); + this._primitive.setIndexBufferBinding(new IndexBufferBinding(this._indexBuffer, IndexFormat.UInt16)); + + // Define vertex elements: + // a_Position: vec3 (12 bytes, offset 0) + // a_BirthTime: float (4 bytes, offset 12) + // a_NormalizedWidth: float (4 bytes, offset 16) + // a_Color: vec4 (16 bytes, offset 20) + // a_Corner: float (4 bytes, offset 36) + // a_Tangent: vec3 (12 bytes, offset 40) + // Total: 52 bytes per vertex + this._primitive.addVertexElement(new VertexElement("a_Position", 0, VertexElementFormat.Vector3, 0)); + this._primitive.addVertexElement(new VertexElement("a_BirthTime", 12, VertexElementFormat.Float, 0)); + this._primitive.addVertexElement(new VertexElement("a_NormalizedWidth", 16, VertexElementFormat.Float, 0)); + this._primitive.addVertexElement(new VertexElement("a_Color", 20, VertexElementFormat.Vector4, 0)); + this._primitive.addVertexElement(new VertexElement("a_Corner", 36, VertexElementFormat.Float, 0)); + this._primitive.addVertexElement(new VertexElement("a_Tangent", 40, VertexElementFormat.Vector3, 0)); + + // Create sub-primitive for drawing + this._subPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); } - private _updateStrapVertices(camera, points: Array) { - const m: Matrix = camera.viewMatrix; - const e = m.elements; - const vx = new Vector3(e[0], e[4], e[8]); - const vy = new Vector3(e[1], e[5], e[9]); - const vz = new Vector3(e[2], e[6], e[10]); - const s = this._stroke; + private _retireActivePoints(): void { + const currentTime = this._playTime; + const lifetime = this.time; - vy.scale(s); + // Move firstActiveElement forward for points that have expired + while (this._firstActiveElement !== this._firstFreeElement) { + const offset = this._firstActiveElement * 2 * TrailRenderer.VERTEX_FLOAT_STRIDE; + const birthTime = this._vertices[offset + 3]; // a_BirthTime offset - const up = new Vector3(); - const down = new Vector3(); + if (currentTime - birthTime < lifetime) { + break; // This point is still alive + } - const rotation = new Quaternion(); + // Move to next element + this._firstActiveElement++; + if (this._firstActiveElement >= this._maxPointCount) { + this._firstActiveElement = 0; + } + this._currentPointCount--; + } - Vector3.transformByQuat(vx, rotation, vx); - Vector3.transformByQuat(vy, rotation, vy); + // If all points have expired, reset the trail state + if (this._currentPointCount === 0) { + this._hasLastPosition = false; + } + } - const dy = new Vector3(); - const cross = new Vector3(); - const perpVector = new Vector3(); + private _tryAddNewPoint(): void { + const worldPosition = this.entity.transform.worldPosition; - vx.normalize(); + // Check if we've moved enough to add a new point + if (this._hasLastPosition) { + const distance = Vector3.distance(worldPosition, this._lastPosition); + if (distance < this.minVertexDistance) { + return; + } + } - const vertices = this._vertices; - //-- quad pos - for (let i = 0; i < this._maxPointNum; i++) { - //-- center pos - if (i < this._curPointNum) { - const p = points[i]; - - if (i === this._curPointNum - 1 && i !== 0) { - Vector3.subtract(p, points[i - 1], perpVector); - } else { - Vector3.subtract(points[i + 1], p, perpVector); - } + // Check if we have space for a new point + let nextFreeElement = this._firstFreeElement + 1; + if (nextFreeElement >= this._maxPointCount) { + nextFreeElement = 0; + } - this._projectOnPlane(perpVector, vz, perpVector); - perpVector.normalize(); + if (nextFreeElement === this._firstActiveElement) { + // Buffer is full, retire oldest point + this._firstActiveElement++; + if (this._firstActiveElement >= this._maxPointCount) { + this._firstActiveElement = 0; + } + this._currentPointCount--; + } - // Calculate angle between vectors - let angle = Math.acos(Vector3.dot(vx, perpVector)); - Vector3.cross(vx, perpVector, cross); - if (Vector3.dot(cross, vz) <= 0) { - angle = Math.PI * 2 - angle; - } - Quaternion.rotationAxisAngle(vz, angle, rotation); - Vector3.transformByQuat(vy, rotation, dy); + // Add the new point + this._addPoint(worldPosition); - Vector3.add(p, dy, up); - Vector3.subtract(p, dy, down); + // Update last position + this._lastPosition.copyFrom(worldPosition); + this._hasLastPosition = true; + } + + private _addPoint(position: Vector3): void { + const idx = this._firstFreeElement; + const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const vertices = this._vertices; + + // Calculate tangent (direction from last position to current position) + const tangent = TrailRenderer._tempVector3; + if (this._hasLastPosition) { + Vector3.subtract(position, this._lastPosition, tangent); + tangent.normalize(); + + // If this is the second point, update the first point's tangent + // to match this tangent (so the tail doesn't have wrong orientation) + if (this._currentPointCount === 1) { + const firstIdx = this._firstActiveElement; + for (let corner = -1; corner <= 1; corner += 2) { + const vertexIdx = firstIdx * 2 + (corner === -1 ? 0 : 1); + const offset = vertexIdx * floatStride; + tangent.copyToArray(vertices, offset + 10); // Update a_Tangent + } } + } else { + // First point - use forward direction (will be updated when second point is added) + tangent.set(0, 0, 1); + } - const p0 = (i * 2 * this._vertexStride) / 4; - const p1 = ((i * 2 + 1) * this._vertexStride) / 4; - vertices[p0] = up.x; - vertices[p0 + 1] = up.y; - vertices[p0 + 2] = up.z; + // Each point has 2 vertices (top and bottom) + const color = this.color; + for (let corner = -1; corner <= 1; corner += 2) { + const vertexIdx = idx * 2 + (corner === -1 ? 0 : 1); + const offset = vertexIdx * floatStride; + + position.copyToArray(vertices, offset); // a_Position (vec3) + vertices[offset + 3] = this._playTime; // a_BirthTime (float) + vertices[offset + 4] = 1.0; // a_NormalizedWidth (float) + color.copyToArray(vertices, offset + 5); // a_Color (vec4) + vertices[offset + 9] = corner; // a_Corner (float) + tangent.copyToArray(vertices, offset + 10); // a_Tangent (vec3) + } - vertices[p1] = down.x; - vertices[p1 + 1] = down.y; - vertices[p1 + 2] = down.z; + // Update pointers + this._firstFreeElement++; + if (this._firstFreeElement >= this._maxPointCount) { + this._firstFreeElement = 0; } + this._currentPointCount++; } - private _updateStrapCoords() { - if (this._prePointsNum === this._curPointNum) { - return; - } + private _getActivePointCount(): number { + return this._currentPointCount; + } + + private _updateVertexBuffer(): void { + const firstActive = this._firstActiveElement; + const firstFree = this._firstFreeElement; - this._prePointsNum = this._curPointNum; + if (this._currentPointCount === 0) { + return; // No active points + } - const count = this._curPointNum; - const texDelta = 1.0 / count; + const byteStride = TrailRenderer.VERTEX_STRIDE; + const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const buffer = this._vertexBuffer; const vertices = this._vertices; - for (let i = 0; i < count; i++) { - const d = 1.0 - i * texDelta; - const p0 = (i * 2 * this._vertexStride) / 4; - const p1 = ((i * 2 + 1) * this._vertexStride) / 4; - vertices[p0] = 0; - vertices[p0 + 1] = d; + // Each point has 2 vertices + if (firstActive < firstFree) { + // Contiguous range - create a view of the relevant portion + const startFloat = firstActive * 2 * floatStride; + const countFloat = (firstFree - firstActive) * 2 * floatStride; + const subArray = new Float32Array(vertices.buffer, startFloat * 4, countFloat); + buffer.setData(subArray, firstActive * 2 * byteStride); + } else { + // Wrapped range - upload in two parts + // First segment: from firstActive to end + const startFloat1 = firstActive * 2 * floatStride; + const countFloat1 = (this._maxPointCount - firstActive) * 2 * floatStride; + const subArray1 = new Float32Array(vertices.buffer, startFloat1 * 4, countFloat1); + buffer.setData(subArray1, firstActive * 2 * byteStride); + + // Second segment: from 0 to firstFree + if (firstFree > 0) { + const countFloat2 = firstFree * 2 * floatStride; + const subArray2 = new Float32Array(vertices.buffer, 0, countFloat2); + buffer.setData(subArray2, 0); + } + } + } - vertices[p1] = 1.0; - vertices[p1 + 1] = d; + private _updateIndexBuffer(activeCount: number): number { + const indices = this._indices; + const firstActive = this._firstActiveElement; + const maxPointCount = this._maxPointCount; + let indexCount = 0; + + // Build index buffer to create proper triangle strip ordering + // This handles the ring buffer wrap-around case + for (let i = 0; i < activeCount; i++) { + const pointIdx = (firstActive + i) % maxPointCount; + const vertexIdx = pointIdx * 2; + + // Each point has 2 vertices (top and bottom) + indices[indexCount++] = vertexIdx; // bottom vertex + indices[indexCount++] = vertexIdx + 1; // top vertex } + + // Upload index buffer + this._indexBuffer.setData(indices, 0, 0, indexCount * 2); + + return indexCount; } - private _projectOnVector(a: Vector3, p: Vector3, out: Vector3): void { - const n_p = p.clone(); - Vector3.normalize(n_p, n_p); - const cosine = Vector3.dot(a, n_p); - out.x = n_p.x * cosine; - out.y = n_p.y * cosine; - out.z = n_p.z * cosine; + private _updateWidthCurve(shaderData: import("../shader/ShaderData").ShaderData): void { + const curve = this.widthCurve; + const widthCurveData = this._widthCurveData || (this._widthCurveData = new Float32Array(8)); + + if (curve.mode === 0) { + // Constant mode + widthCurveData[0] = 0; // time + widthCurveData[1] = curve.constant; // value + shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurveData); + shaderData.setInt(TrailRenderer._widthCurveCountProp, 1); + } else if (curve.mode === 2 && curve.curve) { + // Curve mode + const keys = curve.curve.keys; + const count = Math.min(keys.length, 4); + for (let i = 0; i < count; i++) { + widthCurveData[i * 2] = keys[i].time; + widthCurveData[i * 2 + 1] = keys[i].value; + } + shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurveData); + shaderData.setInt(TrailRenderer._widthCurveCountProp, count); + } else { + // Default: constant 1.0 + widthCurveData[0] = 0; + widthCurveData[1] = 1; + shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurveData); + shaderData.setInt(TrailRenderer._widthCurveCountProp, 1); + } } - private _projectOnPlane(a: Vector3, n: Vector3, out: Vector3) { - this._projectOnVector(a, n, _tempVector3); - Vector3.subtract(a, _tempVector3, out); + private _updateColorGradient(shaderData: import("../shader/ShaderData").ShaderData): void { + const gradient = this.colorGradient; + + if (!gradient) { + // Use vertex color (from this.color) + shaderData.setInt(TrailRenderer._colorKeyCountProp, 0); + shaderData.setInt(TrailRenderer._alphaKeyCountProp, 0); + return; + } + + const colorKeysData = this._colorKeysData || (this._colorKeysData = new Float32Array(16)); + const alphaKeysData = this._alphaKeysData || (this._alphaKeysData = new Float32Array(8)); + + // Color keys + const colorKeys = gradient.colorKeys; + const colorCount = Math.min(colorKeys.length, 4); + for (let i = 0; i < colorCount; i++) { + const key = colorKeys[i]; + colorKeysData[i * 4] = key.time; + colorKeysData[i * 4 + 1] = key.color.r; + colorKeysData[i * 4 + 2] = key.color.g; + colorKeysData[i * 4 + 3] = key.color.b; + } + shaderData.setFloatArray(TrailRenderer._colorKeysProp, colorKeysData); + shaderData.setInt(TrailRenderer._colorKeyCountProp, colorCount); + + // Alpha keys + const alphaKeys = gradient.alphaKeys; + const alphaCount = Math.min(alphaKeys.length, 4); + for (let i = 0; i < alphaCount; i++) { + const key = alphaKeys[i]; + alphaKeysData[i * 2] = key.time; + alphaKeysData[i * 2 + 1] = key.alpha; + } + shaderData.setFloatArray(TrailRenderer._alphaKeysProp, alphaKeysData); + shaderData.setInt(TrailRenderer._alphaKeyCountProp, alphaCount); } } diff --git a/packages/core/src/trail/enums/TrailTextureMode.ts b/packages/core/src/trail/enums/TrailTextureMode.ts new file mode 100644 index 0000000000..d6824f419a --- /dev/null +++ b/packages/core/src/trail/enums/TrailTextureMode.ts @@ -0,0 +1,10 @@ +/** + * Texture mapping mode for trails. + */ +export enum TrailTextureMode { + /** Map the texture once along the entire length of the trail. */ + Stretch = 0, + /** Repeat the texture along the trail based on its length in world units. */ + Tile = 1 +} + diff --git a/packages/core/src/trail/index.ts b/packages/core/src/trail/index.ts index e05821cbdf..9ab5acb143 100644 --- a/packages/core/src/trail/index.ts +++ b/packages/core/src/trail/index.ts @@ -1,2 +1,4 @@ export { TrailRenderer } from "./TrailRenderer"; export { TrailMaterial } from "./TrailMaterial"; +export { TrailTextureMode } from "./enums/TrailTextureMode"; + diff --git a/packages/core/src/trail/trail.fs.glsl b/packages/core/src/trail/trail.fs.glsl index f682678188..d89a21ca92 100644 --- a/packages/core/src/trail/trail.fs.glsl +++ b/packages/core/src/trail/trail.fs.glsl @@ -1,9 +1,21 @@ +#include + varying vec2 v_uv; +varying vec4 v_color; + +uniform vec4 material_BaseColor; -uniform sampler2D u_texture; +#ifdef MATERIAL_HAS_BASETEXTURE + uniform sampler2D material_BaseTexture; +#endif -void main(void) { +void main() { + vec4 baseColor = material_BaseColor * v_color; - gl_FragColor = texture2D(u_texture, v_uv); + #ifdef MATERIAL_HAS_BASETEXTURE + baseColor *= texture2DSRGB(material_BaseTexture, v_uv); + #endif + gl_FragColor = baseColor; } + diff --git a/packages/core/src/trail/trail.vs.glsl b/packages/core/src/trail/trail.vs.glsl index 90a7f7ad25..ece15aa8ff 100644 --- a/packages/core/src/trail/trail.vs.glsl +++ b/packages/core/src/trail/trail.vs.glsl @@ -1,14 +1,151 @@ -attribute vec3 POSITION; -attribute vec2 TEXCOORD_0; +// Trail vertex attributes (per-vertex) +// Each segment has 2 vertices (top and bottom) +attribute vec3 a_Position; // World position of the trail point center +attribute float a_BirthTime; // Time when this point was created +attribute float a_NormalizedWidth; // Width factor at this point (unused, kept for compatibility) +attribute vec4 a_Color; // Color at this point (unused when gradient is used) +attribute float a_Corner; // -1 for bottom, 1 for top +attribute vec3 a_Tangent; // Direction to next point (for billboard calculation) -varying vec2 v_uv; +// Uniforms +uniform float renderer_CurrentTime; +uniform float renderer_Lifetime; +uniform float renderer_Width; // Base width +uniform int renderer_TextureMode; // 0: Stretch, 1: Tile +uniform float renderer_TileScale; -uniform mat4 camera_ProjMat; +uniform vec3 camera_Position; uniform mat4 camera_ViewMat; +uniform mat4 camera_ProjMat; + +// Width curve uniforms (4 keyframes max: x=time, y=value) +uniform vec2 renderer_WidthCurve[4]; +uniform int renderer_WidthCurveCount; + +// Color gradient uniforms +uniform vec4 renderer_ColorKeys[4]; // x=time, yzw=rgb +uniform int renderer_ColorKeyCount; +uniform vec2 renderer_AlphaKeys[4]; // x=time, y=alpha +uniform int renderer_AlphaKeyCount; + +// Varyings +varying vec2 v_uv; +varying vec4 v_color; + +// Evaluate curve at normalized age +float evaluateCurve(in vec2 keys[4], in int count, in float t) { + if (count <= 0) return 1.0; + if (count == 1) return keys[0].y; + + for (int i = 1; i < 4; i++) { + if (i >= count) break; + if (t <= keys[i].x) { + float t0 = keys[i - 1].x; + float t1 = keys[i].x; + float v0 = keys[i - 1].y; + float v1 = keys[i].y; + float factor = (t - t0) / (t1 - t0); + return mix(v0, v1, factor); + } + } + return keys[count - 1].y; +} + +// Evaluate color gradient at normalized age +vec3 evaluateColorGradient(in vec4 keys[4], in int count, in float t) { + if (count <= 0) return vec3(1.0); + if (count == 1) return keys[0].yzw; + + for (int i = 1; i < 4; i++) { + if (i >= count) break; + if (t <= keys[i].x) { + float t0 = keys[i - 1].x; + float t1 = keys[i].x; + vec3 c0 = keys[i - 1].yzw; + vec3 c1 = keys[i].yzw; + float factor = (t - t0) / (t1 - t0); + return mix(c0, c1, factor); + } + } + return keys[count - 1].yzw; +} + +// Evaluate alpha gradient at normalized age +float evaluateAlphaGradient(in vec2 keys[4], in int count, in float t) { + if (count <= 0) return 1.0; + if (count == 1) return keys[0].y; + + for (int i = 1; i < 4; i++) { + if (i >= count) break; + if (t <= keys[i].x) { + float t0 = keys[i - 1].x; + float t1 = keys[i].x; + float a0 = keys[i - 1].y; + float a1 = keys[i].y; + float factor = (t - t0) / (t1 - t0); + return mix(a0, a1, factor); + } + } + return keys[count - 1].y; +} void main() { + // Calculate normalized age (0 = new, 1 = about to die) + float age = renderer_CurrentTime - a_BirthTime; + float normalizedAge = clamp(age / renderer_Lifetime, 0.0, 1.0); + + // Discard vertices that have exceeded their lifetime + if (normalizedAge >= 1.0) { + gl_Position = vec4(2.0, 2.0, 2.0, 1.0); // Move outside clip space + return; + } + + // Calculate billboard offset (View alignment) + vec3 toCamera = normalize(camera_Position - a_Position); + vec3 right = cross(a_Tangent, toCamera); + float rightLen = length(right); + + // Handle edge case when tangent is parallel to camera direction + if (rightLen < 0.001) { + right = cross(a_Tangent, vec3(0.0, 1.0, 0.0)); + rightLen = length(right); + if (rightLen < 0.001) { + right = cross(a_Tangent, vec3(1.0, 0.0, 0.0)); + rightLen = length(right); + } + } + right = right / rightLen; - gl_Position = camera_ProjMat * camera_ViewMat * vec4( POSITION, 1.0 ); - v_uv = TEXCOORD_0; + // Evaluate width curve + float widthMultiplier = evaluateCurve(renderer_WidthCurve, renderer_WidthCurveCount, normalizedAge); + float width = renderer_Width * widthMultiplier; + // Apply offset + vec3 worldPosition = a_Position + right * width * 0.5 * a_Corner; + + gl_Position = camera_ProjMat * camera_ViewMat * vec4(worldPosition, 1.0); + + // Calculate UV based on texture mode + float u = a_Corner * 0.5 + 0.5; // 0 for bottom, 1 for top + float v; + + if (renderer_TextureMode == 0) { + // Stretch mode: UV.v based on normalized age + v = normalizedAge; + } else { + // Tile mode: scale by tile scale + v = normalizedAge * renderer_TileScale; + } + + v_uv = vec2(u, v); + + // Evaluate color gradient or use vertex color + if (renderer_ColorKeyCount > 0 || renderer_AlphaKeyCount > 0) { + vec3 gradientColor = evaluateColorGradient(renderer_ColorKeys, renderer_ColorKeyCount, normalizedAge); + float gradientAlpha = evaluateAlphaGradient(renderer_AlphaKeys, renderer_AlphaKeyCount, normalizedAge); + v_color = vec4(gradientColor, gradientAlpha); + } else { + v_color = a_Color; + } } + From 2d070c1646810b0d75286877015c251638c09710 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 27 Dec 2025 23:54:14 +0800 Subject: [PATCH 02/85] feat: add e2e testing support for trail renderer --- e2e/case/trailRenderer-basic.ts | 8 ++++++-- e2e/config.ts | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/e2e/case/trailRenderer-basic.ts b/e2e/case/trailRenderer-basic.ts index 6e9d10cf23..ec2dfe1743 100644 --- a/e2e/case/trailRenderer-basic.ts +++ b/e2e/case/trailRenderer-basic.ts @@ -17,6 +17,7 @@ import { Vector3, WebGLEngine } from "@galacean/engine"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; // Create engine WebGLEngine.create({ @@ -89,6 +90,9 @@ WebGLEngine.create({ trailEntity.addComponent(MoveScript); - engine.run(); -}); + // engine.run(); + // Run for e2e testing + updateForE2E(engine, 50, 20); + initScreenshot(engine, camera); +}); diff --git a/e2e/config.ts b/e2e/config.ts index e8cc98a9d7..5e4973ab73 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -433,6 +433,14 @@ export const E2E_CONFIG = { diffPercentage: 0 } }, + Trail: { + basic: { + category: "Trail", + caseFileName: "trailRenderer-basic", + threshold: 0, + diffPercentage: 0 + } + }, Other: { ProjectLoader: { category: "Advance", From f8691ec98ade2ef0347469c3f53dc1f7a53ac7cc Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 27 Dec 2025 23:56:55 +0800 Subject: [PATCH 03/85] chore: remove unnecessary blank lines in trail-related files --- packages/core/src/trail/TrailMaterial.ts | 1 - packages/core/src/trail/TrailRenderer.ts | 41 ++++++------------- .../core/src/trail/enums/TrailTextureMode.ts | 1 - packages/core/src/trail/index.ts | 1 - 4 files changed, 12 insertions(+), 32 deletions(-) diff --git a/packages/core/src/trail/TrailMaterial.ts b/packages/core/src/trail/TrailMaterial.ts index 94cc0dbdfc..9faacb4183 100644 --- a/packages/core/src/trail/TrailMaterial.ts +++ b/packages/core/src/trail/TrailMaterial.ts @@ -83,4 +83,3 @@ export class TrailMaterial extends Material { this.renderState.rasterState.cullMode = CullMode.Off; } } - diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 89a2af0eb8..2892f9ef2e 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -229,16 +229,8 @@ export class TrailRenderer extends Renderer { if (this._currentPointCount === 0) { // No active points, use current entity position const worldPosition = this.entity.transform.worldPosition; - worldBounds.min.set( - worldPosition.x - halfWidth, - worldPosition.y - halfWidth, - worldPosition.z - halfWidth - ); - worldBounds.max.set( - worldPosition.x + halfWidth, - worldPosition.y + halfWidth, - worldPosition.z + halfWidth - ); + worldBounds.min.set(worldPosition.x - halfWidth, worldPosition.y - halfWidth, worldPosition.z - halfWidth); + worldBounds.max.set(worldPosition.x + halfWidth, worldPosition.y + halfWidth, worldPosition.z + halfWidth); return; } @@ -294,22 +286,13 @@ export class TrailRenderer extends Renderer { const byteLength = vertexCount * TrailRenderer.VERTEX_STRIDE; // Create vertex buffer - this._vertexBuffer = new Buffer( - engine, - BufferBindFlag.VertexBuffer, - byteLength, - BufferUsage.Dynamic, - false - ); + this._vertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, byteLength, BufferUsage.Dynamic, false); // Create CPU-side vertex array this._vertices = new Float32Array(vertexCount * TrailRenderer.VERTEX_FLOAT_STRIDE); // Create vertex buffer binding - this._vertexBufferBinding = new VertexBufferBinding( - this._vertexBuffer, - TrailRenderer.VERTEX_STRIDE - ); + this._vertexBufferBinding = new VertexBufferBinding(this._vertexBuffer, TrailRenderer.VERTEX_STRIDE); // Create index buffer (max indices = maxPoints * 2 for triangle strip) const maxIndices = maxPoints * 2; @@ -425,7 +408,7 @@ export class TrailRenderer extends Renderer { for (let corner = -1; corner <= 1; corner += 2) { const vertexIdx = firstIdx * 2 + (corner === -1 ? 0 : 1); const offset = vertexIdx * floatStride; - tangent.copyToArray(vertices, offset + 10); // Update a_Tangent + tangent.copyToArray(vertices, offset + 10); // Update a_Tangent } } } else { @@ -439,12 +422,12 @@ export class TrailRenderer extends Renderer { const vertexIdx = idx * 2 + (corner === -1 ? 0 : 1); const offset = vertexIdx * floatStride; - position.copyToArray(vertices, offset); // a_Position (vec3) - vertices[offset + 3] = this._playTime; // a_BirthTime (float) - vertices[offset + 4] = 1.0; // a_NormalizedWidth (float) - color.copyToArray(vertices, offset + 5); // a_Color (vec4) - vertices[offset + 9] = corner; // a_Corner (float) - tangent.copyToArray(vertices, offset + 10); // a_Tangent (vec3) + position.copyToArray(vertices, offset); // a_Position (vec3) + vertices[offset + 3] = this._playTime; // a_BirthTime (float) + vertices[offset + 4] = 1.0; // a_NormalizedWidth (float) + color.copyToArray(vertices, offset + 5); // a_Color (vec4) + vertices[offset + 9] = corner; // a_Corner (float) + tangent.copyToArray(vertices, offset + 10); // a_Tangent (vec3) } // Update pointers @@ -509,7 +492,7 @@ export class TrailRenderer extends Renderer { const vertexIdx = pointIdx * 2; // Each point has 2 vertices (top and bottom) - indices[indexCount++] = vertexIdx; // bottom vertex + indices[indexCount++] = vertexIdx; // bottom vertex indices[indexCount++] = vertexIdx + 1; // top vertex } diff --git a/packages/core/src/trail/enums/TrailTextureMode.ts b/packages/core/src/trail/enums/TrailTextureMode.ts index d6824f419a..937572fc5b 100644 --- a/packages/core/src/trail/enums/TrailTextureMode.ts +++ b/packages/core/src/trail/enums/TrailTextureMode.ts @@ -7,4 +7,3 @@ export enum TrailTextureMode { /** Repeat the texture along the trail based on its length in world units. */ Tile = 1 } - diff --git a/packages/core/src/trail/index.ts b/packages/core/src/trail/index.ts index 9ab5acb143..58823e3bdb 100644 --- a/packages/core/src/trail/index.ts +++ b/packages/core/src/trail/index.ts @@ -1,4 +1,3 @@ export { TrailRenderer } from "./TrailRenderer"; export { TrailMaterial } from "./TrailMaterial"; export { TrailTextureMode } from "./enums/TrailTextureMode"; - From 5c8a90d6ce88e918c6d21f5e0de65dc8c11f3259 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 00:01:52 +0800 Subject: [PATCH 04/85] feat: add basic trail renderer image for e2e testing --- e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg diff --git a/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg b/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg new file mode 100644 index 0000000000..7255c6434f --- /dev/null +++ b/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:173791302147c77c075338bd609a920869353001e2ca991ada3e0270691b8f16 +size 27481 From 72c953d91ae090dbe44f708520172ddb8be3795f Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 00:48:13 +0800 Subject: [PATCH 05/85] feat: update TrailRenderer to support texture loading and material setup --- e2e/case/trailRenderer-basic.ts | 24 ++++++++++++++++++++---- packages/core/src/trail/TrailRenderer.ts | 4 ---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/e2e/case/trailRenderer-basic.ts b/e2e/case/trailRenderer-basic.ts index ec2dfe1743..d88545bdf4 100644 --- a/e2e/case/trailRenderer-basic.ts +++ b/e2e/case/trailRenderer-basic.ts @@ -3,6 +3,7 @@ * @category Trail */ import { + AssetType, Camera, Color, CurveKey, @@ -13,6 +14,8 @@ import { ParticleCurve, ParticleGradient, Script, + Texture2D, + TrailMaterial, TrailRenderer, Vector3, WebGLEngine @@ -43,6 +46,8 @@ WebGLEngine.create({ // Add TrailRenderer component const trail = trailEntity.addComponent(TrailRenderer); + const material = new TrailMaterial(engine); + trail.setMaterial(material); trail.time = 2.0; trail.width = 0.5; trail.minVertexDistance = 0.05; @@ -90,9 +95,20 @@ WebGLEngine.create({ trailEntity.addComponent(MoveScript); - // engine.run(); + // Load trail texture + engine.resourceManager + .load({ + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*-DEWQZ0ncrEAAAAASTAAAAgAeil6AQ/original", + type: AssetType.Texture2D + }) + .then((texture) => { + // Set texture on trail material + material.baseTexture = texture; - // Run for e2e testing - updateForE2E(engine, 50, 20); - initScreenshot(engine, camera); + engine.run(); + + // // Run for e2e testing + // updateForE2E(engine, 50, 20); + // initScreenshot(engine, camera); + }); }); diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 2892f9ef2e..066f38bc76 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -15,7 +15,6 @@ import { MeshTopology } from "../graphic/enums/MeshTopology"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; import { deepClone, ignoreClone } from "../clone/CloneManager"; import { ShaderProperty } from "../shader/ShaderProperty"; -import { TrailMaterial } from "./TrailMaterial"; import { ParticleCompositeCurve } from "../particle/modules/ParticleCompositeCurve"; import { ParticleGradient } from "../particle/modules/ParticleGradient"; import { TrailTextureMode } from "./enums/TrailTextureMode"; @@ -137,9 +136,6 @@ export class TrailRenderer extends Renderer { constructor(entity: Entity) { super(entity); this._initGeometry(); - - // Set default material - this.setMaterial(new TrailMaterial(this.engine)); } /** From cdaec88bab64e8051bfbc54d29bb8e09c855b82b Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 01:02:48 +0800 Subject: [PATCH 06/85] feat: add trail shader support in ShaderPool --- packages/core/src/shader/ShaderPool.ts | 3 ++ .../{trail => shaderlib/extra}/trail.fs.glsl | 0 .../{trail => shaderlib/extra}/trail.vs.glsl | 0 packages/core/src/trail/TrailMaterial.ts | 49 +++++++------------ 4 files changed, 22 insertions(+), 30 deletions(-) rename packages/core/src/{trail => shaderlib/extra}/trail.fs.glsl (100%) rename packages/core/src/{trail => shaderlib/extra}/trail.vs.glsl (100%) diff --git a/packages/core/src/shader/ShaderPool.ts b/packages/core/src/shader/ShaderPool.ts index 17ece94f0b..080b0e50dc 100644 --- a/packages/core/src/shader/ShaderPool.ts +++ b/packages/core/src/shader/ShaderPool.ts @@ -26,6 +26,8 @@ import spriteFs from "../shaderlib/extra/sprite.fs.glsl"; import spriteVs from "../shaderlib/extra/sprite.vs.glsl"; import textFs from "../shaderlib/extra/text.fs.glsl"; import textVs from "../shaderlib/extra/text.vs.glsl"; +import trailFs from "../shaderlib/extra/trail.fs.glsl"; +import trailVs from "../shaderlib/extra/trail.vs.glsl"; import unlitFs from "../shaderlib/extra/unlit.fs.glsl"; import unlitVs from "../shaderlib/extra/unlit.vs.glsl"; import { Shader } from "./Shader"; @@ -72,6 +74,7 @@ export class ShaderPool { Shader.create("SkyProcedural", [new ShaderPass("Forward", skyProceduralVs, skyProceduralFs, forwardPassTags)]); Shader.create("particle-shader", [new ShaderPass("Forward", particleVs, particleFs, forwardPassTags)]); + Shader.create("trail", [new ShaderPass("Forward", trailVs, trailFs, forwardPassTags)]); Shader.create("SpriteMask", [new ShaderPass("Forward", spriteMaskVs, spriteMaskFs, forwardPassTags)]); Shader.create("Sprite", [new ShaderPass("Forward", spriteVs, spriteFs, forwardPassTags)]); Shader.create("Text", [new ShaderPass("Forward", textVs, textFs, forwardPassTags)]); diff --git a/packages/core/src/trail/trail.fs.glsl b/packages/core/src/shaderlib/extra/trail.fs.glsl similarity index 100% rename from packages/core/src/trail/trail.fs.glsl rename to packages/core/src/shaderlib/extra/trail.fs.glsl diff --git a/packages/core/src/trail/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl similarity index 100% rename from packages/core/src/trail/trail.vs.glsl rename to packages/core/src/shaderlib/extra/trail.vs.glsl diff --git a/packages/core/src/trail/TrailMaterial.ts b/packages/core/src/trail/TrailMaterial.ts index 9faacb4183..3182eaf626 100644 --- a/packages/core/src/trail/TrailMaterial.ts +++ b/packages/core/src/trail/TrailMaterial.ts @@ -1,41 +1,22 @@ import { Color } from "@galacean/engine-math"; import { Engine } from "../Engine"; -import { Material } from "../material/Material"; -import { BlendFactor, CullMode, Shader, ShaderPass, SubShader } from "../shader"; +import { BaseMaterial } from "../material/BaseMaterial"; +import { BlendFactor, CullMode, Shader } from "../shader"; import { Texture2D } from "../texture"; -import { ShaderMacro } from "../shader/ShaderMacro"; -import { ShaderProperty } from "../shader/ShaderProperty"; -import FRAG_SHADER from "./trail.fs.glsl"; -import VERT_SHADER from "./trail.vs.glsl"; /** * Trail material. */ -export class TrailMaterial extends Material { - private static _baseTextureMacro: ShaderMacro = ShaderMacro.getByName("MATERIAL_HAS_BASETEXTURE"); - private static _baseColorProp: ShaderProperty = ShaderProperty.getByName("material_BaseColor"); - private static _baseTextureProp: ShaderProperty = ShaderProperty.getByName("material_BaseTexture"); - - private static _isShaderCreated = false; - - private static _createShader(): void { - if (TrailMaterial._isShaderCreated) return; - - const shaderPass = new ShaderPass(VERT_SHADER, FRAG_SHADER); - const subShader = new SubShader("default", [shaderPass]); - Shader.create("trail", [subShader]); - TrailMaterial._isShaderCreated = true; - } - +export class TrailMaterial extends BaseMaterial { /** * Base color. */ get baseColor(): Color { - return this.shaderData.getColor(TrailMaterial._baseColorProp); + return this.shaderData.getColor(BaseMaterial._baseColorProp); } set baseColor(value: Color) { - const baseColor = this.shaderData.getColor(TrailMaterial._baseColorProp); + const baseColor = this.shaderData.getColor(BaseMaterial._baseColorProp); if (value !== baseColor) { baseColor.copyFrom(value); } @@ -45,15 +26,15 @@ export class TrailMaterial extends Material { * Base texture. */ get baseTexture(): Texture2D { - return this.shaderData.getTexture(TrailMaterial._baseTextureProp); + return this.shaderData.getTexture(BaseMaterial._baseTextureProp); } set baseTexture(value: Texture2D) { - this.shaderData.setTexture(TrailMaterial._baseTextureProp, value); + this.shaderData.setTexture(BaseMaterial._baseTextureProp, value); if (value) { - this.shaderData.enableMacro(TrailMaterial._baseTextureMacro); + this.shaderData.enableMacro(BaseMaterial._baseTextureMacro); } else { - this.shaderData.disableMacro(TrailMaterial._baseTextureMacro); + this.shaderData.disableMacro(BaseMaterial._baseTextureMacro); } } @@ -62,11 +43,10 @@ export class TrailMaterial extends Material { * @param engine - Engine to which the material belongs */ constructor(engine: Engine) { - TrailMaterial._createShader(); super(engine, Shader.find("trail")); const shaderData = this.shaderData; - shaderData.setColor(TrailMaterial._baseColorProp, new Color(1, 1, 1, 1)); + shaderData.setColor(BaseMaterial._baseColorProp, new Color(1, 1, 1, 1)); // Default blend state for additive blending const target = this.renderState.blendState.targetBlendState; @@ -82,4 +62,13 @@ export class TrailMaterial extends Material { // Disable culling for double-sided rendering this.renderState.rasterState.cullMode = CullMode.Off; } + + /** + * @inheritdoc + */ + override clone(): TrailMaterial { + const dest = new TrailMaterial(this._engine); + this._cloneToAndModifyName(dest); + return dest; + } } From 0fe1e949e8fa42f6533b58d36c6aaa635ba3a19a Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 01:06:14 +0800 Subject: [PATCH 07/85] feat: add emissive color and texture support in TrailMaterial --- .../core/src/shaderlib/extra/trail.fs.glsl | 19 ++++++++++-- packages/core/src/trail/TrailMaterial.ts | 31 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.fs.glsl b/packages/core/src/shaderlib/extra/trail.fs.glsl index d89a21ca92..4c009b9153 100644 --- a/packages/core/src/shaderlib/extra/trail.fs.glsl +++ b/packages/core/src/shaderlib/extra/trail.fs.glsl @@ -9,13 +9,26 @@ uniform vec4 material_BaseColor; uniform sampler2D material_BaseTexture; #endif +uniform mediump vec3 material_EmissiveColor; +#ifdef MATERIAL_HAS_EMISSIVETEXTURE + uniform sampler2D material_EmissiveTexture; +#endif + void main() { - vec4 baseColor = material_BaseColor * v_color; + vec4 color = material_BaseColor * v_color; #ifdef MATERIAL_HAS_BASETEXTURE - baseColor *= texture2DSRGB(material_BaseTexture, v_uv); + color *= texture2DSRGB(material_BaseTexture, v_uv); #endif - gl_FragColor = baseColor; + // Emissive + vec3 emissiveRadiance = material_EmissiveColor; + #ifdef MATERIAL_HAS_EMISSIVETEXTURE + emissiveRadiance *= texture2DSRGB(material_EmissiveTexture, v_uv).rgb; + #endif + + color.rgb += emissiveRadiance; + + gl_FragColor = color; } diff --git a/packages/core/src/trail/TrailMaterial.ts b/packages/core/src/trail/TrailMaterial.ts index 3182eaf626..4ac33ed506 100644 --- a/packages/core/src/trail/TrailMaterial.ts +++ b/packages/core/src/trail/TrailMaterial.ts @@ -38,6 +38,36 @@ export class TrailMaterial extends BaseMaterial { } } + /** + * Emissive color. + */ + get emissiveColor(): Color { + return this.shaderData.getColor(BaseMaterial._emissiveColorProp); + } + + set emissiveColor(value: Color) { + const emissiveColor = this.shaderData.getColor(BaseMaterial._emissiveColorProp); + if (value !== emissiveColor) { + emissiveColor.copyFrom(value); + } + } + + /** + * Emissive texture. + */ + get emissiveTexture(): Texture2D { + return this.shaderData.getTexture(BaseMaterial._emissiveTextureProp); + } + + set emissiveTexture(value: Texture2D) { + this.shaderData.setTexture(BaseMaterial._emissiveTextureProp, value); + if (value) { + this.shaderData.enableMacro(BaseMaterial._emissiveTextureMacro); + } else { + this.shaderData.disableMacro(BaseMaterial._emissiveTextureMacro); + } + } + /** * Create a trail material instance. * @param engine - Engine to which the material belongs @@ -47,6 +77,7 @@ export class TrailMaterial extends BaseMaterial { const shaderData = this.shaderData; shaderData.setColor(BaseMaterial._baseColorProp, new Color(1, 1, 1, 1)); + shaderData.setColor(BaseMaterial._emissiveColorProp, new Color(0, 0, 0, 1)); // Default blend state for additive blending const target = this.renderState.blendState.targetBlendState; From 2c646ad930009fd9488232cf4fbaeab4ac7712eb Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 01:12:13 +0800 Subject: [PATCH 08/85] feat: refactor TrailMaterial to use new blend and render face enums --- packages/core/src/trail/TrailMaterial.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/core/src/trail/TrailMaterial.ts b/packages/core/src/trail/TrailMaterial.ts index 4ac33ed506..acdbf29992 100644 --- a/packages/core/src/trail/TrailMaterial.ts +++ b/packages/core/src/trail/TrailMaterial.ts @@ -1,7 +1,9 @@ import { Color } from "@galacean/engine-math"; import { Engine } from "../Engine"; import { BaseMaterial } from "../material/BaseMaterial"; -import { BlendFactor, CullMode, Shader } from "../shader"; +import { BlendMode } from "../material/enums/BlendMode"; +import { RenderFace } from "../material/enums/RenderFace"; +import { Shader } from "../shader"; import { Texture2D } from "../texture"; /** @@ -79,19 +81,8 @@ export class TrailMaterial extends BaseMaterial { shaderData.setColor(BaseMaterial._baseColorProp, new Color(1, 1, 1, 1)); shaderData.setColor(BaseMaterial._emissiveColorProp, new Color(0, 0, 0, 1)); - // Default blend state for additive blending - const target = this.renderState.blendState.targetBlendState; - target.enabled = true; - target.sourceColorBlendFactor = BlendFactor.SourceAlpha; - target.destinationColorBlendFactor = BlendFactor.One; - target.sourceAlphaBlendFactor = BlendFactor.SourceAlpha; - target.destinationAlphaBlendFactor = BlendFactor.One; - - // Disable depth write for transparent rendering - this.renderState.depthState.writeEnabled = false; - - // Disable culling for double-sided rendering - this.renderState.rasterState.cullMode = CullMode.Off; + this.isTransparent = true; + this.renderFace = RenderFace.Double; } /** From 7c7361888c255d035b16a4cc47aaeec2d028186b Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 01:13:48 +0800 Subject: [PATCH 09/85] feat: remove unused imports and set render face to default in TrailMaterial --- packages/core/src/trail/TrailMaterial.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/core/src/trail/TrailMaterial.ts b/packages/core/src/trail/TrailMaterial.ts index acdbf29992..07e44f9bc3 100644 --- a/packages/core/src/trail/TrailMaterial.ts +++ b/packages/core/src/trail/TrailMaterial.ts @@ -1,8 +1,6 @@ import { Color } from "@galacean/engine-math"; import { Engine } from "../Engine"; import { BaseMaterial } from "../material/BaseMaterial"; -import { BlendMode } from "../material/enums/BlendMode"; -import { RenderFace } from "../material/enums/RenderFace"; import { Shader } from "../shader"; import { Texture2D } from "../texture"; @@ -82,7 +80,6 @@ export class TrailMaterial extends BaseMaterial { shaderData.setColor(BaseMaterial._emissiveColorProp, new Color(0, 0, 0, 1)); this.isTransparent = true; - this.renderFace = RenderFace.Double; } /** From 9be80c4127ec374de51d64c0b172496dbf09649d Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 01:20:10 +0800 Subject: [PATCH 10/85] feat: refactor ParticleMaterial and TrailMaterial to extend EffectMaterial --- packages/core/src/material/EffectMaterial.ts | 86 +++++++++++++++++++ .../core/src/particle/ParticleMaterial.ts | 74 +--------------- packages/core/src/trail/TrailMaterial.ts | 72 +--------------- 3 files changed, 91 insertions(+), 141 deletions(-) create mode 100644 packages/core/src/material/EffectMaterial.ts diff --git a/packages/core/src/material/EffectMaterial.ts b/packages/core/src/material/EffectMaterial.ts new file mode 100644 index 0000000000..17873323d6 --- /dev/null +++ b/packages/core/src/material/EffectMaterial.ts @@ -0,0 +1,86 @@ +import { Color } from "@galacean/engine-math"; +import { Engine } from "../Engine"; +import { Shader } from "../shader"; +import { Texture2D } from "../texture"; +import { BaseMaterial } from "./BaseMaterial"; + +/** + * Base material for visual effects like particles and trails. + */ +export class EffectMaterial extends BaseMaterial { + /** + * Base color. + */ + get baseColor(): Color { + return this.shaderData.getColor(BaseMaterial._baseColorProp); + } + + set baseColor(value: Color) { + const baseColor = this.shaderData.getColor(BaseMaterial._baseColorProp); + if (value !== baseColor) { + baseColor.copyFrom(value); + } + } + + /** + * Base texture. + */ + get baseTexture(): Texture2D { + return this.shaderData.getTexture(BaseMaterial._baseTextureProp); + } + + set baseTexture(value: Texture2D) { + this.shaderData.setTexture(BaseMaterial._baseTextureProp, value); + if (value) { + this.shaderData.enableMacro(BaseMaterial._baseTextureMacro); + } else { + this.shaderData.disableMacro(BaseMaterial._baseTextureMacro); + } + } + + /** + * Emissive color. + */ + get emissiveColor(): Color { + return this.shaderData.getColor(BaseMaterial._emissiveColorProp); + } + + set emissiveColor(value: Color) { + const emissiveColor = this.shaderData.getColor(BaseMaterial._emissiveColorProp); + if (value !== emissiveColor) { + emissiveColor.copyFrom(value); + } + } + + /** + * Emissive texture. + */ + get emissiveTexture(): Texture2D { + return this.shaderData.getTexture(BaseMaterial._emissiveTextureProp); + } + + set emissiveTexture(value: Texture2D) { + this.shaderData.setTexture(BaseMaterial._emissiveTextureProp, value); + if (value) { + this.shaderData.enableMacro(BaseMaterial._emissiveTextureMacro); + } else { + this.shaderData.disableMacro(BaseMaterial._emissiveTextureMacro); + } + } + + /** + * Create an effect material instance. + * @param engine - Engine to which the material belongs + * @param shader - Shader used by the material + */ + constructor(engine: Engine, shader: Shader) { + super(engine, shader); + + const shaderData = this.shaderData; + shaderData.setColor(BaseMaterial._baseColorProp, new Color(1, 1, 1, 1)); + shaderData.setColor(BaseMaterial._emissiveColorProp, new Color(0, 0, 0, 1)); + + this.isTransparent = true; + } +} + diff --git a/packages/core/src/particle/ParticleMaterial.ts b/packages/core/src/particle/ParticleMaterial.ts index bb950d9c66..36c95a8547 100644 --- a/packages/core/src/particle/ParticleMaterial.ts +++ b/packages/core/src/particle/ParticleMaterial.ts @@ -1,85 +1,17 @@ -import { Color } from "@galacean/engine-math"; import { Engine } from "../Engine"; -import { BaseMaterial } from "../material/BaseMaterial"; +import { EffectMaterial } from "../material/EffectMaterial"; import { Shader } from "../shader/Shader"; -import { Texture2D } from "../texture/Texture2D"; /** * Particle Material. */ -export class ParticleMaterial extends BaseMaterial { +export class ParticleMaterial extends EffectMaterial { /** - * Base color. - */ - get baseColor(): Color { - return this.shaderData.getColor(BaseMaterial._baseColorProp); - } - - set baseColor(value: Color) { - const baseColor = this.shaderData.getColor(BaseMaterial._baseColorProp); - if (value !== baseColor) { - baseColor.copyFrom(value); - } - } - - /** - * Base texture. - */ - get baseTexture(): Texture2D { - return this.shaderData.getTexture(BaseMaterial._baseTextureProp); - } - - set baseTexture(value: Texture2D) { - this.shaderData.setTexture(BaseMaterial._baseTextureProp, value); - if (value) { - this.shaderData.enableMacro(BaseMaterial._baseTextureMacro); - } else { - this.shaderData.disableMacro(BaseMaterial._baseTextureMacro); - } - } - - /** - * Emissive color. - */ - get emissiveColor(): Color { - return this.shaderData.getColor(BaseMaterial._emissiveColorProp); - } - - set emissiveColor(value: Color) { - const emissiveColor = this.shaderData.getColor(BaseMaterial._emissiveColorProp); - if (value !== emissiveColor) { - emissiveColor.copyFrom(value); - } - } - - /** - * Emissive texture. - */ - get emissiveTexture(): Texture2D { - return this.shaderData.getTexture(BaseMaterial._emissiveTextureProp); - } - - set emissiveTexture(value: Texture2D) { - this.shaderData.setTexture(BaseMaterial._emissiveTextureProp, value); - if (value) { - this.shaderData.enableMacro(BaseMaterial._emissiveTextureMacro); - } else { - this.shaderData.disableMacro(BaseMaterial._emissiveTextureMacro); - } - } - - /** - * Create a unlit material instance. + * Create a particle material instance. * @param engine - Engine to which the material belongs */ constructor(engine: Engine) { super(engine, Shader.find("particle-shader")); - - const shaderData = this.shaderData; - shaderData.setColor(BaseMaterial._baseColorProp, new Color(1, 1, 1, 1)); - shaderData.setColor(BaseMaterial._emissiveColorProp, new Color(0, 0, 0, 1)); - - this.isTransparent = true; } /** diff --git a/packages/core/src/trail/TrailMaterial.ts b/packages/core/src/trail/TrailMaterial.ts index 07e44f9bc3..001c4f3c07 100644 --- a/packages/core/src/trail/TrailMaterial.ts +++ b/packages/core/src/trail/TrailMaterial.ts @@ -1,85 +1,17 @@ -import { Color } from "@galacean/engine-math"; import { Engine } from "../Engine"; -import { BaseMaterial } from "../material/BaseMaterial"; +import { EffectMaterial } from "../material/EffectMaterial"; import { Shader } from "../shader"; -import { Texture2D } from "../texture"; /** * Trail material. */ -export class TrailMaterial extends BaseMaterial { - /** - * Base color. - */ - get baseColor(): Color { - return this.shaderData.getColor(BaseMaterial._baseColorProp); - } - - set baseColor(value: Color) { - const baseColor = this.shaderData.getColor(BaseMaterial._baseColorProp); - if (value !== baseColor) { - baseColor.copyFrom(value); - } - } - - /** - * Base texture. - */ - get baseTexture(): Texture2D { - return this.shaderData.getTexture(BaseMaterial._baseTextureProp); - } - - set baseTexture(value: Texture2D) { - this.shaderData.setTexture(BaseMaterial._baseTextureProp, value); - if (value) { - this.shaderData.enableMacro(BaseMaterial._baseTextureMacro); - } else { - this.shaderData.disableMacro(BaseMaterial._baseTextureMacro); - } - } - - /** - * Emissive color. - */ - get emissiveColor(): Color { - return this.shaderData.getColor(BaseMaterial._emissiveColorProp); - } - - set emissiveColor(value: Color) { - const emissiveColor = this.shaderData.getColor(BaseMaterial._emissiveColorProp); - if (value !== emissiveColor) { - emissiveColor.copyFrom(value); - } - } - - /** - * Emissive texture. - */ - get emissiveTexture(): Texture2D { - return this.shaderData.getTexture(BaseMaterial._emissiveTextureProp); - } - - set emissiveTexture(value: Texture2D) { - this.shaderData.setTexture(BaseMaterial._emissiveTextureProp, value); - if (value) { - this.shaderData.enableMacro(BaseMaterial._emissiveTextureMacro); - } else { - this.shaderData.disableMacro(BaseMaterial._emissiveTextureMacro); - } - } - +export class TrailMaterial extends EffectMaterial { /** * Create a trail material instance. * @param engine - Engine to which the material belongs */ constructor(engine: Engine) { super(engine, Shader.find("trail")); - - const shaderData = this.shaderData; - shaderData.setColor(BaseMaterial._baseColorProp, new Color(1, 1, 1, 1)); - shaderData.setColor(BaseMaterial._emissiveColorProp, new Color(0, 0, 0, 1)); - - this.isTransparent = true; } /** From b502942da8f63e7ed4234b86d87cd331eadfbf79 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 01:26:54 +0800 Subject: [PATCH 11/85] feat: enhance TrailRenderer to optimize vertex buffer updates and manage new points --- packages/core/src/trail/TrailRenderer.ts | 52 +++++++++++++++++------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 066f38bc76..d235e7d73f 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -106,11 +106,15 @@ export class TrailRenderer extends Renderer { @ignoreClone private _firstActiveElement: number = 0; @ignoreClone + private _firstNewElement: number = 0; + @ignoreClone private _firstFreeElement: number = 0; @ignoreClone private _currentPointCount: number = 0; @ignoreClone private _maxPointCount: number = 256; + @ignoreClone + private _vertexBufferNeedsUpdate: boolean = false; // Last recorded position @ignoreClone @@ -143,6 +147,7 @@ export class TrailRenderer extends Renderer { */ clear(): void { this._firstActiveElement = 0; + this._firstNewElement = 0; this._firstFreeElement = 0; this._currentPointCount = 0; this._hasLastPosition = false; @@ -189,8 +194,14 @@ export class TrailRenderer extends Renderer { return; // Need at least 2 points to form a segment } - // Update vertex buffer with new points - this._updateVertexBuffer(); + // Only update vertex buffer when there are new points or buffer needs full update + if ( + this._firstNewElement !== this._firstFreeElement || + this._vertexBufferNeedsUpdate || + this._vertexBuffer.isContentLost + ) { + this._uploadNewVertices(); + } // Update index buffer to handle ring buffer wrap-around const indexCount = this._updateIndexBuffer(activeCount); @@ -406,6 +417,8 @@ export class TrailRenderer extends Renderer { const offset = vertexIdx * floatStride; tangent.copyToArray(vertices, offset + 10); // Update a_Tangent } + // First point's tangent was updated, need to re-upload it + this._firstNewElement = this._firstActiveElement; } } else { // First point - use forward direction (will be updated when second point is added) @@ -438,33 +451,38 @@ export class TrailRenderer extends Renderer { return this._currentPointCount; } - private _updateVertexBuffer(): void { + private _uploadNewVertices(): void { const firstActive = this._firstActiveElement; const firstFree = this._firstFreeElement; + const buffer = this._vertexBuffer; - if (this._currentPointCount === 0) { - return; // No active points + // If buffer content is lost, need to re-upload all active vertices + const needFullUpload = this._vertexBufferNeedsUpdate || buffer.isContentLost; + const firstNew = needFullUpload ? firstActive : this._firstNewElement; + + // No vertices to upload + if (firstNew === firstFree) { + return; } const byteStride = TrailRenderer.VERTEX_STRIDE; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; - const buffer = this._vertexBuffer; const vertices = this._vertices; // Each point has 2 vertices - if (firstActive < firstFree) { - // Contiguous range - create a view of the relevant portion - const startFloat = firstActive * 2 * floatStride; - const countFloat = (firstFree - firstActive) * 2 * floatStride; + if (firstNew < firstFree) { + // Contiguous range - only upload new vertices + const startFloat = firstNew * 2 * floatStride; + const countFloat = (firstFree - firstNew) * 2 * floatStride; const subArray = new Float32Array(vertices.buffer, startFloat * 4, countFloat); - buffer.setData(subArray, firstActive * 2 * byteStride); + buffer.setData(subArray, firstNew * 2 * byteStride); } else { // Wrapped range - upload in two parts - // First segment: from firstActive to end - const startFloat1 = firstActive * 2 * floatStride; - const countFloat1 = (this._maxPointCount - firstActive) * 2 * floatStride; + // First segment: from firstNew to end + const startFloat1 = firstNew * 2 * floatStride; + const countFloat1 = (this._maxPointCount - firstNew) * 2 * floatStride; const subArray1 = new Float32Array(vertices.buffer, startFloat1 * 4, countFloat1); - buffer.setData(subArray1, firstActive * 2 * byteStride); + buffer.setData(subArray1, firstNew * 2 * byteStride); // Second segment: from 0 to firstFree if (firstFree > 0) { @@ -473,6 +491,10 @@ export class TrailRenderer extends Renderer { buffer.setData(subArray2, 0); } } + + // Update the new element pointer to match free element + this._firstNewElement = firstFree; + this._vertexBufferNeedsUpdate = false; } private _updateIndexBuffer(activeCount: number): number { From 1ddda45f0ddfdb2d85aca6378d7c925ab4a920f8 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 01:33:23 +0800 Subject: [PATCH 12/85] no message --- e2e/case/trailRenderer-basic.ts | 8 ++--- .../originImage/Trail_trailRenderer-basic.jpg | 4 +-- packages/core/src/trail/TrailRenderer.ts | 36 ++++++++----------- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/e2e/case/trailRenderer-basic.ts b/e2e/case/trailRenderer-basic.ts index d88545bdf4..137329b2b0 100644 --- a/e2e/case/trailRenderer-basic.ts +++ b/e2e/case/trailRenderer-basic.ts @@ -105,10 +105,10 @@ WebGLEngine.create({ // Set texture on trail material material.baseTexture = texture; - engine.run(); + // engine.run(); - // // Run for e2e testing - // updateForE2E(engine, 50, 20); - // initScreenshot(engine, camera); + // Run for e2e testing + updateForE2E(engine, 50, 20); + initScreenshot(engine, camera); }); }); diff --git a/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg b/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg index 7255c6434f..d4c04376e1 100644 --- a/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg +++ b/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:173791302147c77c075338bd609a920869353001e2ca991ada3e0270691b8f16 -size 27481 +oid sha256:cb1a31551a907e4b71000e74f6a87b09cb5ace36c6e79e63afec6c6b7a3eee92 +size 21056 diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index d235e7d73f..5a9a72af9d 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -110,11 +110,7 @@ export class TrailRenderer extends Renderer { @ignoreClone private _firstFreeElement: number = 0; @ignoreClone - private _currentPointCount: number = 0; - @ignoreClone private _maxPointCount: number = 256; - @ignoreClone - private _vertexBufferNeedsUpdate: boolean = false; // Last recorded position @ignoreClone @@ -149,7 +145,6 @@ export class TrailRenderer extends Renderer { this._firstActiveElement = 0; this._firstNewElement = 0; this._firstFreeElement = 0; - this._currentPointCount = 0; this._hasLastPosition = false; } @@ -194,12 +189,8 @@ export class TrailRenderer extends Renderer { return; // Need at least 2 points to form a segment } - // Only update vertex buffer when there are new points or buffer needs full update - if ( - this._firstNewElement !== this._firstFreeElement || - this._vertexBufferNeedsUpdate || - this._vertexBuffer.isContentLost - ) { + // Only update vertex buffer when there are new points or buffer content is lost + if (this._firstNewElement !== this._firstFreeElement || this._vertexBuffer.isContentLost) { this._uploadNewVertices(); } @@ -232,8 +223,9 @@ export class TrailRenderer extends Renderer { const vertices = this._vertices; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const halfWidth = this.width * 0.5; + const activeCount = this._getActivePointCount(); - if (this._currentPointCount === 0) { + if (activeCount === 0) { // No active points, use current entity position const worldPosition = this.entity.transform.worldPosition; worldBounds.min.set(worldPosition.x - halfWidth, worldPosition.y - halfWidth, worldPosition.z - halfWidth); @@ -252,7 +244,7 @@ export class TrailRenderer extends Renderer { // Iterate through all active points let idx = this._firstActiveElement; - for (let i = 0; i < this._currentPointCount; i++) { + for (let i = 0; i < activeCount; i++) { const offset = idx * 2 * floatStride; const x = vertices[offset]; const y = vertices[offset + 1]; @@ -354,11 +346,10 @@ export class TrailRenderer extends Renderer { if (this._firstActiveElement >= this._maxPointCount) { this._firstActiveElement = 0; } - this._currentPointCount--; } // If all points have expired, reset the trail state - if (this._currentPointCount === 0) { + if (this._firstActiveElement === this._firstFreeElement) { this._hasLastPosition = false; } } @@ -386,7 +377,6 @@ export class TrailRenderer extends Renderer { if (this._firstActiveElement >= this._maxPointCount) { this._firstActiveElement = 0; } - this._currentPointCount--; } // Add the new point @@ -410,7 +400,7 @@ export class TrailRenderer extends Renderer { // If this is the second point, update the first point's tangent // to match this tangent (so the tail doesn't have wrong orientation) - if (this._currentPointCount === 1) { + if (this._getActivePointCount() === 1) { const firstIdx = this._firstActiveElement; for (let corner = -1; corner <= 1; corner += 2) { const vertexIdx = firstIdx * 2 + (corner === -1 ? 0 : 1); @@ -444,11 +434,15 @@ export class TrailRenderer extends Renderer { if (this._firstFreeElement >= this._maxPointCount) { this._firstFreeElement = 0; } - this._currentPointCount++; } private _getActivePointCount(): number { - return this._currentPointCount; + const firstActive = this._firstActiveElement; + const firstFree = this._firstFreeElement; + if (firstFree >= firstActive) { + return firstFree - firstActive; + } + return this._maxPointCount - firstActive + firstFree; } private _uploadNewVertices(): void { @@ -457,8 +451,7 @@ export class TrailRenderer extends Renderer { const buffer = this._vertexBuffer; // If buffer content is lost, need to re-upload all active vertices - const needFullUpload = this._vertexBufferNeedsUpdate || buffer.isContentLost; - const firstNew = needFullUpload ? firstActive : this._firstNewElement; + const firstNew = buffer.isContentLost ? firstActive : this._firstNewElement; // No vertices to upload if (firstNew === firstFree) { @@ -494,7 +487,6 @@ export class TrailRenderer extends Renderer { // Update the new element pointer to match free element this._firstNewElement = firstFree; - this._vertexBufferNeedsUpdate = false; } private _updateIndexBuffer(activeCount: number): number { From 451a2dedbc303a8271935048c988440377732406 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 01:41:57 +0800 Subject: [PATCH 13/85] feat: optimize bounds calculation in TrailRenderer with caching and dirty flag --- packages/core/src/trail/TrailRenderer.ts | 119 +++++++++++++++++------ 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 5a9a72af9d..fd03d2276c 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -122,6 +122,14 @@ export class TrailRenderer extends Renderer { @ignoreClone private _playTime: number = 0; + // Bounds optimization: cached bounds and dirty flag + @ignoreClone + private _boundsMin: Vector3 = new Vector3(); + @ignoreClone + private _boundsMax: Vector3 = new Vector3(); + @ignoreClone + private _boundsDirty: boolean = true; + // Shader data cache @ignoreClone private _widthCurveData: Float32Array; @@ -146,6 +154,7 @@ export class TrailRenderer extends Renderer { this._firstNewElement = 0; this._firstFreeElement = 0; this._hasLastPosition = false; + this._boundsDirty = true; } /** @@ -220,10 +229,8 @@ export class TrailRenderer extends Renderer { * @internal */ protected override _updateBounds(worldBounds: BoundingBox): void { - const vertices = this._vertices; - const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; - const halfWidth = this.width * 0.5; const activeCount = this._getActivePointCount(); + const halfWidth = this.width * 0.5; if (activeCount === 0) { // No active points, use current entity position @@ -233,37 +240,15 @@ export class TrailRenderer extends Renderer { return; } - // Initialize with first active point - const firstOffset = this._firstActiveElement * 2 * floatStride; - let minX = vertices[firstOffset]; - let minY = vertices[firstOffset + 1]; - let minZ = vertices[firstOffset + 2]; - let maxX = minX; - let maxY = minY; - let maxZ = minZ; - - // Iterate through all active points - let idx = this._firstActiveElement; - for (let i = 0; i < activeCount; i++) { - const offset = idx * 2 * floatStride; - const x = vertices[offset]; - const y = vertices[offset + 1]; - const z = vertices[offset + 2]; - - if (x < minX) minX = x; - if (y < minY) minY = y; - if (z < minZ) minZ = z; - if (x > maxX) maxX = x; - if (y > maxY) maxY = y; - if (z > maxZ) maxZ = z; - - idx++; - if (idx >= this._maxPointCount) idx = 0; + // Recalculate bounds only when dirty + if (this._boundsDirty) { + this._recalculateBounds(); } - // Expand bounds by half width (for billboard offset) - worldBounds.min.set(minX - halfWidth, minY - halfWidth, minZ - halfWidth); - worldBounds.max.set(maxX + halfWidth, maxY + halfWidth, maxZ + halfWidth); + // Apply half width offset to cached bounds + const { _boundsMin: min, _boundsMax: max } = this; + worldBounds.min.set(min.x - halfWidth, min.y - halfWidth, min.z - halfWidth); + worldBounds.max.set(max.x + halfWidth, max.y + halfWidth, max.z + halfWidth); } /** @@ -331,6 +316,7 @@ export class TrailRenderer extends Renderer { private _retireActivePoints(): void { const currentTime = this._playTime; const lifetime = this.time; + const firstActiveOld = this._firstActiveElement; // Move firstActiveElement forward for points that have expired while (this._firstActiveElement !== this._firstFreeElement) { @@ -348,6 +334,11 @@ export class TrailRenderer extends Renderer { } } + // If points were retired, bounds need recalculation + if (this._firstActiveElement !== firstActiveOld) { + this._boundsDirty = true; + } + // If all points have expired, reset the trail state if (this._firstActiveElement === this._firstFreeElement) { this._hasLastPosition = false; @@ -429,6 +420,9 @@ export class TrailRenderer extends Renderer { tangent.copyToArray(vertices, offset + 10); // a_Tangent (vec3) } + // Expand cached bounds with new point (incremental update) + this._expandBounds(position); + // Update pointers this._firstFreeElement++; if (this._firstFreeElement >= this._maxPointCount) { @@ -445,6 +439,67 @@ export class TrailRenderer extends Renderer { return this._maxPointCount - firstActive + firstFree; } + private _expandBounds(position: Vector3): void { + const { _boundsMin: min, _boundsMax: max } = this; + + // If bounds are dirty (need full recalc), initialize with first point + if (this._boundsDirty) { + min.copyFrom(position); + max.copyFrom(position); + this._boundsDirty = false; + return; + } + + // Expand bounds incrementally + const { x, y, z } = position; + if (x < min.x) min.x = x; + if (y < min.y) min.y = y; + if (z < min.z) min.z = z; + if (x > max.x) max.x = x; + if (y > max.y) max.y = y; + if (z > max.z) max.z = z; + } + + private _recalculateBounds(): void { + const vertices = this._vertices; + const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const activeCount = this._getActivePointCount(); + const { _boundsMin: min, _boundsMax: max } = this; + + if (activeCount === 0) { + min.set(0, 0, 0); + max.set(0, 0, 0); + this._boundsDirty = false; + return; + } + + // Initialize with first active point + const firstOffset = this._firstActiveElement * 2 * floatStride; + min.set(vertices[firstOffset], vertices[firstOffset + 1], vertices[firstOffset + 2]); + max.copyFrom(min); + + // Iterate through all active points + let idx = this._firstActiveElement; + for (let i = 0; i < activeCount; i++) { + const offset = idx * 2 * floatStride; + const x = vertices[offset]; + const y = vertices[offset + 1]; + const z = vertices[offset + 2]; + + if (x < min.x) min.x = x; + if (y < min.y) min.y = y; + if (z < min.z) min.z = z; + if (x > max.x) max.x = x; + if (y > max.y) max.y = y; + if (z > max.z) max.z = z; + + idx++; + if (idx >= this._maxPointCount) idx = 0; + } + + this._boundsDirty = false; + } + private _uploadNewVertices(): void { const firstActive = this._firstActiveElement; const firstFree = this._firstFreeElement; From 9f2421056e0942a03ff2aba68bc3f66ae3c6d202 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 01:50:08 +0800 Subject: [PATCH 14/85] feat: optimize bounds calculation and shader data updates in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 65 +++++++++++------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index fd03d2276c..422022c4a7 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -2,6 +2,7 @@ import { BoundingBox, Color, Vector3 } from "@galacean/engine-math"; import { Entity } from "../Entity"; import { Renderer } from "../Renderer"; import { RenderContext } from "../RenderPipeline/RenderContext"; +import { deepClone, ignoreClone } from "../clone/CloneManager"; import { Buffer } from "../graphic/Buffer"; import { IndexBufferBinding } from "../graphic/IndexBufferBinding"; import { Primitive } from "../graphic/Primitive"; @@ -13,10 +14,10 @@ import { BufferUsage } from "../graphic/enums/BufferUsage"; import { IndexFormat } from "../graphic/enums/IndexFormat"; import { MeshTopology } from "../graphic/enums/MeshTopology"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; -import { ShaderProperty } from "../shader/ShaderProperty"; import { ParticleCompositeCurve } from "../particle/modules/ParticleCompositeCurve"; import { ParticleGradient } from "../particle/modules/ParticleGradient"; +import { ShaderData } from "../shader/ShaderData"; +import { ShaderProperty } from "../shader/ShaderProperty"; import { TrailTextureMode } from "./enums/TrailTextureMode"; /** @@ -451,13 +452,8 @@ export class TrailRenderer extends Renderer { } // Expand bounds incrementally - const { x, y, z } = position; - if (x < min.x) min.x = x; - if (y < min.y) min.y = y; - if (z < min.z) min.z = z; - if (x > max.x) max.x = x; - if (y > max.y) max.y = y; - if (z > max.z) max.z = z; + Vector3.min(min, position, min); + Vector3.max(max, position, max); } private _recalculateBounds(): void { @@ -475,23 +471,19 @@ export class TrailRenderer extends Renderer { // Initialize with first active point const firstOffset = this._firstActiveElement * 2 * floatStride; - min.set(vertices[firstOffset], vertices[firstOffset + 1], vertices[firstOffset + 2]); + min.copyFromArray(vertices, firstOffset); max.copyFrom(min); - // Iterate through all active points - let idx = this._firstActiveElement; - for (let i = 0; i < activeCount; i++) { - const offset = idx * 2 * floatStride; - const x = vertices[offset]; - const y = vertices[offset + 1]; - const z = vertices[offset + 2]; + // Iterate through remaining active points + const tempPos = TrailRenderer._tempVector3; + let idx = this._firstActiveElement + 1; + if (idx >= this._maxPointCount) idx = 0; - if (x < min.x) min.x = x; - if (y < min.y) min.y = y; - if (z < min.z) min.z = z; - if (x > max.x) max.x = x; - if (y > max.y) max.y = y; - if (z > max.z) max.z = z; + for (let i = 1; i < activeCount; i++) { + const offset = idx * 2 * floatStride; + tempPos.copyFromArray(vertices, offset); + Vector3.min(min, tempPos, min); + Vector3.max(max, tempPos, max); idx++; if (idx >= this._maxPointCount) idx = 0; @@ -567,7 +559,7 @@ export class TrailRenderer extends Renderer { return indexCount; } - private _updateWidthCurve(shaderData: import("../shader/ShaderData").ShaderData): void { + private _updateWidthCurve(shaderData: ShaderData): void { const curve = this.widthCurve; const widthCurveData = this._widthCurveData || (this._widthCurveData = new Float32Array(8)); @@ -581,9 +573,10 @@ export class TrailRenderer extends Renderer { // Curve mode const keys = curve.curve.keys; const count = Math.min(keys.length, 4); - for (let i = 0; i < count; i++) { - widthCurveData[i * 2] = keys[i].time; - widthCurveData[i * 2 + 1] = keys[i].value; + for (let i = 0, offset = 0; i < count; i++, offset += 2) { + const key = keys[i]; + widthCurveData[offset] = key.time; + widthCurveData[offset + 1] = key.value; } shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurveData); shaderData.setInt(TrailRenderer._widthCurveCountProp, count); @@ -596,7 +589,7 @@ export class TrailRenderer extends Renderer { } } - private _updateColorGradient(shaderData: import("../shader/ShaderData").ShaderData): void { + private _updateColorGradient(shaderData: ShaderData): void { const gradient = this.colorGradient; if (!gradient) { @@ -612,12 +605,12 @@ export class TrailRenderer extends Renderer { // Color keys const colorKeys = gradient.colorKeys; const colorCount = Math.min(colorKeys.length, 4); - for (let i = 0; i < colorCount; i++) { + for (let i = 0, offset = 0; i < colorCount; i++, offset += 4) { const key = colorKeys[i]; - colorKeysData[i * 4] = key.time; - colorKeysData[i * 4 + 1] = key.color.r; - colorKeysData[i * 4 + 2] = key.color.g; - colorKeysData[i * 4 + 3] = key.color.b; + colorKeysData[offset] = key.time; + colorKeysData[offset + 1] = key.color.r; + colorKeysData[offset + 2] = key.color.g; + colorKeysData[offset + 3] = key.color.b; } shaderData.setFloatArray(TrailRenderer._colorKeysProp, colorKeysData); shaderData.setInt(TrailRenderer._colorKeyCountProp, colorCount); @@ -625,10 +618,10 @@ export class TrailRenderer extends Renderer { // Alpha keys const alphaKeys = gradient.alphaKeys; const alphaCount = Math.min(alphaKeys.length, 4); - for (let i = 0; i < alphaCount; i++) { + for (let i = 0, offset = 0; i < alphaCount; i++, offset += 2) { const key = alphaKeys[i]; - alphaKeysData[i * 2] = key.time; - alphaKeysData[i * 2 + 1] = key.alpha; + alphaKeysData[offset] = key.time; + alphaKeysData[offset + 1] = key.alpha; } shaderData.setFloatArray(TrailRenderer._alphaKeysProp, alphaKeysData); shaderData.setInt(TrailRenderer._alphaKeyCountProp, alphaCount); From 3f9ed765124ee09f152a6863593e63161d2e6311 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 01:54:56 +0800 Subject: [PATCH 15/85] feat: remove internal documentation comments and improve variable naming in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 422022c4a7..13971ea623 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -158,9 +158,6 @@ export class TrailRenderer extends Renderer { this._boundsDirty = true; } - /** - * @internal - */ protected override _update(context: RenderContext): void { super._update(context); @@ -190,9 +187,6 @@ export class TrailRenderer extends Renderer { this._updateColorGradient(shaderData); } - /** - * @internal - */ protected override _render(context: RenderContext): void { const activeCount = this._getActivePointCount(); if (activeCount < 2) { @@ -226,9 +220,6 @@ export class TrailRenderer extends Renderer { context.camera._renderPipeline.pushRenderElement(context, renderElement); } - /** - * @internal - */ protected override _updateBounds(worldBounds: BoundingBox): void { const activeCount = this._getActivePointCount(); const halfWidth = this.width * 0.5; @@ -252,9 +243,6 @@ export class TrailRenderer extends Renderer { worldBounds.max.set(max.x + halfWidth, max.y + halfWidth, max.z + halfWidth); } - /** - * @internal - */ protected override _onDestroy(): void { super._onDestroy(); this._vertexBuffer?.destroy(); @@ -475,15 +463,15 @@ export class TrailRenderer extends Renderer { max.copyFrom(min); // Iterate through remaining active points - const tempPos = TrailRenderer._tempVector3; + const pointPosition = TrailRenderer._tempVector3; let idx = this._firstActiveElement + 1; if (idx >= this._maxPointCount) idx = 0; for (let i = 1; i < activeCount; i++) { const offset = idx * 2 * floatStride; - tempPos.copyFromArray(vertices, offset); - Vector3.min(min, tempPos, min); - Vector3.max(max, tempPos, max); + pointPosition.copyFromArray(vertices, offset); + Vector3.min(min, pointPosition, min); + Vector3.max(max, pointPosition, max); idx++; if (idx >= this._maxPointCount) idx = 0; From 871a61293ce430911acba72206f5901cfb18b712 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 01:59:08 +0800 Subject: [PATCH 16/85] feat: simplify property initialization in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 13971ea623..30b018d2e4 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -50,23 +50,23 @@ export class TrailRenderer extends Renderer { private static _tempVector3 = new Vector3(); /** How long the trail points last (in seconds). */ - time: number = 5.0; + time = 5.0; /** The width of the trail. */ - width: number = 1.0; + width = 1.0; /** The minimum distance between trail points (in world units). */ - minVertexDistance: number = 0.1; + minVertexDistance = 0.1; /** Controls how the texture is applied to the trail. */ - textureMode: TrailTextureMode = TrailTextureMode.Stretch; + textureMode = TrailTextureMode.Stretch; /** The tile scale for Tile texture mode. */ - tileScale: number = 1.0; + tileScale = 1.0; /** Trail color (used when colorGradient is not set). */ @deepClone - color: Color = new Color(1, 1, 1, 1); + color = new Color(1, 1, 1, 1); /** * Width curve over lifetime. @@ -74,7 +74,7 @@ export class TrailRenderer extends Renderer { * Default is a constant curve of 1.0. */ @deepClone - widthCurve: ParticleCompositeCurve = new ParticleCompositeCurve(1.0); + widthCurve = new ParticleCompositeCurve(1.0); /** * Color gradient over lifetime. @@ -85,7 +85,7 @@ export class TrailRenderer extends Renderer { colorGradient: ParticleGradient = null; /** Whether the trail is currently emitting new points. */ - emitting: boolean = true; + emitting = true; // Internal state @ignoreClone From 461b87d44b304e6829769fc557bc05d6a72c9fd7 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 02:01:49 +0800 Subject: [PATCH 17/85] feat: refactor vertex buffer and primitive initialization in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 29 ++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 30b018d2e4..e1d1050a87 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -95,8 +95,6 @@ export class TrailRenderer extends Renderer { @ignoreClone private _vertexBuffer: Buffer; @ignoreClone - private _vertexBufferBinding: VertexBufferBinding; - @ignoreClone private _vertices: Float32Array; @ignoreClone private _indexBuffer: Buffer; @@ -259,29 +257,32 @@ export class TrailRenderer extends Renderer { const byteLength = vertexCount * TrailRenderer.VERTEX_STRIDE; // Create vertex buffer - this._vertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, byteLength, BufferUsage.Dynamic, false); + const vertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, byteLength, BufferUsage.Dynamic, false); + this._vertexBuffer = vertexBuffer; // Create CPU-side vertex array this._vertices = new Float32Array(vertexCount * TrailRenderer.VERTEX_FLOAT_STRIDE); // Create vertex buffer binding - this._vertexBufferBinding = new VertexBufferBinding(this._vertexBuffer, TrailRenderer.VERTEX_STRIDE); + const vertexBufferBinding = new VertexBufferBinding(vertexBuffer, TrailRenderer.VERTEX_STRIDE); // Create index buffer (max indices = maxPoints * 2 for triangle strip) const maxIndices = maxPoints * 2; - this._indexBuffer = new Buffer( + const indexBuffer = new Buffer( engine, BufferBindFlag.IndexBuffer, maxIndices * 2, // Uint16 = 2 bytes BufferUsage.Dynamic, false ); + this._indexBuffer = indexBuffer; this._indices = new Uint16Array(maxIndices); // Create primitive - this._primitive = new Primitive(engine); - this._primitive.vertexBufferBindings.push(this._vertexBufferBinding); - this._primitive.setIndexBufferBinding(new IndexBufferBinding(this._indexBuffer, IndexFormat.UInt16)); + const primitive = new Primitive(engine); + this._primitive = primitive; + primitive.vertexBufferBindings.push(vertexBufferBinding); + primitive.setIndexBufferBinding(new IndexBufferBinding(indexBuffer, IndexFormat.UInt16)); // Define vertex elements: // a_Position: vec3 (12 bytes, offset 0) @@ -291,12 +292,12 @@ export class TrailRenderer extends Renderer { // a_Corner: float (4 bytes, offset 36) // a_Tangent: vec3 (12 bytes, offset 40) // Total: 52 bytes per vertex - this._primitive.addVertexElement(new VertexElement("a_Position", 0, VertexElementFormat.Vector3, 0)); - this._primitive.addVertexElement(new VertexElement("a_BirthTime", 12, VertexElementFormat.Float, 0)); - this._primitive.addVertexElement(new VertexElement("a_NormalizedWidth", 16, VertexElementFormat.Float, 0)); - this._primitive.addVertexElement(new VertexElement("a_Color", 20, VertexElementFormat.Vector4, 0)); - this._primitive.addVertexElement(new VertexElement("a_Corner", 36, VertexElementFormat.Float, 0)); - this._primitive.addVertexElement(new VertexElement("a_Tangent", 40, VertexElementFormat.Vector3, 0)); + primitive.addVertexElement(new VertexElement("a_Position", 0, VertexElementFormat.Vector3, 0)); + primitive.addVertexElement(new VertexElement("a_BirthTime", 12, VertexElementFormat.Float, 0)); + primitive.addVertexElement(new VertexElement("a_NormalizedWidth", 16, VertexElementFormat.Float, 0)); + primitive.addVertexElement(new VertexElement("a_Color", 20, VertexElementFormat.Vector4, 0)); + primitive.addVertexElement(new VertexElement("a_Corner", 36, VertexElementFormat.Float, 0)); + primitive.addVertexElement(new VertexElement("a_Tangent", 40, VertexElementFormat.Vector3, 0)); // Create sub-primitive for drawing this._subPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); From dcd01931272baa69d269c6b887beb3cce5d8af33 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 02:06:44 +0800 Subject: [PATCH 18/85] feat: update vertex attributes in TrailRenderer for improved data structure --- .../core/src/shaderlib/extra/trail.vs.glsl | 27 ++++++----- packages/core/src/trail/TrailRenderer.ts | 47 +++++++++---------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index ece15aa8ff..74a3ae7057 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -1,11 +1,8 @@ // Trail vertex attributes (per-vertex) // Each segment has 2 vertices (top and bottom) -attribute vec3 a_Position; // World position of the trail point center -attribute float a_BirthTime; // Time when this point was created -attribute float a_NormalizedWidth; // Width factor at this point (unused, kept for compatibility) +attribute vec4 a_PositionBirthTime; // xyz: World position, w: Birth time attribute vec4 a_Color; // Color at this point (unused when gradient is used) -attribute float a_Corner; // -1 for bottom, 1 for top -attribute vec3 a_Tangent; // Direction to next point (for billboard calculation) +attribute vec4 a_CornerTangent; // x: Corner (-1 or 1), yzw: Tangent direction // Uniforms uniform float renderer_CurrentTime; @@ -90,8 +87,14 @@ float evaluateAlphaGradient(in vec2 keys[4], in int count, in float t) { } void main() { + // Extract position and birth time + vec3 position = a_PositionBirthTime.xyz; + float birthTime = a_PositionBirthTime.w; + float corner = a_CornerTangent.x; + vec3 tangent = a_CornerTangent.yzw; + // Calculate normalized age (0 = new, 1 = about to die) - float age = renderer_CurrentTime - a_BirthTime; + float age = renderer_CurrentTime - birthTime; float normalizedAge = clamp(age / renderer_Lifetime, 0.0, 1.0); // Discard vertices that have exceeded their lifetime @@ -101,16 +104,16 @@ void main() { } // Calculate billboard offset (View alignment) - vec3 toCamera = normalize(camera_Position - a_Position); - vec3 right = cross(a_Tangent, toCamera); + vec3 toCamera = normalize(camera_Position - position); + vec3 right = cross(tangent, toCamera); float rightLen = length(right); // Handle edge case when tangent is parallel to camera direction if (rightLen < 0.001) { - right = cross(a_Tangent, vec3(0.0, 1.0, 0.0)); + right = cross(tangent, vec3(0.0, 1.0, 0.0)); rightLen = length(right); if (rightLen < 0.001) { - right = cross(a_Tangent, vec3(1.0, 0.0, 0.0)); + right = cross(tangent, vec3(1.0, 0.0, 0.0)); rightLen = length(right); } } @@ -121,12 +124,12 @@ void main() { float width = renderer_Width * widthMultiplier; // Apply offset - vec3 worldPosition = a_Position + right * width * 0.5 * a_Corner; + vec3 worldPosition = position + right * width * 0.5 * corner; gl_Position = camera_ProjMat * camera_ViewMat * vec4(worldPosition, 1.0); // Calculate UV based on texture mode - float u = a_Corner * 0.5 + 0.5; // 0 for bottom, 1 for top + float u = corner * 0.5 + 0.5; // 0 for bottom, 1 for top float v; if (renderer_TextureMode == 0) { diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index e1d1050a87..15244df766 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -42,9 +42,9 @@ export class TrailRenderer extends Renderer { private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); private static _alphaKeyCountProp = ShaderProperty.getByName("renderer_AlphaKeyCount"); - // Vertex layout constants - private static readonly VERTEX_STRIDE = 52; // bytes per vertex - private static readonly VERTEX_FLOAT_STRIDE = 13; // floats per vertex + // Vertex layout constants (3 x vec4 = 48 bytes) + private static readonly VERTEX_STRIDE = 48; // bytes per vertex + private static readonly VERTEX_FLOAT_STRIDE = 12; // floats per vertex // Temp variables private static _tempVector3 = new Vector3(); @@ -284,20 +284,13 @@ export class TrailRenderer extends Renderer { primitive.vertexBufferBindings.push(vertexBufferBinding); primitive.setIndexBufferBinding(new IndexBufferBinding(indexBuffer, IndexFormat.UInt16)); - // Define vertex elements: - // a_Position: vec3 (12 bytes, offset 0) - // a_BirthTime: float (4 bytes, offset 12) - // a_NormalizedWidth: float (4 bytes, offset 16) - // a_Color: vec4 (16 bytes, offset 20) - // a_Corner: float (4 bytes, offset 36) - // a_Tangent: vec3 (12 bytes, offset 40) - // Total: 52 bytes per vertex - primitive.addVertexElement(new VertexElement("a_Position", 0, VertexElementFormat.Vector3, 0)); - primitive.addVertexElement(new VertexElement("a_BirthTime", 12, VertexElementFormat.Float, 0)); - primitive.addVertexElement(new VertexElement("a_NormalizedWidth", 16, VertexElementFormat.Float, 0)); - primitive.addVertexElement(new VertexElement("a_Color", 20, VertexElementFormat.Vector4, 0)); - primitive.addVertexElement(new VertexElement("a_Corner", 36, VertexElementFormat.Float, 0)); - primitive.addVertexElement(new VertexElement("a_Tangent", 40, VertexElementFormat.Vector3, 0)); + // Define vertex elements (3 x vec4 = 48 bytes per vertex): + // a_PositionBirthTime: vec4 (16 bytes, offset 0) - xyz: position, w: birthTime + // a_Color: vec4 (16 bytes, offset 16) + // a_CornerTangent: vec4 (16 bytes, offset 32) - x: corner, yzw: tangent + primitive.addVertexElement(new VertexElement("a_PositionBirthTime", 0, VertexElementFormat.Vector4, 0)); + primitive.addVertexElement(new VertexElement("a_Color", 16, VertexElementFormat.Vector4, 0)); + primitive.addVertexElement(new VertexElement("a_CornerTangent", 32, VertexElementFormat.Vector4, 0)); // Create sub-primitive for drawing this._subPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); @@ -386,7 +379,10 @@ export class TrailRenderer extends Renderer { for (let corner = -1; corner <= 1; corner += 2) { const vertexIdx = firstIdx * 2 + (corner === -1 ? 0 : 1); const offset = vertexIdx * floatStride; - tangent.copyToArray(vertices, offset + 10); // Update a_Tangent + // Update a_CornerTangent.yzw (tangent part) + vertices[offset + 9] = tangent.x; + vertices[offset + 10] = tangent.y; + vertices[offset + 11] = tangent.z; } // First point's tangent was updated, need to re-upload it this._firstNewElement = this._firstActiveElement; @@ -397,17 +393,20 @@ export class TrailRenderer extends Renderer { } // Each point has 2 vertices (top and bottom) + // Layout: a_PositionBirthTime (vec4), a_Color (vec4), a_CornerTangent (vec4) const color = this.color; for (let corner = -1; corner <= 1; corner += 2) { const vertexIdx = idx * 2 + (corner === -1 ? 0 : 1); const offset = vertexIdx * floatStride; - position.copyToArray(vertices, offset); // a_Position (vec3) - vertices[offset + 3] = this._playTime; // a_BirthTime (float) - vertices[offset + 4] = 1.0; // a_NormalizedWidth (float) - color.copyToArray(vertices, offset + 5); // a_Color (vec4) - vertices[offset + 9] = corner; // a_Corner (float) - tangent.copyToArray(vertices, offset + 10); // a_Tangent (vec3) + // a_PositionBirthTime: xyz = position, w = birthTime + position.copyToArray(vertices, offset); + vertices[offset + 3] = this._playTime; + // a_Color + color.copyToArray(vertices, offset + 4); + // a_CornerTangent: x = corner, yzw = tangent + vertices[offset + 8] = corner; + tangent.copyToArray(vertices, offset + 9); } // Expand cached bounds with new point (incremental update) From 2a8b7b8629e16798692d285d924e9dc8ce2b8756 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 02:15:55 +0800 Subject: [PATCH 19/85] feat: streamline property definitions and improve code readability in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 237 ++++++----------------- 1 file changed, 54 insertions(+), 183 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 15244df766..cbd41f5456 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -25,69 +25,43 @@ import { TrailTextureMode } from "./enums/TrailTextureMode"; * Renders a trail behind a moving object. */ export class TrailRenderer extends Renderer { - // Shader properties private static _currentTimeProp = ShaderProperty.getByName("renderer_CurrentTime"); private static _lifetimeProp = ShaderProperty.getByName("renderer_Lifetime"); private static _widthProp = ShaderProperty.getByName("renderer_Width"); private static _textureModeProp = ShaderProperty.getByName("renderer_TextureMode"); private static _tileScaleProp = ShaderProperty.getByName("renderer_TileScale"); - - // Width curve shader properties private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); private static _widthCurveCountProp = ShaderProperty.getByName("renderer_WidthCurveCount"); - - // Color gradient shader properties private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); private static _colorKeyCountProp = ShaderProperty.getByName("renderer_ColorKeyCount"); private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); private static _alphaKeyCountProp = ShaderProperty.getByName("renderer_AlphaKeyCount"); - - // Vertex layout constants (3 x vec4 = 48 bytes) - private static readonly VERTEX_STRIDE = 48; // bytes per vertex - private static readonly VERTEX_FLOAT_STRIDE = 12; // floats per vertex - - // Temp variables + private static readonly VERTEX_STRIDE = 48; + private static readonly VERTEX_FLOAT_STRIDE = 12; private static _tempVector3 = new Vector3(); /** How long the trail points last (in seconds). */ time = 5.0; - /** The width of the trail. */ width = 1.0; - - /** The minimum distance between trail points (in world units). */ + /** The minimum distance between trail points. */ minVertexDistance = 0.1; - /** Controls how the texture is applied to the trail. */ textureMode = TrailTextureMode.Stretch; - /** The tile scale for Tile texture mode. */ tileScale = 1.0; - /** Trail color (used when colorGradient is not set). */ @deepClone color = new Color(1, 1, 1, 1); - - /** - * Width curve over lifetime. - * The curve is evaluated based on normalizedAge (0 = head, 1 = tail). - * Default is a constant curve of 1.0. - */ + /** Width curve over lifetime (0 = head, 1 = tail). */ @deepClone widthCurve = new ParticleCompositeCurve(1.0); - - /** - * Color gradient over lifetime. - * The gradient is evaluated based on normalizedAge (0 = head, 1 = tail). - * If not set (null), the color property is used. - */ + /** Color gradient over lifetime (0 = head, 1 = tail). If null, uses color property. */ @deepClone colorGradient: ParticleGradient = null; - /** Whether the trail is currently emitting new points. */ emitting = true; - // Internal state @ignoreClone private _primitive: Primitive; @ignoreClone @@ -100,36 +74,26 @@ export class TrailRenderer extends Renderer { private _indexBuffer: Buffer; @ignoreClone private _indices: Uint16Array; - - // Ring buffer pointers @ignoreClone - private _firstActiveElement: number = 0; + private _firstActiveElement = 0; @ignoreClone - private _firstNewElement: number = 0; + private _firstNewElement = 0; @ignoreClone - private _firstFreeElement: number = 0; + private _firstFreeElement = 0; @ignoreClone - private _maxPointCount: number = 256; - - // Last recorded position + private _maxPointCount = 256; @ignoreClone - private _lastPosition: Vector3 = new Vector3(); + private _lastPosition = new Vector3(); @ignoreClone - private _hasLastPosition: boolean = false; - - // Playback time + private _hasLastPosition = false; @ignoreClone - private _playTime: number = 0; - - // Bounds optimization: cached bounds and dirty flag + private _playTime = 0; @ignoreClone - private _boundsMin: Vector3 = new Vector3(); + private _boundsMin = new Vector3(); @ignoreClone - private _boundsMax: Vector3 = new Vector3(); + private _boundsMax = new Vector3(); @ignoreClone - private _boundsDirty: boolean = true; - - // Shader data cache + private _boundsDirty = true; @ignoreClone private _widthCurveData: Float32Array; @ignoreClone @@ -159,55 +123,36 @@ export class TrailRenderer extends Renderer { protected override _update(context: RenderContext): void { super._update(context); - const deltaTime = this.engine.time.deltaTime; - this._playTime += deltaTime; - - // Retire old points + this._playTime += this.engine.time.deltaTime; this._retireActivePoints(); - // Add new point if emitting and moved enough if (this.emitting) { this._tryAddNewPoint(); } - // Update shader uniforms const shaderData = this.shaderData; shaderData.setFloat(TrailRenderer._currentTimeProp, this._playTime); shaderData.setFloat(TrailRenderer._lifetimeProp, this.time); shaderData.setFloat(TrailRenderer._widthProp, this.width); shaderData.setInt(TrailRenderer._textureModeProp, this.textureMode); shaderData.setFloat(TrailRenderer._tileScaleProp, this.tileScale); - - // Update width curve this._updateWidthCurve(shaderData); - - // Update color gradient this._updateColorGradient(shaderData); } protected override _render(context: RenderContext): void { const activeCount = this._getActivePointCount(); - if (activeCount < 2) { - return; // Need at least 2 points to form a segment - } + if (activeCount < 2) return; - // Only update vertex buffer when there are new points or buffer content is lost if (this._firstNewElement !== this._firstFreeElement || this._vertexBuffer.isContentLost) { this._uploadNewVertices(); } - // Update index buffer to handle ring buffer wrap-around const indexCount = this._updateIndexBuffer(activeCount); this._subPrimitive.count = indexCount; - let material = this.getMaterial(); - if (!material) { - return; - } - - if (material.destroyed || material.shader.destroyed) { - return; - } + const material = this.getMaterial(); + if (!material || material.destroyed || material.shader.destroyed) return; const engine = this._engine; const renderElement = engine._renderElementPool.get(); @@ -223,19 +168,16 @@ export class TrailRenderer extends Renderer { const halfWidth = this.width * 0.5; if (activeCount === 0) { - // No active points, use current entity position const worldPosition = this.entity.transform.worldPosition; worldBounds.min.set(worldPosition.x - halfWidth, worldPosition.y - halfWidth, worldPosition.z - halfWidth); worldBounds.max.set(worldPosition.x + halfWidth, worldPosition.y + halfWidth, worldPosition.z + halfWidth); return; } - // Recalculate bounds only when dirty if (this._boundsDirty) { this._recalculateBounds(); } - // Apply half width offset to cached bounds const { _boundsMin: min, _boundsMax: max } = this; worldBounds.min.set(min.x - halfWidth, min.y - halfWidth, min.z - halfWidth); worldBounds.max.set(max.x + halfWidth, max.y + halfWidth, max.z + halfWidth); @@ -251,48 +193,38 @@ export class TrailRenderer extends Renderer { private _initGeometry(): void { const engine = this.engine; const maxPoints = this._maxPointCount; - // Each point generates 2 vertices (top and bottom of the trail strip) const vertexCount = maxPoints * 2; - const byteLength = vertexCount * TrailRenderer.VERTEX_STRIDE; - // Create vertex buffer - const vertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, byteLength, BufferUsage.Dynamic, false); + const vertexBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + vertexCount * TrailRenderer.VERTEX_STRIDE, + BufferUsage.Dynamic, + false + ); this._vertexBuffer = vertexBuffer; - - // Create CPU-side vertex array this._vertices = new Float32Array(vertexCount * TrailRenderer.VERTEX_FLOAT_STRIDE); - // Create vertex buffer binding const vertexBufferBinding = new VertexBufferBinding(vertexBuffer, TrailRenderer.VERTEX_STRIDE); - // Create index buffer (max indices = maxPoints * 2 for triangle strip) const maxIndices = maxPoints * 2; - const indexBuffer = new Buffer( - engine, - BufferBindFlag.IndexBuffer, - maxIndices * 2, // Uint16 = 2 bytes - BufferUsage.Dynamic, - false - ); + const indexBuffer = new Buffer(engine, BufferBindFlag.IndexBuffer, maxIndices * 2, BufferUsage.Dynamic, false); this._indexBuffer = indexBuffer; this._indices = new Uint16Array(maxIndices); - // Create primitive const primitive = new Primitive(engine); this._primitive = primitive; primitive.vertexBufferBindings.push(vertexBufferBinding); primitive.setIndexBufferBinding(new IndexBufferBinding(indexBuffer, IndexFormat.UInt16)); - - // Define vertex elements (3 x vec4 = 48 bytes per vertex): - // a_PositionBirthTime: vec4 (16 bytes, offset 0) - xyz: position, w: birthTime - // a_Color: vec4 (16 bytes, offset 16) - // a_CornerTangent: vec4 (16 bytes, offset 32) - x: corner, yzw: tangent + // Vertex layout (3 x vec4 = 48 bytes): + // a_PositionBirthTime: xyz = position, w = birthTime + // a_Color: rgba + // a_CornerTangent: x = corner (-1 or 1), yzw = tangent direction primitive.addVertexElement(new VertexElement("a_PositionBirthTime", 0, VertexElementFormat.Vector4, 0)); primitive.addVertexElement(new VertexElement("a_Color", 16, VertexElementFormat.Vector4, 0)); primitive.addVertexElement(new VertexElement("a_CornerTangent", 32, VertexElementFormat.Vector4, 0)); - // Create sub-primitive for drawing this._subPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); } @@ -301,28 +233,22 @@ export class TrailRenderer extends Renderer { const lifetime = this.time; const firstActiveOld = this._firstActiveElement; - // Move firstActiveElement forward for points that have expired while (this._firstActiveElement !== this._firstFreeElement) { const offset = this._firstActiveElement * 2 * TrailRenderer.VERTEX_FLOAT_STRIDE; - const birthTime = this._vertices[offset + 3]; // a_BirthTime offset + const birthTime = this._vertices[offset + 3]; - if (currentTime - birthTime < lifetime) { - break; // This point is still alive - } + if (currentTime - birthTime < lifetime) break; - // Move to next element this._firstActiveElement++; if (this._firstActiveElement >= this._maxPointCount) { this._firstActiveElement = 0; } } - // If points were retired, bounds need recalculation if (this._firstActiveElement !== firstActiveOld) { this._boundsDirty = true; } - // If all points have expired, reset the trail state if (this._firstActiveElement === this._firstFreeElement) { this._hasLastPosition = false; } @@ -331,32 +257,25 @@ export class TrailRenderer extends Renderer { private _tryAddNewPoint(): void { const worldPosition = this.entity.transform.worldPosition; - // Check if we've moved enough to add a new point if (this._hasLastPosition) { - const distance = Vector3.distance(worldPosition, this._lastPosition); - if (distance < this.minVertexDistance) { + if (Vector3.distance(worldPosition, this._lastPosition) < this.minVertexDistance) { return; } } - // Check if we have space for a new point let nextFreeElement = this._firstFreeElement + 1; if (nextFreeElement >= this._maxPointCount) { nextFreeElement = 0; } if (nextFreeElement === this._firstActiveElement) { - // Buffer is full, retire oldest point this._firstActiveElement++; if (this._firstActiveElement >= this._maxPointCount) { this._firstActiveElement = 0; } } - // Add the new point this._addPoint(worldPosition); - - // Update last position this._lastPosition.copyFrom(worldPosition); this._hasLastPosition = true; } @@ -366,53 +285,41 @@ export class TrailRenderer extends Renderer { const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const vertices = this._vertices; - // Calculate tangent (direction from last position to current position) const tangent = TrailRenderer._tempVector3; if (this._hasLastPosition) { Vector3.subtract(position, this._lastPosition, tangent); tangent.normalize(); - // If this is the second point, update the first point's tangent - // to match this tangent (so the tail doesn't have wrong orientation) + // First point has placeholder tangent, update it when second point is added if (this._getActivePointCount() === 1) { const firstIdx = this._firstActiveElement; for (let corner = -1; corner <= 1; corner += 2) { const vertexIdx = firstIdx * 2 + (corner === -1 ? 0 : 1); - const offset = vertexIdx * floatStride; - // Update a_CornerTangent.yzw (tangent part) - vertices[offset + 9] = tangent.x; - vertices[offset + 10] = tangent.y; - vertices[offset + 11] = tangent.z; + tangent.copyToArray(vertices, vertexIdx * floatStride + 9); } - // First point's tangent was updated, need to re-upload it + // Mark first point for re-upload since its tangent changed this._firstNewElement = this._firstActiveElement; } } else { - // First point - use forward direction (will be updated when second point is added) + // First point uses placeholder tangent (will be corrected when second point arrives) tangent.set(0, 0, 1); } - // Each point has 2 vertices (top and bottom) - // Layout: a_PositionBirthTime (vec4), a_Color (vec4), a_CornerTangent (vec4) + // Write vertex data for top and bottom vertices (corner = -1 and 1) const color = this.color; for (let corner = -1; corner <= 1; corner += 2) { const vertexIdx = idx * 2 + (corner === -1 ? 0 : 1); const offset = vertexIdx * floatStride; - // a_PositionBirthTime: xyz = position, w = birthTime position.copyToArray(vertices, offset); vertices[offset + 3] = this._playTime; - // a_Color color.copyToArray(vertices, offset + 4); - // a_CornerTangent: x = corner, yzw = tangent vertices[offset + 8] = corner; tangent.copyToArray(vertices, offset + 9); } - // Expand cached bounds with new point (incremental update) this._expandBounds(position); - // Update pointers this._firstFreeElement++; if (this._firstFreeElement >= this._maxPointCount) { this._firstFreeElement = 0; @@ -420,18 +327,13 @@ export class TrailRenderer extends Renderer { } private _getActivePointCount(): number { - const firstActive = this._firstActiveElement; - const firstFree = this._firstFreeElement; - if (firstFree >= firstActive) { - return firstFree - firstActive; - } - return this._maxPointCount - firstActive + firstFree; + const { _firstActiveElement: firstActive, _firstFreeElement: firstFree } = this; + return firstFree >= firstActive ? firstFree - firstActive : this._maxPointCount - firstActive + firstFree; } private _expandBounds(position: Vector3): void { const { _boundsMin: min, _boundsMax: max } = this; - // If bounds are dirty (need full recalc), initialize with first point if (this._boundsDirty) { min.copyFrom(position); max.copyFrom(position); @@ -439,7 +341,6 @@ export class TrailRenderer extends Renderer { return; } - // Expand bounds incrementally Vector3.min(min, position, min); Vector3.max(max, position, max); } @@ -457,19 +358,16 @@ export class TrailRenderer extends Renderer { return; } - // Initialize with first active point const firstOffset = this._firstActiveElement * 2 * floatStride; min.copyFromArray(vertices, firstOffset); max.copyFrom(min); - // Iterate through remaining active points const pointPosition = TrailRenderer._tempVector3; let idx = this._firstActiveElement + 1; if (idx >= this._maxPointCount) idx = 0; for (let i = 1; i < activeCount; i++) { - const offset = idx * 2 * floatStride; - pointPosition.copyFromArray(vertices, offset); + pointPosition.copyFromArray(vertices, idx * 2 * floatStride); Vector3.min(min, pointPosition, min); Vector3.max(max, pointPosition, max); @@ -481,46 +379,31 @@ export class TrailRenderer extends Renderer { } private _uploadNewVertices(): void { - const firstActive = this._firstActiveElement; - const firstFree = this._firstFreeElement; - const buffer = this._vertexBuffer; - - // If buffer content is lost, need to re-upload all active vertices + const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _vertexBuffer: buffer } = this; const firstNew = buffer.isContentLost ? firstActive : this._firstNewElement; - // No vertices to upload - if (firstNew === firstFree) { - return; - } + if (firstNew === firstFree) return; const byteStride = TrailRenderer.VERTEX_STRIDE; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const vertices = this._vertices; - // Each point has 2 vertices if (firstNew < firstFree) { - // Contiguous range - only upload new vertices const startFloat = firstNew * 2 * floatStride; const countFloat = (firstFree - firstNew) * 2 * floatStride; - const subArray = new Float32Array(vertices.buffer, startFloat * 4, countFloat); - buffer.setData(subArray, firstNew * 2 * byteStride); + buffer.setData(new Float32Array(vertices.buffer, startFloat * 4, countFloat), firstNew * 2 * byteStride); } else { - // Wrapped range - upload in two parts - // First segment: from firstNew to end + // Wrapped range: upload in two parts const startFloat1 = firstNew * 2 * floatStride; const countFloat1 = (this._maxPointCount - firstNew) * 2 * floatStride; - const subArray1 = new Float32Array(vertices.buffer, startFloat1 * 4, countFloat1); - buffer.setData(subArray1, firstNew * 2 * byteStride); + buffer.setData(new Float32Array(vertices.buffer, startFloat1 * 4, countFloat1), firstNew * 2 * byteStride); - // Second segment: from 0 to firstFree if (firstFree > 0) { const countFloat2 = firstFree * 2 * floatStride; - const subArray2 = new Float32Array(vertices.buffer, 0, countFloat2); - buffer.setData(subArray2, 0); + buffer.setData(new Float32Array(vertices.buffer, 0, countFloat2), 0); } } - // Update the new element pointer to match free element this._firstNewElement = firstFree; } @@ -530,20 +413,14 @@ export class TrailRenderer extends Renderer { const maxPointCount = this._maxPointCount; let indexCount = 0; - // Build index buffer to create proper triangle strip ordering - // This handles the ring buffer wrap-around case + // Build triangle strip indices, handling ring buffer wrap-around for (let i = 0; i < activeCount; i++) { - const pointIdx = (firstActive + i) % maxPointCount; - const vertexIdx = pointIdx * 2; - - // Each point has 2 vertices (top and bottom) - indices[indexCount++] = vertexIdx; // bottom vertex - indices[indexCount++] = vertexIdx + 1; // top vertex + const vertexIdx = ((firstActive + i) % maxPointCount) * 2; + indices[indexCount++] = vertexIdx; + indices[indexCount++] = vertexIdx + 1; } - // Upload index buffer this._indexBuffer.setData(indices, 0, 0, indexCount * 2); - return indexCount; } @@ -552,13 +429,11 @@ export class TrailRenderer extends Renderer { const widthCurveData = this._widthCurveData || (this._widthCurveData = new Float32Array(8)); if (curve.mode === 0) { - // Constant mode - widthCurveData[0] = 0; // time - widthCurveData[1] = curve.constant; // value + widthCurveData[0] = 0; + widthCurveData[1] = curve.constant; shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurveData); shaderData.setInt(TrailRenderer._widthCurveCountProp, 1); } else if (curve.mode === 2 && curve.curve) { - // Curve mode const keys = curve.curve.keys; const count = Math.min(keys.length, 4); for (let i = 0, offset = 0; i < count; i++, offset += 2) { @@ -569,7 +444,6 @@ export class TrailRenderer extends Renderer { shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurveData); shaderData.setInt(TrailRenderer._widthCurveCountProp, count); } else { - // Default: constant 1.0 widthCurveData[0] = 0; widthCurveData[1] = 1; shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurveData); @@ -581,7 +455,6 @@ export class TrailRenderer extends Renderer { const gradient = this.colorGradient; if (!gradient) { - // Use vertex color (from this.color) shaderData.setInt(TrailRenderer._colorKeyCountProp, 0); shaderData.setInt(TrailRenderer._alphaKeyCountProp, 0); return; @@ -590,7 +463,6 @@ export class TrailRenderer extends Renderer { const colorKeysData = this._colorKeysData || (this._colorKeysData = new Float32Array(16)); const alphaKeysData = this._alphaKeysData || (this._alphaKeysData = new Float32Array(8)); - // Color keys const colorKeys = gradient.colorKeys; const colorCount = Math.min(colorKeys.length, 4); for (let i = 0, offset = 0; i < colorCount; i++, offset += 4) { @@ -603,7 +475,6 @@ export class TrailRenderer extends Renderer { shaderData.setFloatArray(TrailRenderer._colorKeysProp, colorKeysData); shaderData.setInt(TrailRenderer._colorKeyCountProp, colorCount); - // Alpha keys const alphaKeys = gradient.alphaKeys; const alphaCount = Math.min(alphaKeys.length, 4); for (let i = 0, offset = 0; i < alphaCount; i++, offset += 2) { From aaca29ee40f04c882c9e5fb15b04ac0411ef330a Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 02:31:54 +0800 Subject: [PATCH 20/85] feat: optimize color and alpha key handling in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 32 +++--------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index cbd41f5456..27b4bae51e 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -96,10 +96,6 @@ export class TrailRenderer extends Renderer { private _boundsDirty = true; @ignoreClone private _widthCurveData: Float32Array; - @ignoreClone - private _colorKeysData: Float32Array; - @ignoreClone - private _alphaKeysData: Float32Array; /** * @internal @@ -460,29 +456,9 @@ export class TrailRenderer extends Renderer { return; } - const colorKeysData = this._colorKeysData || (this._colorKeysData = new Float32Array(16)); - const alphaKeysData = this._alphaKeysData || (this._alphaKeysData = new Float32Array(8)); - - const colorKeys = gradient.colorKeys; - const colorCount = Math.min(colorKeys.length, 4); - for (let i = 0, offset = 0; i < colorCount; i++, offset += 4) { - const key = colorKeys[i]; - colorKeysData[offset] = key.time; - colorKeysData[offset + 1] = key.color.r; - colorKeysData[offset + 2] = key.color.g; - colorKeysData[offset + 3] = key.color.b; - } - shaderData.setFloatArray(TrailRenderer._colorKeysProp, colorKeysData); - shaderData.setInt(TrailRenderer._colorKeyCountProp, colorCount); - - const alphaKeys = gradient.alphaKeys; - const alphaCount = Math.min(alphaKeys.length, 4); - for (let i = 0, offset = 0; i < alphaCount; i++, offset += 2) { - const key = alphaKeys[i]; - alphaKeysData[offset] = key.time; - alphaKeysData[offset + 1] = key.alpha; - } - shaderData.setFloatArray(TrailRenderer._alphaKeysProp, alphaKeysData); - shaderData.setInt(TrailRenderer._alphaKeyCountProp, alphaCount); + shaderData.setFloatArray(TrailRenderer._colorKeysProp, gradient._getColorTypeArray()); + shaderData.setInt(TrailRenderer._colorKeyCountProp, gradient.colorKeys.length); + shaderData.setFloatArray(TrailRenderer._alphaKeysProp, gradient._getAlphaTypeArray()); + shaderData.setInt(TrailRenderer._alphaKeyCountProp, gradient.alphaKeys.length); } } From 28573028a4ecfc5a5267b408d49d8c826b962cfd Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 12:40:58 +0800 Subject: [PATCH 21/85] feat: rename tileScale to textureScale for consistency in TrailRenderer --- packages/core/src/shaderlib/extra/trail.vs.glsl | 4 ++-- packages/core/src/trail/TrailRenderer.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index 74a3ae7057..3823dd8e2f 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -9,7 +9,7 @@ uniform float renderer_CurrentTime; uniform float renderer_Lifetime; uniform float renderer_Width; // Base width uniform int renderer_TextureMode; // 0: Stretch, 1: Tile -uniform float renderer_TileScale; +uniform float renderer_TextureScale; uniform vec3 camera_Position; uniform mat4 camera_ViewMat; @@ -137,7 +137,7 @@ void main() { v = normalizedAge; } else { // Tile mode: scale by tile scale - v = normalizedAge * renderer_TileScale; + v = normalizedAge * renderer_TextureScale; } v_uv = vec2(u, v); diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 27b4bae51e..ec52122df6 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -29,7 +29,7 @@ export class TrailRenderer extends Renderer { private static _lifetimeProp = ShaderProperty.getByName("renderer_Lifetime"); private static _widthProp = ShaderProperty.getByName("renderer_Width"); private static _textureModeProp = ShaderProperty.getByName("renderer_TextureMode"); - private static _tileScaleProp = ShaderProperty.getByName("renderer_TileScale"); + private static _textureScaleProp = ShaderProperty.getByName("renderer_TextureScale"); private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); private static _widthCurveCountProp = ShaderProperty.getByName("renderer_WidthCurveCount"); private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); @@ -48,8 +48,8 @@ export class TrailRenderer extends Renderer { minVertexDistance = 0.1; /** Controls how the texture is applied to the trail. */ textureMode = TrailTextureMode.Stretch; - /** The tile scale for Tile texture mode. */ - tileScale = 1.0; + /** The texture scale for Tile texture mode. */ + textureScale = 1.0; /** Trail color (used when colorGradient is not set). */ @deepClone color = new Color(1, 1, 1, 1); @@ -131,7 +131,7 @@ export class TrailRenderer extends Renderer { shaderData.setFloat(TrailRenderer._lifetimeProp, this.time); shaderData.setFloat(TrailRenderer._widthProp, this.width); shaderData.setInt(TrailRenderer._textureModeProp, this.textureMode); - shaderData.setFloat(TrailRenderer._tileScaleProp, this.tileScale); + shaderData.setFloat(TrailRenderer._textureScaleProp, this.textureScale); this._updateWidthCurve(shaderData); this._updateColorGradient(shaderData); } From 11ecb81ab63ecf6bd1a049566746523bf5b33d0b Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 28 Dec 2025 12:47:03 +0800 Subject: [PATCH 22/85] feat: remove unnecessary blank line at the end of EffectMaterial.ts --- packages/core/src/material/EffectMaterial.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/material/EffectMaterial.ts b/packages/core/src/material/EffectMaterial.ts index 17873323d6..e053ce2432 100644 --- a/packages/core/src/material/EffectMaterial.ts +++ b/packages/core/src/material/EffectMaterial.ts @@ -83,4 +83,3 @@ export class EffectMaterial extends BaseMaterial { this.isTransparent = true; } } - From e4350e488669de3b44fbaee9ec6b5b8f6c913577 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Mon, 29 Dec 2025 15:35:27 +0800 Subject: [PATCH 23/85] feat: refactor TrailRenderer to use GradientColorKey and GradientAlphaKey for color gradients --- .../core/src/shaderlib/extra/trail.vs.glsl | 81 +++++++------------ packages/core/src/trail/TrailRenderer.ts | 67 ++++++--------- 2 files changed, 57 insertions(+), 91 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index 3823dd8e2f..df1a7d71a8 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -1,14 +1,13 @@ // Trail vertex attributes (per-vertex) // Each segment has 2 vertices (top and bottom) attribute vec4 a_PositionBirthTime; // xyz: World position, w: Birth time -attribute vec4 a_Color; // Color at this point (unused when gradient is used) -attribute vec4 a_CornerTangent; // x: Corner (-1 or 1), yzw: Tangent direction +attribute vec4 a_CornerTangent; // x: Corner (-1 or 1), yzw: Tangent direction // Uniforms uniform float renderer_CurrentTime; uniform float renderer_Lifetime; -uniform float renderer_Width; // Base width -uniform int renderer_TextureMode; // 0: Stretch, 1: Tile +uniform float renderer_Width; +uniform int renderer_TextureMode; // 0: Stretch, 1: Tile uniform float renderer_TextureScale; uniform vec3 camera_Position; @@ -19,17 +18,15 @@ uniform mat4 camera_ProjMat; uniform vec2 renderer_WidthCurve[4]; uniform int renderer_WidthCurveCount; -// Color gradient uniforms -uniform vec4 renderer_ColorKeys[4]; // x=time, yzw=rgb -uniform int renderer_ColorKeyCount; -uniform vec2 renderer_AlphaKeys[4]; // x=time, y=alpha -uniform int renderer_AlphaKeyCount; +// Color gradient uniforms (4 keyframes max) +uniform vec4 renderer_ColorKeys[4]; // x=time, yzw=rgb +uniform vec2 renderer_AlphaKeys[4]; // x=time, y=alpha // Varyings varying vec2 v_uv; varying vec4 v_color; -// Evaluate curve at normalized age +// Evaluate width curve at normalized age float evaluateCurve(in vec2 keys[4], in int count, in float t) { if (count <= 0) return 1.0; if (count == 1) return keys[0].y; @@ -39,51 +36,42 @@ float evaluateCurve(in vec2 keys[4], in int count, in float t) { if (t <= keys[i].x) { float t0 = keys[i - 1].x; float t1 = keys[i].x; - float v0 = keys[i - 1].y; - float v1 = keys[i].y; float factor = (t - t0) / (t1 - t0); - return mix(v0, v1, factor); + return mix(keys[i - 1].y, keys[i].y, factor); } } return keys[count - 1].y; } -// Evaluate color gradient at normalized age -vec3 evaluateColorGradient(in vec4 keys[4], in int count, in float t) { - if (count <= 0) return vec3(1.0); - if (count == 1) return keys[0].yzw; +// Evaluate color gradient at normalized age (fixed 4 iterations) +vec4 evaluateGradient(in vec4 colorKeys[4], in vec2 alphaKeys[4], in float t) { + vec4 result = vec4(colorKeys[0].yzw, alphaKeys[0].y); + // Evaluate color keys for (int i = 1; i < 4; i++) { - if (i >= count) break; - if (t <= keys[i].x) { - float t0 = keys[i - 1].x; - float t1 = keys[i].x; - vec3 c0 = keys[i - 1].yzw; - vec3 c1 = keys[i].yzw; - float factor = (t - t0) / (t1 - t0); - return mix(c0, c1, factor); + vec4 key = colorKeys[i]; + if (t <= key.x) { + float t0 = colorKeys[i - 1].x; + float factor = (t - t0) / (key.x - t0); + result.rgb = mix(colorKeys[i - 1].yzw, key.yzw, factor); + break; } + result.rgb = key.yzw; } - return keys[count - 1].yzw; -} - -// Evaluate alpha gradient at normalized age -float evaluateAlphaGradient(in vec2 keys[4], in int count, in float t) { - if (count <= 0) return 1.0; - if (count == 1) return keys[0].y; + // Evaluate alpha keys for (int i = 1; i < 4; i++) { - if (i >= count) break; - if (t <= keys[i].x) { - float t0 = keys[i - 1].x; - float t1 = keys[i].x; - float a0 = keys[i - 1].y; - float a1 = keys[i].y; - float factor = (t - t0) / (t1 - t0); - return mix(a0, a1, factor); + vec2 key = alphaKeys[i]; + if (t <= key.x) { + float t0 = alphaKeys[i - 1].x; + float factor = (t - t0) / (key.x - t0); + result.a = mix(alphaKeys[i - 1].y, key.y, factor); + break; } + result.a = key.y; } - return keys[count - 1].y; + + return result; } void main() { @@ -142,13 +130,6 @@ void main() { v_uv = vec2(u, v); - // Evaluate color gradient or use vertex color - if (renderer_ColorKeyCount > 0 || renderer_AlphaKeyCount > 0) { - vec3 gradientColor = evaluateColorGradient(renderer_ColorKeys, renderer_ColorKeyCount, normalizedAge); - float gradientAlpha = evaluateAlphaGradient(renderer_AlphaKeys, renderer_AlphaKeyCount, normalizedAge); - v_color = vec4(gradientColor, gradientAlpha); - } else { - v_color = a_Color; - } + // Evaluate color gradient + v_color = evaluateGradient(renderer_ColorKeys, renderer_AlphaKeys, normalizedAge); } - diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index ec52122df6..62ad783ccb 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -15,7 +15,7 @@ import { IndexFormat } from "../graphic/enums/IndexFormat"; import { MeshTopology } from "../graphic/enums/MeshTopology"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; import { ParticleCompositeCurve } from "../particle/modules/ParticleCompositeCurve"; -import { ParticleGradient } from "../particle/modules/ParticleGradient"; +import { GradientAlphaKey, GradientColorKey, ParticleGradient } from "../particle/modules/ParticleGradient"; import { ShaderData } from "../shader/ShaderData"; import { ShaderProperty } from "../shader/ShaderProperty"; import { TrailTextureMode } from "./enums/TrailTextureMode"; @@ -33,11 +33,9 @@ export class TrailRenderer extends Renderer { private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); private static _widthCurveCountProp = ShaderProperty.getByName("renderer_WidthCurveCount"); private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); - private static _colorKeyCountProp = ShaderProperty.getByName("renderer_ColorKeyCount"); private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); - private static _alphaKeyCountProp = ShaderProperty.getByName("renderer_AlphaKeyCount"); - private static readonly VERTEX_STRIDE = 48; - private static readonly VERTEX_FLOAT_STRIDE = 12; + private static readonly VERTEX_STRIDE = 32; + private static readonly VERTEX_FLOAT_STRIDE = 8; private static _tempVector3 = new Vector3(); /** How long the trail points last (in seconds). */ @@ -50,15 +48,15 @@ export class TrailRenderer extends Renderer { textureMode = TrailTextureMode.Stretch; /** The texture scale for Tile texture mode. */ textureScale = 1.0; - /** Trail color (used when colorGradient is not set). */ - @deepClone - color = new Color(1, 1, 1, 1); /** Width curve over lifetime (0 = head, 1 = tail). */ @deepClone widthCurve = new ParticleCompositeCurve(1.0); - /** Color gradient over lifetime (0 = head, 1 = tail). If null, uses color property. */ + /** Color gradient over lifetime (0 = head, 1 = tail). */ @deepClone - colorGradient: ParticleGradient = null; + colorGradient = new ParticleGradient( + [new GradientColorKey(0, new Color(1, 1, 1, 1)), new GradientColorKey(1, new Color(1, 1, 1, 1))], + [new GradientAlphaKey(0, 1), new GradientAlphaKey(1, 1)] + ); /** Whether the trail is currently emitting new points. */ emitting = true; @@ -213,13 +211,11 @@ export class TrailRenderer extends Renderer { this._primitive = primitive; primitive.vertexBufferBindings.push(vertexBufferBinding); primitive.setIndexBufferBinding(new IndexBufferBinding(indexBuffer, IndexFormat.UInt16)); - // Vertex layout (3 x vec4 = 48 bytes): + // Vertex layout (2 x vec4 = 32 bytes): // a_PositionBirthTime: xyz = position, w = birthTime - // a_Color: rgba // a_CornerTangent: x = corner (-1 or 1), yzw = tangent direction primitive.addVertexElement(new VertexElement("a_PositionBirthTime", 0, VertexElementFormat.Vector4, 0)); - primitive.addVertexElement(new VertexElement("a_Color", 16, VertexElementFormat.Vector4, 0)); - primitive.addVertexElement(new VertexElement("a_CornerTangent", 32, VertexElementFormat.Vector4, 0)); + primitive.addVertexElement(new VertexElement("a_CornerTangent", 16, VertexElementFormat.Vector4, 0)); this._subPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); } @@ -277,7 +273,7 @@ export class TrailRenderer extends Renderer { } private _addPoint(position: Vector3): void { - const idx = this._firstFreeElement; + const pointIndex = this._firstFreeElement; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const vertices = this._vertices; @@ -288,10 +284,10 @@ export class TrailRenderer extends Renderer { // First point has placeholder tangent, update it when second point is added if (this._getActivePointCount() === 1) { - const firstIdx = this._firstActiveElement; + const firstPointIndex = this._firstActiveElement; for (let corner = -1; corner <= 1; corner += 2) { - const vertexIdx = firstIdx * 2 + (corner === -1 ? 0 : 1); - tangent.copyToArray(vertices, vertexIdx * floatStride + 9); + const vertexIndex = firstPointIndex * 2 + (corner === -1 ? 0 : 1); + tangent.copyToArray(vertices, vertexIndex * floatStride + 5); } // Mark first point for re-upload since its tangent changed this._firstNewElement = this._firstActiveElement; @@ -302,16 +298,14 @@ export class TrailRenderer extends Renderer { } // Write vertex data for top and bottom vertices (corner = -1 and 1) - const color = this.color; for (let corner = -1; corner <= 1; corner += 2) { - const vertexIdx = idx * 2 + (corner === -1 ? 0 : 1); - const offset = vertexIdx * floatStride; + const vertexIndex = pointIndex * 2 + (corner === -1 ? 0 : 1); + const offset = vertexIndex * floatStride; position.copyToArray(vertices, offset); vertices[offset + 3] = this._playTime; - color.copyToArray(vertices, offset + 4); - vertices[offset + 8] = corner; - tangent.copyToArray(vertices, offset + 9); + vertices[offset + 4] = corner; + tangent.copyToArray(vertices, offset + 5); } this._expandBounds(position); @@ -359,16 +353,16 @@ export class TrailRenderer extends Renderer { max.copyFrom(min); const pointPosition = TrailRenderer._tempVector3; - let idx = this._firstActiveElement + 1; - if (idx >= this._maxPointCount) idx = 0; + let pointIndex = this._firstActiveElement + 1; + if (pointIndex >= this._maxPointCount) pointIndex = 0; for (let i = 1; i < activeCount; i++) { - pointPosition.copyFromArray(vertices, idx * 2 * floatStride); + pointPosition.copyFromArray(vertices, pointIndex * 2 * floatStride); Vector3.min(min, pointPosition, min); Vector3.max(max, pointPosition, max); - idx++; - if (idx >= this._maxPointCount) idx = 0; + pointIndex++; + if (pointIndex >= this._maxPointCount) pointIndex = 0; } this._boundsDirty = false; @@ -411,9 +405,9 @@ export class TrailRenderer extends Renderer { // Build triangle strip indices, handling ring buffer wrap-around for (let i = 0; i < activeCount; i++) { - const vertexIdx = ((firstActive + i) % maxPointCount) * 2; - indices[indexCount++] = vertexIdx; - indices[indexCount++] = vertexIdx + 1; + const vertexIndex = ((firstActive + i) % maxPointCount) * 2; + indices[indexCount++] = vertexIndex; + indices[indexCount++] = vertexIndex + 1; } this._indexBuffer.setData(indices, 0, 0, indexCount * 2); @@ -449,16 +443,7 @@ export class TrailRenderer extends Renderer { private _updateColorGradient(shaderData: ShaderData): void { const gradient = this.colorGradient; - - if (!gradient) { - shaderData.setInt(TrailRenderer._colorKeyCountProp, 0); - shaderData.setInt(TrailRenderer._alphaKeyCountProp, 0); - return; - } - shaderData.setFloatArray(TrailRenderer._colorKeysProp, gradient._getColorTypeArray()); - shaderData.setInt(TrailRenderer._colorKeyCountProp, gradient.colorKeys.length); shaderData.setFloatArray(TrailRenderer._alphaKeysProp, gradient._getAlphaTypeArray()); - shaderData.setInt(TrailRenderer._alphaKeyCountProp, gradient.alphaKeys.length); } } From 08a4b13e61ba0fa761d01696229b8aa12d00e89e Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Mon, 29 Dec 2025 15:59:50 +0800 Subject: [PATCH 24/85] feat: enhance TrailRenderer to support dynamic point capacity and buffer resizing --- packages/core/src/trail/TrailRenderer.ts | 127 +++++++++++++++-------- 1 file changed, 86 insertions(+), 41 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 62ad783ccb..41476732e8 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -36,6 +36,7 @@ export class TrailRenderer extends Renderer { private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); private static readonly VERTEX_STRIDE = 32; private static readonly VERTEX_FLOAT_STRIDE = 8; + private static readonly _pointIncreaseCount = 128; private static _tempVector3 = new Vector3(); /** How long the trail points last (in seconds). */ @@ -79,7 +80,9 @@ export class TrailRenderer extends Renderer { @ignoreClone private _firstFreeElement = 0; @ignoreClone - private _maxPointCount = 256; + private _currentPointCapacity = 0; + @ignoreClone + private _bufferResized = false; @ignoreClone private _lastPosition = new Vector3(); @ignoreClone @@ -185,39 +188,82 @@ export class TrailRenderer extends Renderer { } private _initGeometry(): void { - const engine = this.engine; - const maxPoints = this._maxPointCount; - // Each point generates 2 vertices (top and bottom of the trail strip) - const vertexCount = maxPoints * 2; - - const vertexBuffer = new Buffer( - engine, - BufferBindFlag.VertexBuffer, - vertexCount * TrailRenderer.VERTEX_STRIDE, - BufferUsage.Dynamic, - false - ); - this._vertexBuffer = vertexBuffer; - this._vertices = new Float32Array(vertexCount * TrailRenderer.VERTEX_FLOAT_STRIDE); - - const vertexBufferBinding = new VertexBufferBinding(vertexBuffer, TrailRenderer.VERTEX_STRIDE); - - const maxIndices = maxPoints * 2; - const indexBuffer = new Buffer(engine, BufferBindFlag.IndexBuffer, maxIndices * 2, BufferUsage.Dynamic, false); - this._indexBuffer = indexBuffer; - this._indices = new Uint16Array(maxIndices); - - const primitive = new Primitive(engine); + const primitive = new Primitive(this.engine); this._primitive = primitive; - primitive.vertexBufferBindings.push(vertexBufferBinding); - primitive.setIndexBufferBinding(new IndexBufferBinding(indexBuffer, IndexFormat.UInt16)); // Vertex layout (2 x vec4 = 32 bytes): // a_PositionBirthTime: xyz = position, w = birthTime // a_CornerTangent: x = corner (-1 or 1), yzw = tangent direction primitive.addVertexElement(new VertexElement("a_PositionBirthTime", 0, VertexElementFormat.Vector4, 0)); primitive.addVertexElement(new VertexElement("a_CornerTangent", 16, VertexElementFormat.Vector4, 0)); - this._subPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); + + this._resizeBuffer(TrailRenderer._pointIncreaseCount); + } + + private _resizeBuffer(increaseCount: number): void { + const engine = this.engine; + const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const byteStride = TrailRenderer.VERTEX_STRIDE; + + const newCapacity = this._currentPointCapacity + increaseCount; + const vertexCount = newCapacity * 2; + + // Create new vertex buffer + const newVertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, vertexCount * byteStride, BufferUsage.Dynamic, false); + const newVertices = new Float32Array(vertexCount * floatStride); + + // Create new index buffer + const newIndexBuffer = new Buffer(engine, BufferBindFlag.IndexBuffer, vertexCount * 2, BufferUsage.Dynamic, false); + const newIndices = new Uint16Array(vertexCount); + + // Migrate existing data if any + const lastVertices = this._vertices; + if (lastVertices) { + const firstFreeElement = this._firstFreeElement; + + // Copy data before firstFreeElement + newVertices.set(new Float32Array(lastVertices.buffer, 0, firstFreeElement * 2 * floatStride)); + + // Copy data after firstFreeElement (shift by increaseCount) + const nextFreeElement = firstFreeElement + 1; + if (nextFreeElement < this._currentPointCapacity) { + const freeEndOffset = (nextFreeElement + increaseCount) * 2 * floatStride; + newVertices.set( + new Float32Array(lastVertices.buffer, nextFreeElement * 2 * floatStride * 4), + freeEndOffset + ); + } + + // Update pointers + if (this._firstNewElement > firstFreeElement) { + this._firstNewElement += increaseCount; + } + if (this._firstActiveElement > firstFreeElement) { + this._firstActiveElement += increaseCount; + } + + this._bufferResized = true; + } + + // Destroy old buffers + this._vertexBuffer?.destroy(); + this._indexBuffer?.destroy(); + + this._vertexBuffer = newVertexBuffer; + this._vertices = newVertices; + this._indexBuffer = newIndexBuffer; + this._indices = newIndices; + this._currentPointCapacity = newCapacity; + + // Update primitive bindings + const primitive = this._primitive; + const vertexBufferBinding = new VertexBufferBinding(newVertexBuffer, byteStride); + if (primitive.vertexBufferBindings.length > 0) { + primitive.setVertexBufferBinding(0, vertexBufferBinding); + } else { + primitive.vertexBufferBindings.push(vertexBufferBinding); + } + primitive.setIndexBufferBinding(new IndexBufferBinding(newIndexBuffer, IndexFormat.UInt16)); } private _retireActivePoints(): void { @@ -232,7 +278,7 @@ export class TrailRenderer extends Renderer { if (currentTime - birthTime < lifetime) break; this._firstActiveElement++; - if (this._firstActiveElement >= this._maxPointCount) { + if (this._firstActiveElement >= this._currentPointCapacity) { this._firstActiveElement = 0; } } @@ -256,15 +302,13 @@ export class TrailRenderer extends Renderer { } let nextFreeElement = this._firstFreeElement + 1; - if (nextFreeElement >= this._maxPointCount) { + if (nextFreeElement >= this._currentPointCapacity) { nextFreeElement = 0; } + // If buffer is full, expand it if (nextFreeElement === this._firstActiveElement) { - this._firstActiveElement++; - if (this._firstActiveElement >= this._maxPointCount) { - this._firstActiveElement = 0; - } + this._resizeBuffer(TrailRenderer._pointIncreaseCount); } this._addPoint(worldPosition); @@ -311,14 +355,14 @@ export class TrailRenderer extends Renderer { this._expandBounds(position); this._firstFreeElement++; - if (this._firstFreeElement >= this._maxPointCount) { + if (this._firstFreeElement >= this._currentPointCapacity) { this._firstFreeElement = 0; } } private _getActivePointCount(): number { const { _firstActiveElement: firstActive, _firstFreeElement: firstFree } = this; - return firstFree >= firstActive ? firstFree - firstActive : this._maxPointCount - firstActive + firstFree; + return firstFree >= firstActive ? firstFree - firstActive : this._currentPointCapacity - firstActive + firstFree; } private _expandBounds(position: Vector3): void { @@ -354,7 +398,7 @@ export class TrailRenderer extends Renderer { const pointPosition = TrailRenderer._tempVector3; let pointIndex = this._firstActiveElement + 1; - if (pointIndex >= this._maxPointCount) pointIndex = 0; + if (pointIndex >= this._currentPointCapacity) pointIndex = 0; for (let i = 1; i < activeCount; i++) { pointPosition.copyFromArray(vertices, pointIndex * 2 * floatStride); @@ -362,7 +406,7 @@ export class TrailRenderer extends Renderer { Vector3.max(max, pointPosition, max); pointIndex++; - if (pointIndex >= this._maxPointCount) pointIndex = 0; + if (pointIndex >= this._currentPointCapacity) pointIndex = 0; } this._boundsDirty = false; @@ -370,7 +414,8 @@ export class TrailRenderer extends Renderer { private _uploadNewVertices(): void { const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _vertexBuffer: buffer } = this; - const firstNew = buffer.isContentLost ? firstActive : this._firstNewElement; + const firstNew = buffer.isContentLost || this._bufferResized ? firstActive : this._firstNewElement; + this._bufferResized = false; if (firstNew === firstFree) return; @@ -385,7 +430,7 @@ export class TrailRenderer extends Renderer { } else { // Wrapped range: upload in two parts const startFloat1 = firstNew * 2 * floatStride; - const countFloat1 = (this._maxPointCount - firstNew) * 2 * floatStride; + const countFloat1 = (this._currentPointCapacity - firstNew) * 2 * floatStride; buffer.setData(new Float32Array(vertices.buffer, startFloat1 * 4, countFloat1), firstNew * 2 * byteStride); if (firstFree > 0) { @@ -400,12 +445,12 @@ export class TrailRenderer extends Renderer { private _updateIndexBuffer(activeCount: number): number { const indices = this._indices; const firstActive = this._firstActiveElement; - const maxPointCount = this._maxPointCount; + const capacity = this._currentPointCapacity; let indexCount = 0; // Build triangle strip indices, handling ring buffer wrap-around for (let i = 0; i < activeCount; i++) { - const vertexIndex = ((firstActive + i) % maxPointCount) * 2; + const vertexIndex = ((firstActive + i) % capacity) * 2; indices[indexCount++] = vertexIndex; indices[indexCount++] = vertexIndex + 1; } From 4f15c065774f833abbe1bacb02114cc4c9eb63f5 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Mon, 29 Dec 2025 23:02:39 +0800 Subject: [PATCH 25/85] feat: refactor TrailRenderer to eliminate index buffer and optimize vertex handling --- packages/core/src/trail/TrailRenderer.ts | 105 +++++++++++++++-------- 1 file changed, 68 insertions(+), 37 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 41476732e8..1c65e54bb3 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -4,14 +4,12 @@ import { Renderer } from "../Renderer"; import { RenderContext } from "../RenderPipeline/RenderContext"; import { deepClone, ignoreClone } from "../clone/CloneManager"; import { Buffer } from "../graphic/Buffer"; -import { IndexBufferBinding } from "../graphic/IndexBufferBinding"; import { Primitive } from "../graphic/Primitive"; import { SubPrimitive } from "../graphic/SubPrimitive"; import { VertexBufferBinding } from "../graphic/VertexBufferBinding"; import { VertexElement } from "../graphic/VertexElement"; import { BufferBindFlag } from "../graphic/enums/BufferBindFlag"; import { BufferUsage } from "../graphic/enums/BufferUsage"; -import { IndexFormat } from "../graphic/enums/IndexFormat"; import { MeshTopology } from "../graphic/enums/MeshTopology"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; import { ParticleCompositeCurve } from "../particle/modules/ParticleCompositeCurve"; @@ -66,14 +64,12 @@ export class TrailRenderer extends Renderer { @ignoreClone private _subPrimitive: SubPrimitive; @ignoreClone + private _subPrimitive2: SubPrimitive; + @ignoreClone private _vertexBuffer: Buffer; @ignoreClone private _vertices: Float32Array; @ignoreClone - private _indexBuffer: Buffer; - @ignoreClone - private _indices: Uint16Array; - @ignoreClone private _firstActiveElement = 0; @ignoreClone private _firstNewElement = 0; @@ -145,18 +141,51 @@ export class TrailRenderer extends Renderer { this._uploadNewVertices(); } - const indexCount = this._updateIndexBuffer(activeCount); - this._subPrimitive.count = indexCount; - const material = this.getMaterial(); if (!material || material.destroyed || material.shader.destroyed) return; + const firstActive = this._firstActiveElement; + const firstFree = this._firstFreeElement; + const capacity = this._currentPointCapacity; + const engine = this._engine; const renderElement = engine._renderElementPool.get(); renderElement.set(this.priority, this._distanceForSort); - const subRenderElement = engine._subRenderElementPool.get(); - subRenderElement.set(this, material, this._primitive, this._subPrimitive); - renderElement.addSubRenderElement(subRenderElement); + const subRenderElementPool = engine._subRenderElementPool; + const primitive = this._primitive; + + if (firstActive < firstFree) { + // Non-wrapped case: single draw call + const subPrimitive = this._subPrimitive; + subPrimitive.start = firstActive * 2; + subPrimitive.count = (firstFree - firstActive) * 2; + const subRenderElement = subRenderElementPool.get(); + subRenderElement.set(this, material, primitive, subPrimitive); + renderElement.addSubRenderElement(subRenderElement); + } else { + // Wrapped case: two draw calls + // Copy point 0 to bridge position (capacity) to connect the two segments + this._copyBridgePoint(); + + // First draw: from firstActive to capacity (includes bridge = copy of point 0) + const subPrimitive1 = this._subPrimitive; + subPrimitive1.start = firstActive * 2; + subPrimitive1.count = (capacity - firstActive + 1) * 2; // +1 for bridge point + const subRenderElement1 = subRenderElementPool.get(); + subRenderElement1.set(this, material, primitive, subPrimitive1); + renderElement.addSubRenderElement(subRenderElement1); + + // Second draw: from 0 to firstFree (point 0 drawn twice, acceptable) + if (firstFree > 0) { + const subPrimitive2 = this._subPrimitive2; + subPrimitive2.start = 0; + subPrimitive2.count = firstFree * 2; + const subRenderElement2 = subRenderElementPool.get(); + subRenderElement2.set(this, material, primitive, subPrimitive2); + renderElement.addSubRenderElement(subRenderElement2); + } + } + context.camera._renderPipeline.pushRenderElement(context, renderElement); } @@ -183,7 +212,6 @@ export class TrailRenderer extends Renderer { protected override _onDestroy(): void { super._onDestroy(); this._vertexBuffer?.destroy(); - this._indexBuffer?.destroy(); this._primitive?.destroy(); } @@ -196,6 +224,7 @@ export class TrailRenderer extends Renderer { primitive.addVertexElement(new VertexElement("a_PositionBirthTime", 0, VertexElementFormat.Vector4, 0)); primitive.addVertexElement(new VertexElement("a_CornerTangent", 16, VertexElementFormat.Vector4, 0)); this._subPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); + this._subPrimitive2 = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); this._resizeBuffer(TrailRenderer._pointIncreaseCount); } @@ -206,17 +235,15 @@ export class TrailRenderer extends Renderer { const byteStride = TrailRenderer.VERTEX_STRIDE; const newCapacity = this._currentPointCapacity + increaseCount; - const vertexCount = newCapacity * 2; + // Buffer layout: [capacity points] + [1 bridge point] + // Bridge point is copy of point 0, placed at position capacity to connect wrap-around + const vertexCount = newCapacity * 2 + 2; // +2 vertices for bridge point - // Create new vertex buffer + // Create new vertex buffer (no index buffer needed - using drawArrays) const newVertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, vertexCount * byteStride, BufferUsage.Dynamic, false); const newVertices = new Float32Array(vertexCount * floatStride); - // Create new index buffer - const newIndexBuffer = new Buffer(engine, BufferBindFlag.IndexBuffer, vertexCount * 2, BufferUsage.Dynamic, false); - const newIndices = new Uint16Array(vertexCount); - - // Migrate existing data if any + // Migrate existing vertex data if any const lastVertices = this._vertices; if (lastVertices) { const firstFreeElement = this._firstFreeElement; @@ -245,17 +272,14 @@ export class TrailRenderer extends Renderer { this._bufferResized = true; } - // Destroy old buffers + // Destroy old vertex buffer this._vertexBuffer?.destroy(); - this._indexBuffer?.destroy(); this._vertexBuffer = newVertexBuffer; this._vertices = newVertices; - this._indexBuffer = newIndexBuffer; - this._indices = newIndices; this._currentPointCapacity = newCapacity; - // Update primitive bindings + // Update primitive vertex buffer binding (no index buffer) const primitive = this._primitive; const vertexBufferBinding = new VertexBufferBinding(newVertexBuffer, byteStride); if (primitive.vertexBufferBindings.length > 0) { @@ -263,7 +287,6 @@ export class TrailRenderer extends Renderer { } else { primitive.vertexBufferBindings.push(vertexBufferBinding); } - primitive.setIndexBufferBinding(new IndexBufferBinding(newIndexBuffer, IndexFormat.UInt16)); } private _retireActivePoints(): void { @@ -442,21 +465,29 @@ export class TrailRenderer extends Renderer { this._firstNewElement = firstFree; } - private _updateIndexBuffer(activeCount: number): number { - const indices = this._indices; - const firstActive = this._firstActiveElement; + private _copyBridgePoint(): void { + // Copy point 0 to bridge position (capacity) to connect wrap-around + const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const capacity = this._currentPointCapacity; - let indexCount = 0; + const vertices = this._vertices; + + // Source: point 0 (2 vertices) + // Dest: bridge position = capacity (2 vertices) + const dstOffset = capacity * 2 * floatStride; + const pointFloats = 2 * floatStride; // 2 vertices per point - // Build triangle strip indices, handling ring buffer wrap-around - for (let i = 0; i < activeCount; i++) { - const vertexIndex = ((firstActive + i) % capacity) * 2; - indices[indexCount++] = vertexIndex; - indices[indexCount++] = vertexIndex + 1; + // Copy in CPU array + for (let i = 0; i < pointFloats; i++) { + vertices[dstOffset + i] = vertices[i]; } - this._indexBuffer.setData(indices, 0, 0, indexCount * 2); - return indexCount; + // Upload bridge point to GPU + this._vertexBuffer.setData( + new Float32Array(vertices.buffer, dstOffset * 4, pointFloats), + dstOffset * 4, + 0, + pointFloats * 4 + ); } private _updateWidthCurve(shaderData: ShaderData): void { From f08e3ecfac68b59c736ab7ae2ceb1e938f44a07f Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Mon, 29 Dec 2025 23:21:45 +0800 Subject: [PATCH 26/85] refactor: opt code --- packages/core/src/trail/TrailRenderer.ts | 80 +++++++++++------------- 1 file changed, 36 insertions(+), 44 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 1c65e54bb3..9e41e0f93e 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -154,28 +154,17 @@ export class TrailRenderer extends Renderer { const subRenderElementPool = engine._subRenderElementPool; const primitive = this._primitive; - if (firstActive < firstFree) { - // Non-wrapped case: single draw call - const subPrimitive = this._subPrimitive; - subPrimitive.start = firstActive * 2; - subPrimitive.count = (firstFree - firstActive) * 2; + const subPrimitive = this._subPrimitive; + subPrimitive.start = firstActive * 2; + + if (firstActive >= firstFree) { + // Wrapped: first segment + bridge, then second segment + subPrimitive.count = (capacity - firstActive + 1) * 2; + const subRenderElement = subRenderElementPool.get(); subRenderElement.set(this, material, primitive, subPrimitive); renderElement.addSubRenderElement(subRenderElement); - } else { - // Wrapped case: two draw calls - // Copy point 0 to bridge position (capacity) to connect the two segments - this._copyBridgePoint(); - - // First draw: from firstActive to capacity (includes bridge = copy of point 0) - const subPrimitive1 = this._subPrimitive; - subPrimitive1.start = firstActive * 2; - subPrimitive1.count = (capacity - firstActive + 1) * 2; // +1 for bridge point - const subRenderElement1 = subRenderElementPool.get(); - subRenderElement1.set(this, material, primitive, subPrimitive1); - renderElement.addSubRenderElement(subRenderElement1); - - // Second draw: from 0 to firstFree (point 0 drawn twice, acceptable) + if (firstFree > 0) { const subPrimitive2 = this._subPrimitive2; subPrimitive2.start = 0; @@ -184,6 +173,12 @@ export class TrailRenderer extends Renderer { subRenderElement2.set(this, material, primitive, subPrimitive2); renderElement.addSubRenderElement(subRenderElement2); } + } else { + // Non-wrapped: single draw + subPrimitive.count = (firstFree - firstActive) * 2; + const subRenderElement = subRenderElementPool.get(); + subRenderElement.set(this, material, primitive, subPrimitive); + renderElement.addSubRenderElement(subRenderElement); } context.camera._renderPipeline.pushRenderElement(context, renderElement); @@ -373,6 +368,15 @@ export class TrailRenderer extends Renderer { vertices[offset + 3] = this._playTime; vertices[offset + 4] = corner; tangent.copyToArray(vertices, offset + 5); + + // Also write to bridge position when writing point 0 + if (pointIndex === 0) { + const bridgeOffset = (this._currentPointCapacity * 2 + (corner === -1 ? 0 : 1)) * floatStride; + position.copyToArray(vertices, bridgeOffset); + vertices[bridgeOffset + 3] = this._playTime; + vertices[bridgeOffset + 4] = corner; + tangent.copyToArray(vertices, bridgeOffset + 5); + } } this._expandBounds(position); @@ -446,48 +450,36 @@ export class TrailRenderer extends Renderer { const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const vertices = this._vertices; + const capacity = this._currentPointCapacity; + let uploadBridge = false; + if (firstNew < firstFree) { const startFloat = firstNew * 2 * floatStride; const countFloat = (firstFree - firstNew) * 2 * floatStride; buffer.setData(new Float32Array(vertices.buffer, startFloat * 4, countFloat), firstNew * 2 * byteStride); + uploadBridge = firstNew === 0; } else { // Wrapped range: upload in two parts const startFloat1 = firstNew * 2 * floatStride; - const countFloat1 = (this._currentPointCapacity - firstNew) * 2 * floatStride; + const countFloat1 = (capacity - firstNew) * 2 * floatStride; buffer.setData(new Float32Array(vertices.buffer, startFloat1 * 4, countFloat1), firstNew * 2 * byteStride); if (firstFree > 0) { const countFloat2 = firstFree * 2 * floatStride; buffer.setData(new Float32Array(vertices.buffer, 0, countFloat2), 0); + uploadBridge = true; } } - this._firstNewElement = firstFree; - } - - private _copyBridgePoint(): void { - // Copy point 0 to bridge position (capacity) to connect wrap-around - const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; - const capacity = this._currentPointCapacity; - const vertices = this._vertices; - - // Source: point 0 (2 vertices) - // Dest: bridge position = capacity (2 vertices) - const dstOffset = capacity * 2 * floatStride; - const pointFloats = 2 * floatStride; // 2 vertices per point - - // Copy in CPU array - for (let i = 0; i < pointFloats; i++) { - vertices[dstOffset + i] = vertices[i]; + // Upload bridge (copy of point 0) if point 0 was updated + if (uploadBridge) { + const bridgeByteOffset = capacity * 2 * byteStride; + const bridgeFloatOffset = capacity * 2 * floatStride; + const bridgeFloats = 2 * floatStride; + buffer.setData(new Float32Array(vertices.buffer, bridgeFloatOffset * 4, bridgeFloats), bridgeByteOffset); } - // Upload bridge point to GPU - this._vertexBuffer.setData( - new Float32Array(vertices.buffer, dstOffset * 4, pointFloats), - dstOffset * 4, - 0, - pointFloats * 4 - ); + this._firstNewElement = firstFree; } private _updateWidthCurve(shaderData: ShaderData): void { From ea809c439a40f4c486a4f3ece45ad142328aa599 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Mon, 29 Dec 2025 23:28:12 +0800 Subject: [PATCH 27/85] feat: optimize TrailRenderer for better vertex data handling and material setup --- packages/core/src/trail/TrailRenderer.ts | 38 +++++++++++------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 9e41e0f93e..4e7662e6cc 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -144,10 +144,7 @@ export class TrailRenderer extends Renderer { const material = this.getMaterial(); if (!material || material.destroyed || material.shader.destroyed) return; - const firstActive = this._firstActiveElement; - const firstFree = this._firstFreeElement; - const capacity = this._currentPointCapacity; - + const { _firstActiveElement: firstActive, _firstFreeElement: firstFree } = this; const engine = this._engine; const renderElement = engine._renderElementPool.get(); renderElement.set(this.priority, this._distanceForSort); @@ -159,7 +156,7 @@ export class TrailRenderer extends Renderer { if (firstActive >= firstFree) { // Wrapped: first segment + bridge, then second segment - subPrimitive.count = (capacity - firstActive + 1) * 2; + subPrimitive.count = (this._currentPointCapacity - firstActive + 1) * 2; const subRenderElement = subRenderElementPool.get(); subRenderElement.set(this, material, primitive, subPrimitive); @@ -446,37 +443,36 @@ export class TrailRenderer extends Renderer { if (firstNew === firstFree) return; - const byteStride = TrailRenderer.VERTEX_STRIDE; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; - const vertices = this._vertices; - + const byteStride = TrailRenderer.VERTEX_STRIDE; + const { buffer: vertexData } = this._vertices; const capacity = this._currentPointCapacity; let uploadBridge = false; if (firstNew < firstFree) { - const startFloat = firstNew * 2 * floatStride; - const countFloat = (firstFree - firstNew) * 2 * floatStride; - buffer.setData(new Float32Array(vertices.buffer, startFloat * 4, countFloat), firstNew * 2 * byteStride); + buffer.setData( + new Float32Array(vertexData, firstNew * 2 * floatStride * 4, (firstFree - firstNew) * 2 * floatStride), + firstNew * 2 * byteStride + ); uploadBridge = firstNew === 0; } else { // Wrapped range: upload in two parts - const startFloat1 = firstNew * 2 * floatStride; - const countFloat1 = (capacity - firstNew) * 2 * floatStride; - buffer.setData(new Float32Array(vertices.buffer, startFloat1 * 4, countFloat1), firstNew * 2 * byteStride); - + buffer.setData( + new Float32Array(vertexData, firstNew * 2 * floatStride * 4, (capacity - firstNew) * 2 * floatStride), + firstNew * 2 * byteStride + ); if (firstFree > 0) { - const countFloat2 = firstFree * 2 * floatStride; - buffer.setData(new Float32Array(vertices.buffer, 0, countFloat2), 0); + buffer.setData(new Float32Array(vertexData, 0, firstFree * 2 * floatStride), 0); uploadBridge = true; } } // Upload bridge (copy of point 0) if point 0 was updated if (uploadBridge) { - const bridgeByteOffset = capacity * 2 * byteStride; - const bridgeFloatOffset = capacity * 2 * floatStride; - const bridgeFloats = 2 * floatStride; - buffer.setData(new Float32Array(vertices.buffer, bridgeFloatOffset * 4, bridgeFloats), bridgeByteOffset); + buffer.setData( + new Float32Array(vertexData, capacity * 2 * floatStride * 4, 2 * floatStride), + capacity * 2 * byteStride + ); } this._firstNewElement = firstFree; From 021144d29045f242a677012a5fa68f01e2c50ca7 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Mon, 29 Dec 2025 23:41:04 +0800 Subject: [PATCH 28/85] feat: simplify TrailRenderer's rendering logic and improve buffer management --- packages/core/src/trail/TrailRenderer.ts | 93 ++++++++---------------- 1 file changed, 32 insertions(+), 61 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 4e7662e6cc..783c34312d 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -144,38 +144,31 @@ export class TrailRenderer extends Renderer { const material = this.getMaterial(); if (!material || material.destroyed || material.shader.destroyed) return; - const { _firstActiveElement: firstActive, _firstFreeElement: firstFree } = this; - const engine = this._engine; - const renderElement = engine._renderElementPool.get(); + const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _primitive: primitive } = this; + const { _renderElementPool: renderElementPool, _subRenderElementPool: subRenderElementPool } = this._engine; + + const renderElement = renderElementPool.get(); renderElement.set(this.priority, this._distanceForSort); - const subRenderElementPool = engine._subRenderElementPool; - const primitive = this._primitive; + // First segment const subPrimitive = this._subPrimitive; subPrimitive.start = firstActive * 2; - - if (firstActive >= firstFree) { - // Wrapped: first segment + bridge, then second segment - subPrimitive.count = (this._currentPointCapacity - firstActive + 1) * 2; - - const subRenderElement = subRenderElementPool.get(); - subRenderElement.set(this, material, primitive, subPrimitive); - renderElement.addSubRenderElement(subRenderElement); - - if (firstFree > 0) { - const subPrimitive2 = this._subPrimitive2; - subPrimitive2.start = 0; - subPrimitive2.count = firstFree * 2; - const subRenderElement2 = subRenderElementPool.get(); - subRenderElement2.set(this, material, primitive, subPrimitive2); - renderElement.addSubRenderElement(subRenderElement2); - } - } else { - // Non-wrapped: single draw - subPrimitive.count = (firstFree - firstActive) * 2; - const subRenderElement = subRenderElementPool.get(); - subRenderElement.set(this, material, primitive, subPrimitive); - renderElement.addSubRenderElement(subRenderElement); + subPrimitive.count = + firstActive >= firstFree + ? (this._currentPointCapacity - firstActive + 1) * 2 // Wrapped: includes bridge + : (firstFree - firstActive) * 2; + const subRenderElement = subRenderElementPool.get(); + subRenderElement.set(this, material, primitive, subPrimitive); + renderElement.addSubRenderElement(subRenderElement); + + // Second segment (wrapped case only) + if (firstActive >= firstFree && firstFree > 0) { + const subPrimitive2 = this._subPrimitive2; + subPrimitive2.start = 0; + subPrimitive2.count = firstFree * 2; + const subRenderElement2 = subRenderElementPool.get(); + subRenderElement2.set(this, material, primitive, subPrimitive2); + renderElement.addSubRenderElement(subRenderElement2); } context.camera._renderPipeline.pushRenderElement(context, renderElement); @@ -282,26 +275,19 @@ export class TrailRenderer extends Renderer { } private _retireActivePoints(): void { - const currentTime = this._playTime; - const lifetime = this.time; + const { _playTime: currentTime, time: lifetime, _vertices: vertices, _currentPointCapacity: capacity } = this; const firstActiveOld = this._firstActiveElement; + const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; while (this._firstActiveElement !== this._firstFreeElement) { - const offset = this._firstActiveElement * 2 * TrailRenderer.VERTEX_FLOAT_STRIDE; - const birthTime = this._vertices[offset + 3]; - + const birthTime = vertices[this._firstActiveElement * 2 * floatStride + 3]; if (currentTime - birthTime < lifetime) break; - - this._firstActiveElement++; - if (this._firstActiveElement >= this._currentPointCapacity) { - this._firstActiveElement = 0; - } + this._firstActiveElement = (this._firstActiveElement + 1) % capacity; } if (this._firstActiveElement !== firstActiveOld) { this._boundsDirty = true; } - if (this._firstActiveElement === this._firstFreeElement) { this._hasLastPosition = false; } @@ -310,18 +296,11 @@ export class TrailRenderer extends Renderer { private _tryAddNewPoint(): void { const worldPosition = this.entity.transform.worldPosition; - if (this._hasLastPosition) { - if (Vector3.distance(worldPosition, this._lastPosition) < this.minVertexDistance) { - return; - } - } - - let nextFreeElement = this._firstFreeElement + 1; - if (nextFreeElement >= this._currentPointCapacity) { - nextFreeElement = 0; + if (this._hasLastPosition && Vector3.distance(worldPosition, this._lastPosition) < this.minVertexDistance) { + return; } - // If buffer is full, expand it + const nextFreeElement = (this._firstFreeElement + 1) % this._currentPointCapacity; if (nextFreeElement === this._firstActiveElement) { this._resizeBuffer(TrailRenderer._pointIncreaseCount); } @@ -377,11 +356,7 @@ export class TrailRenderer extends Renderer { } this._expandBounds(position); - - this._firstFreeElement++; - if (this._firstFreeElement >= this._currentPointCapacity) { - this._firstFreeElement = 0; - } + this._firstFreeElement = (this._firstFreeElement + 1) % this._currentPointCapacity; } private _getActivePointCount(): number { @@ -421,16 +396,12 @@ export class TrailRenderer extends Renderer { max.copyFrom(min); const pointPosition = TrailRenderer._tempVector3; - let pointIndex = this._firstActiveElement + 1; - if (pointIndex >= this._currentPointCapacity) pointIndex = 0; - - for (let i = 1; i < activeCount; i++) { + const capacity = this._currentPointCapacity; + for (let i = 1, pointIndex = (this._firstActiveElement + 1) % capacity; i < activeCount; i++) { pointPosition.copyFromArray(vertices, pointIndex * 2 * floatStride); Vector3.min(min, pointPosition, min); Vector3.max(max, pointPosition, max); - - pointIndex++; - if (pointIndex >= this._currentPointCapacity) pointIndex = 0; + pointIndex = (pointIndex + 1) % capacity; } this._boundsDirty = false; From 5683fab829e7bd1ce68fa072e47e684479f4705c Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 00:01:40 +0800 Subject: [PATCH 29/85] feat: optimize vertex upload logic in TrailRenderer for improved performance --- packages/core/src/trail/TrailRenderer.ts | 30 ++++++++++-------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 783c34312d..ef22be5a94 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -418,28 +418,22 @@ export class TrailRenderer extends Renderer { const byteStride = TrailRenderer.VERTEX_STRIDE; const { buffer: vertexData } = this._vertices; const capacity = this._currentPointCapacity; - let uploadBridge = false; + const wrapped = firstNew >= firstFree; - if (firstNew < firstFree) { - buffer.setData( - new Float32Array(vertexData, firstNew * 2 * floatStride * 4, (firstFree - firstNew) * 2 * floatStride), - firstNew * 2 * byteStride - ); - uploadBridge = firstNew === 0; - } else { - // Wrapped range: upload in two parts - buffer.setData( - new Float32Array(vertexData, firstNew * 2 * floatStride * 4, (capacity - firstNew) * 2 * floatStride), - firstNew * 2 * byteStride - ); + // First segment: wrapped includes bridge (+1 point), non-wrapped ends at firstFree + const endPoint = wrapped ? capacity + 1 : firstFree; + buffer.setData( + new Float32Array(vertexData, firstNew * 2 * floatStride * 4, (endPoint - firstNew) * 2 * floatStride), + firstNew * 2 * byteStride + ); + + if (wrapped) { + // Second segment if (firstFree > 0) { buffer.setData(new Float32Array(vertexData, 0, firstFree * 2 * floatStride), 0); - uploadBridge = true; } - } - - // Upload bridge (copy of point 0) if point 0 was updated - if (uploadBridge) { + } else if (firstNew === 0) { + // Upload bridge separately if point 0 was updated buffer.setData( new Float32Array(vertexData, capacity * 2 * floatStride * 4, 2 * floatStride), capacity * 2 * byteStride From 85eda2558d0dac5c3c38cadf786a479d2c2d3174 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 01:28:41 +0800 Subject: [PATCH 30/85] feat: add oldest and newest birth time calculations for UV mapping in TrailRenderer --- .../core/src/shaderlib/extra/trail.vs.glsl | 26 +++++++++++++------ packages/core/src/trail/TrailRenderer.ts | 17 ++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index df1a7d71a8..a89867dd71 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -9,6 +9,8 @@ uniform float renderer_Lifetime; uniform float renderer_Width; uniform int renderer_TextureMode; // 0: Stretch, 1: Tile uniform float renderer_TextureScale; +uniform float renderer_OldestBirthTime; // Birth time of oldest (tail) point +uniform float renderer_NewestBirthTime; // Birth time of newest (head) point uniform vec3 camera_Position; uniform mat4 camera_ViewMat; @@ -81,7 +83,7 @@ void main() { float corner = a_CornerTangent.x; vec3 tangent = a_CornerTangent.yzw; - // Calculate normalized age (0 = new, 1 = about to die) + // Calculate normalized age (0 = new, 1 = about to die) for lifetime check float age = renderer_CurrentTime - birthTime; float normalizedAge = clamp(age / renderer_Lifetime, 0.0, 1.0); @@ -91,6 +93,14 @@ void main() { return; } + // Calculate relative position in trail (0 = head/newest, 1 = tail/oldest) + // Used for width curve, color gradient, and UV in Stretch mode + float timeRange = renderer_NewestBirthTime - renderer_OldestBirthTime; + float relativePosition = 0.0; + if (timeRange > 0.0001) { + relativePosition = (renderer_NewestBirthTime - birthTime) / timeRange; + } + // Calculate billboard offset (View alignment) vec3 toCamera = normalize(camera_Position - position); vec3 right = cross(tangent, toCamera); @@ -107,8 +117,8 @@ void main() { } right = right / rightLen; - // Evaluate width curve - float widthMultiplier = evaluateCurve(renderer_WidthCurve, renderer_WidthCurveCount, normalizedAge); + // Evaluate width curve using relative position + float widthMultiplier = evaluateCurve(renderer_WidthCurve, renderer_WidthCurveCount, relativePosition); float width = renderer_Width * widthMultiplier; // Apply offset @@ -121,15 +131,15 @@ void main() { float v; if (renderer_TextureMode == 0) { - // Stretch mode: UV.v based on normalized age - v = normalizedAge; + // Stretch mode: UV.v based on relative position in trail + v = relativePosition; } else { - // Tile mode: scale by tile scale + // Tile mode: scale by tile scale (use normalizedAge for tiling effect) v = normalizedAge * renderer_TextureScale; } v_uv = vec2(u, v); - // Evaluate color gradient - v_color = evaluateGradient(renderer_ColorKeys, renderer_AlphaKeys, normalizedAge); + // Evaluate color gradient using relative position + v_color = evaluateGradient(renderer_ColorKeys, renderer_AlphaKeys, relativePosition); } diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index ef22be5a94..0c2880bbc3 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -32,6 +32,8 @@ export class TrailRenderer extends Renderer { private static _widthCurveCountProp = ShaderProperty.getByName("renderer_WidthCurveCount"); private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); + private static _oldestBirthTimeProp = ShaderProperty.getByName("renderer_OldestBirthTime"); + private static _newestBirthTimeProp = ShaderProperty.getByName("renderer_NewestBirthTime"); private static readonly VERTEX_STRIDE = 32; private static readonly VERTEX_FLOAT_STRIDE = 8; private static readonly _pointIncreaseCount = 128; @@ -129,6 +131,21 @@ export class TrailRenderer extends Renderer { shaderData.setFloat(TrailRenderer._widthProp, this.width); shaderData.setInt(TrailRenderer._textureModeProp, this.textureMode); shaderData.setFloat(TrailRenderer._textureScaleProp, this.textureScale); + + // Calculate oldest and newest birth times for UV stretch mode + const activeCount = this._getActivePointCount(); + if (activeCount >= 2) { + const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const vertices = this._vertices; + const oldestBirthTime = vertices[this._firstActiveElement * 2 * floatStride + 3]; + // Newest point is at (firstFree - 1), with wrap handling + const newestIndex = + this._firstFreeElement > 0 ? this._firstFreeElement - 1 : this._currentPointCapacity - 1; + const newestBirthTime = vertices[newestIndex * 2 * floatStride + 3]; + shaderData.setFloat(TrailRenderer._oldestBirthTimeProp, oldestBirthTime); + shaderData.setFloat(TrailRenderer._newestBirthTimeProp, newestBirthTime); + } + this._updateWidthCurve(shaderData); this._updateColorGradient(shaderData); } From df12aa2744520f9bcc05ce4ed870d47cb233be1f Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 11:17:02 +0800 Subject: [PATCH 31/85] feat: update TrailRenderer settings and optimize background color --- e2e/case/trailRenderer-basic.ts | 13 +++++-------- .../originImage/Trail_trailRenderer-basic.jpg | 4 ++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/e2e/case/trailRenderer-basic.ts b/e2e/case/trailRenderer-basic.ts index 137329b2b0..0576123fb9 100644 --- a/e2e/case/trailRenderer-basic.ts +++ b/e2e/case/trailRenderer-basic.ts @@ -4,6 +4,7 @@ */ import { AssetType, + BlendMode, Camera, Color, CurveKey, @@ -31,7 +32,7 @@ WebGLEngine.create({ const scene = engine.sceneManager.activeScene; const rootEntity = scene.createRootEntity(); - scene.background.solidColor = new Color(0.1, 0.1, 0.15, 1); + scene.background.solidColor = new Color(0.1, 0.1, 0.1, 1); // Create camera const cameraEntity = rootEntity.createChild("camera"); @@ -47,11 +48,11 @@ WebGLEngine.create({ // Add TrailRenderer component const trail = trailEntity.addComponent(TrailRenderer); const material = new TrailMaterial(engine); + material.blendMode = BlendMode.Additive; trail.setMaterial(material); trail.time = 2.0; trail.width = 0.5; - trail.minVertexDistance = 0.05; - trail.color.set(1, 0.5, 0, 1); + trail.minVertexDistance = 0.2; // Setup width curve (taper from head to tail) trail.widthCurve = new ParticleCompositeCurve( @@ -65,11 +66,7 @@ WebGLEngine.create({ new GradientColorKey(0.5, new Color(1, 0, 0.5, 1)), new GradientColorKey(1, new Color(0, 0.5, 1, 1)) ], - [ - new GradientAlphaKey(0, 1), - new GradientAlphaKey(0.7, 0.8), - new GradientAlphaKey(1, 0) - ] + [new GradientAlphaKey(0, 1), new GradientAlphaKey(0.7, 0.8), new GradientAlphaKey(1, 0)] ); trail.colorGradient = gradient; diff --git a/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg b/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg index d4c04376e1..ede3eeaaaa 100644 --- a/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg +++ b/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb1a31551a907e4b71000e74f6a87b09cb5ace36c6e79e63afec6c6b7a3eee92 -size 21056 +oid sha256:6ebbc5a9ff36dc8a0889bd766d67d4c9291294af80da6d1f2d98bd65e99ba3fc +size 18741 From 36eb579ee74771b26adb044b1a616434cdfc933f Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 11:32:48 +0800 Subject: [PATCH 32/85] feat: enhance TrailRenderer by managing retired points and optimizing buffer handling --- packages/core/src/trail/TrailRenderer.ts | 47 +++++++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 0c2880bbc3..2bf334e237 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -78,6 +78,8 @@ export class TrailRenderer extends Renderer { @ignoreClone private _firstFreeElement = 0; @ignoreClone + private _firstRetiredElement = 0; + @ignoreClone private _currentPointCapacity = 0; @ignoreClone private _bufferResized = false; @@ -111,6 +113,7 @@ export class TrailRenderer extends Renderer { this._firstActiveElement = 0; this._firstNewElement = 0; this._firstFreeElement = 0; + this._firstRetiredElement = 0; this._hasLastPosition = false; this._boundsDirty = true; } @@ -119,6 +122,7 @@ export class TrailRenderer extends Renderer { super._update(context); this._playTime += this.engine.time.deltaTime; + this._freeRetiredPoints(); this._retireActivePoints(); if (this.emitting) { @@ -270,6 +274,9 @@ export class TrailRenderer extends Renderer { if (this._firstActiveElement > firstFreeElement) { this._firstActiveElement += increaseCount; } + if (this._firstRetiredElement > firstFreeElement) { + this._firstRetiredElement += increaseCount; + } this._bufferResized = true; } @@ -291,14 +298,22 @@ export class TrailRenderer extends Renderer { } } + /** + * Move expired points from active to retired state. + * Points in retired state are waiting for GPU to finish rendering before they can be freed. + */ private _retireActivePoints(): void { const { _playTime: currentTime, time: lifetime, _vertices: vertices, _currentPointCapacity: capacity } = this; const firstActiveOld = this._firstActiveElement; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const frameCount = this.engine.time.frameCount; while (this._firstActiveElement !== this._firstFreeElement) { - const birthTime = vertices[this._firstActiveElement * 2 * floatStride + 3]; + const offset = this._firstActiveElement * 2 * floatStride + 3; + const birthTime = vertices[offset]; if (currentTime - birthTime < lifetime) break; + // Record the frame when this point was retired (reuse birthTime field) + vertices[offset] = frameCount; this._firstActiveElement = (this._firstActiveElement + 1) % capacity; } @@ -310,6 +325,30 @@ export class TrailRenderer extends Renderer { } } + /** + * Free retired points that GPU has finished rendering. + * WebGL doesn't support mapBufferRange, so this optimization is currently disabled. + * The condition `frameCount - retireFrame < 0` will never be true, effectively skipping the check. + */ + private _freeRetiredPoints(): void { + const frameCount = this.engine.time.frameCount; + const capacity = this._currentPointCapacity; + const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const vertices = this._vertices; + + while (this._firstRetiredElement !== this._firstActiveElement) { + const retireFrame = vertices[this._firstRetiredElement * 2 * floatStride + 3]; + + // WebGL doesn't support mapBufferRange, so this optimization is disabled. + // When mapBufferRange is available, change condition to check if GPU finished rendering. + if (frameCount - retireFrame < 0) { + break; + } + + this._firstRetiredElement = (this._firstRetiredElement + 1) % capacity; + } + } + private _tryAddNewPoint(): void { const worldPosition = this.entity.transform.worldPosition; @@ -317,8 +356,12 @@ export class TrailRenderer extends Renderer { return; } + // Using 'nextFreeElement' instead of 'freeElement' when comparing with '_firstRetiredElement' + // aids in definitively identifying the head and tail of the circular queue. + // Failure to adopt this approach may impede growth initiation + // due to the initial alignment of 'freeElement' and 'firstRetiredElement'. const nextFreeElement = (this._firstFreeElement + 1) % this._currentPointCapacity; - if (nextFreeElement === this._firstActiveElement) { + if (nextFreeElement === this._firstRetiredElement) { this._resizeBuffer(TrailRenderer._pointIncreaseCount); } From a113b8fd624b64f259eec62b7151e07b80b1c7ea Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 11:57:05 +0800 Subject: [PATCH 33/85] feat: optimize TrailRenderer update logic and improve point management --- packages/core/src/trail/TrailRenderer.ts | 82 +++++++++++++----------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 2bf334e237..d32bb60b63 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -121,9 +121,10 @@ export class TrailRenderer extends Renderer { protected override _update(context: RenderContext): void { super._update(context); - this._playTime += this.engine.time.deltaTime; - this._freeRetiredPoints(); - this._retireActivePoints(); + const time = this.engine.time; + this._playTime += time.deltaTime; + this._freeRetiredPoints(time.frameCount); + this._retireActivePoints(time.frameCount); if (this.emitting) { this._tryAddNewPoint(); @@ -137,14 +138,13 @@ export class TrailRenderer extends Renderer { shaderData.setFloat(TrailRenderer._textureScaleProp, this.textureScale); // Calculate oldest and newest birth times for UV stretch mode - const activeCount = this._getActivePointCount(); - if (activeCount >= 2) { + const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _currentPointCapacity: capacity } = this; + if (firstActive !== firstFree) { const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const vertices = this._vertices; - const oldestBirthTime = vertices[this._firstActiveElement * 2 * floatStride + 3]; + const oldestBirthTime = vertices[firstActive * 2 * floatStride + 3]; // Newest point is at (firstFree - 1), with wrap handling - const newestIndex = - this._firstFreeElement > 0 ? this._firstFreeElement - 1 : this._currentPointCapacity - 1; + const newestIndex = firstFree > 0 ? firstFree - 1 : capacity - 1; const newestBirthTime = vertices[newestIndex * 2 * floatStride + 3]; shaderData.setFloat(TrailRenderer._oldestBirthTimeProp, oldestBirthTime); shaderData.setFloat(TrailRenderer._newestBirthTimeProp, newestBirthTime); @@ -155,8 +155,8 @@ export class TrailRenderer extends Renderer { } protected override _render(context: RenderContext): void { - const activeCount = this._getActivePointCount(); - if (activeCount < 2) return; + // Need at least 2 points to form a trail segment + if (this._getActivePointCount() < 2) return; if (this._firstNewElement !== this._firstFreeElement || this._vertexBuffer.isContentLost) { this._uploadNewVertices(); @@ -196,10 +196,9 @@ export class TrailRenderer extends Renderer { } protected override _updateBounds(worldBounds: BoundingBox): void { - const activeCount = this._getActivePointCount(); const halfWidth = this.width * 0.5; - if (activeCount === 0) { + if (this._firstActiveElement === this._firstFreeElement) { const worldPosition = this.entity.transform.worldPosition; worldBounds.min.set(worldPosition.x - halfWidth, worldPosition.y - halfWidth, worldPosition.z - halfWidth); worldBounds.max.set(worldPosition.x + halfWidth, worldPosition.y + halfWidth, worldPosition.z + halfWidth); @@ -302,11 +301,10 @@ export class TrailRenderer extends Renderer { * Move expired points from active to retired state. * Points in retired state are waiting for GPU to finish rendering before they can be freed. */ - private _retireActivePoints(): void { + private _retireActivePoints(frameCount: number): void { const { _playTime: currentTime, time: lifetime, _vertices: vertices, _currentPointCapacity: capacity } = this; const firstActiveOld = this._firstActiveElement; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; - const frameCount = this.engine.time.frameCount; while (this._firstActiveElement !== this._firstFreeElement) { const offset = this._firstActiveElement * 2 * floatStride + 3; @@ -330,8 +328,7 @@ export class TrailRenderer extends Renderer { * WebGL doesn't support mapBufferRange, so this optimization is currently disabled. * The condition `frameCount - retireFrame < 0` will never be true, effectively skipping the check. */ - private _freeRetiredPoints(): void { - const frameCount = this.engine.time.frameCount; + private _freeRetiredPoints(frameCount: number): void { const capacity = this._currentPointCapacity; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const vertices = this._vertices; @@ -374,6 +371,7 @@ export class TrailRenderer extends Renderer { const pointIndex = this._firstFreeElement; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const vertices = this._vertices; + const playTime = this._playTime; const tangent = TrailRenderer._tempVector3; if (this._hasLastPosition) { @@ -383,10 +381,10 @@ export class TrailRenderer extends Renderer { // First point has placeholder tangent, update it when second point is added if (this._getActivePointCount() === 1) { const firstPointIndex = this._firstActiveElement; - for (let corner = -1; corner <= 1; corner += 2) { - const vertexIndex = firstPointIndex * 2 + (corner === -1 ? 0 : 1); - tangent.copyToArray(vertices, vertexIndex * floatStride + 5); - } + const offset0 = firstPointIndex * 2 * floatStride + 5; + const offset1 = offset0 + floatStride; + tangent.copyToArray(vertices, offset0); + tangent.copyToArray(vertices, offset1); // Mark first point for re-upload since its tangent changed this._firstNewElement = this._firstActiveElement; } @@ -395,24 +393,32 @@ export class TrailRenderer extends Renderer { tangent.set(0, 0, 1); } - // Write vertex data for top and bottom vertices (corner = -1 and 1) - for (let corner = -1; corner <= 1; corner += 2) { - const vertexIndex = pointIndex * 2 + (corner === -1 ? 0 : 1); - const offset = vertexIndex * floatStride; - - position.copyToArray(vertices, offset); - vertices[offset + 3] = this._playTime; - vertices[offset + 4] = corner; - tangent.copyToArray(vertices, offset + 5); - - // Also write to bridge position when writing point 0 - if (pointIndex === 0) { - const bridgeOffset = (this._currentPointCapacity * 2 + (corner === -1 ? 0 : 1)) * floatStride; - position.copyToArray(vertices, bridgeOffset); - vertices[bridgeOffset + 3] = this._playTime; - vertices[bridgeOffset + 4] = corner; - tangent.copyToArray(vertices, bridgeOffset + 5); - } + // Write vertex data for top vertex (corner = -1) + const topOffset = pointIndex * 2 * floatStride; + position.copyToArray(vertices, topOffset); + vertices[topOffset + 3] = playTime; + vertices[topOffset + 4] = -1; + tangent.copyToArray(vertices, topOffset + 5); + + // Write vertex data for bottom vertex (corner = 1) + const bottomOffset = topOffset + floatStride; + position.copyToArray(vertices, bottomOffset); + vertices[bottomOffset + 3] = playTime; + vertices[bottomOffset + 4] = 1; + tangent.copyToArray(vertices, bottomOffset + 5); + + // Also write to bridge position when writing point 0 + if (pointIndex === 0) { + const bridgeTopOffset = this._currentPointCapacity * 2 * floatStride; + const bridgeBottomOffset = bridgeTopOffset + floatStride; + position.copyToArray(vertices, bridgeTopOffset); + vertices[bridgeTopOffset + 3] = playTime; + vertices[bridgeTopOffset + 4] = -1; + tangent.copyToArray(vertices, bridgeTopOffset + 5); + position.copyToArray(vertices, bridgeBottomOffset); + vertices[bridgeBottomOffset + 3] = playTime; + vertices[bridgeBottomOffset + 4] = 1; + tangent.copyToArray(vertices, bridgeBottomOffset + 5); } this._expandBounds(position); From 69d2e826bc8c0b6d1f866deffb18c417bf24aaa3 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 12:04:02 +0800 Subject: [PATCH 34/85] feat: refine width curve handling in TrailRenderer for improved flexibility --- packages/core/src/trail/TrailRenderer.ts | 31 ++++++++---------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index d32bb60b63..4eb4f97e3a 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -12,6 +12,7 @@ import { BufferBindFlag } from "../graphic/enums/BufferBindFlag"; import { BufferUsage } from "../graphic/enums/BufferUsage"; import { MeshTopology } from "../graphic/enums/MeshTopology"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; +import { ParticleCurveMode } from "../particle/enums/ParticleCurveMode"; import { ParticleCompositeCurve } from "../particle/modules/ParticleCompositeCurve"; import { GradientAlphaKey, GradientColorKey, ParticleGradient } from "../particle/modules/ParticleGradient"; import { ShaderData } from "../shader/ShaderData"; @@ -38,6 +39,7 @@ export class TrailRenderer extends Renderer { private static readonly VERTEX_FLOAT_STRIDE = 8; private static readonly _pointIncreaseCount = 128; private static _tempVector3 = new Vector3(); + private static _defaultWidthCurve = new Float32Array([0, 1, 0, 0, 0, 0, 0, 0]); /** How long the trail points last (in seconds). */ time = 5.0; @@ -95,8 +97,6 @@ export class TrailRenderer extends Renderer { private _boundsMax = new Vector3(); @ignoreClone private _boundsDirty = true; - @ignoreClone - private _widthCurveData: Float32Array; /** * @internal @@ -511,27 +511,16 @@ export class TrailRenderer extends Renderer { private _updateWidthCurve(shaderData: ShaderData): void { const curve = this.widthCurve; - const widthCurveData = this._widthCurveData || (this._widthCurveData = new Float32Array(8)); + const mode = curve.mode; - if (curve.mode === 0) { - widthCurveData[0] = 0; - widthCurveData[1] = curve.constant; - shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurveData); - shaderData.setInt(TrailRenderer._widthCurveCountProp, 1); - } else if (curve.mode === 2 && curve.curve) { - const keys = curve.curve.keys; - const count = Math.min(keys.length, 4); - for (let i = 0, offset = 0; i < count; i++, offset += 2) { - const key = keys[i]; - widthCurveData[offset] = key.time; - widthCurveData[offset + 1] = key.value; - } - shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurveData); - shaderData.setInt(TrailRenderer._widthCurveCountProp, count); + if (mode === ParticleCurveMode.Curve && curve.curve) { + shaderData.setFloatArray(TrailRenderer._widthCurveProp, curve.curve._getTypeArray()); + shaderData.setInt(TrailRenderer._widthCurveCountProp, curve.curve.keys.length); } else { - widthCurveData[0] = 0; - widthCurveData[1] = 1; - shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurveData); + // For Constant mode, use constant value; otherwise default to 1.0 + const value = mode === ParticleCurveMode.Constant ? curve.constant : 1.0; + shaderData.setFloatArray(TrailRenderer._widthCurveProp, TrailRenderer._defaultWidthCurve); + TrailRenderer._defaultWidthCurve[1] = value; shaderData.setInt(TrailRenderer._widthCurveCountProp, 1); } } From 66ef04e115f32bc02a213f42dda0db969d6ed544 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 12:13:59 +0800 Subject: [PATCH 35/85] feat: update width curve handling in TrailRenderer for improved clarity and functionality --- packages/core/src/trail/TrailRenderer.ts | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 4eb4f97e3a..ab36510275 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -12,8 +12,7 @@ import { BufferBindFlag } from "../graphic/enums/BufferBindFlag"; import { BufferUsage } from "../graphic/enums/BufferUsage"; import { MeshTopology } from "../graphic/enums/MeshTopology"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; -import { ParticleCurveMode } from "../particle/enums/ParticleCurveMode"; -import { ParticleCompositeCurve } from "../particle/modules/ParticleCompositeCurve"; +import { CurveKey, ParticleCurve } from "../particle/modules/ParticleCurve"; import { GradientAlphaKey, GradientColorKey, ParticleGradient } from "../particle/modules/ParticleGradient"; import { ShaderData } from "../shader/ShaderData"; import { ShaderProperty } from "../shader/ShaderProperty"; @@ -39,7 +38,6 @@ export class TrailRenderer extends Renderer { private static readonly VERTEX_FLOAT_STRIDE = 8; private static readonly _pointIncreaseCount = 128; private static _tempVector3 = new Vector3(); - private static _defaultWidthCurve = new Float32Array([0, 1, 0, 0, 0, 0, 0, 0]); /** How long the trail points last (in seconds). */ time = 5.0; @@ -51,10 +49,10 @@ export class TrailRenderer extends Renderer { textureMode = TrailTextureMode.Stretch; /** The texture scale for Tile texture mode. */ textureScale = 1.0; - /** Width curve over lifetime (0 = head, 1 = tail). */ + /** Width multiplier curve over lifetime, evaluated from the newest point to the oldest point. */ @deepClone - widthCurve = new ParticleCompositeCurve(1.0); - /** Color gradient over lifetime (0 = head, 1 = tail). */ + widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 1)); + /** Color gradient over lifetime, evaluated from the newest point to the oldest point. */ @deepClone colorGradient = new ParticleGradient( [new GradientColorKey(0, new Color(1, 1, 1, 1)), new GradientColorKey(1, new Color(1, 1, 1, 1))], @@ -511,18 +509,8 @@ export class TrailRenderer extends Renderer { private _updateWidthCurve(shaderData: ShaderData): void { const curve = this.widthCurve; - const mode = curve.mode; - - if (mode === ParticleCurveMode.Curve && curve.curve) { - shaderData.setFloatArray(TrailRenderer._widthCurveProp, curve.curve._getTypeArray()); - shaderData.setInt(TrailRenderer._widthCurveCountProp, curve.curve.keys.length); - } else { - // For Constant mode, use constant value; otherwise default to 1.0 - const value = mode === ParticleCurveMode.Constant ? curve.constant : 1.0; - shaderData.setFloatArray(TrailRenderer._widthCurveProp, TrailRenderer._defaultWidthCurve); - TrailRenderer._defaultWidthCurve[1] = value; - shaderData.setInt(TrailRenderer._widthCurveCountProp, 1); - } + shaderData.setFloatArray(TrailRenderer._widthCurveProp, curve._getTypeArray()); + shaderData.setInt(TrailRenderer._widthCurveCountProp, curve.keys.length); } private _updateColorGradient(shaderData: ShaderData): void { From a1a9c500cdb4b5e1437d8e8f31a24556d099afe6 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 12:20:26 +0800 Subject: [PATCH 36/85] feat: streamline shader data updates in TrailRenderer for improved performance --- e2e/case/trailRenderer-basic.ts | 5 +--- packages/core/src/trail/TrailRenderer.ts | 33 ++++++++++-------------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/e2e/case/trailRenderer-basic.ts b/e2e/case/trailRenderer-basic.ts index 0576123fb9..4e3c0a3fbc 100644 --- a/e2e/case/trailRenderer-basic.ts +++ b/e2e/case/trailRenderer-basic.ts @@ -11,7 +11,6 @@ import { GradientAlphaKey, GradientColorKey, Logger, - ParticleCompositeCurve, ParticleCurve, ParticleGradient, Script, @@ -55,9 +54,7 @@ WebGLEngine.create({ trail.minVertexDistance = 0.2; // Setup width curve (taper from head to tail) - trail.widthCurve = new ParticleCompositeCurve( - new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 0)) - ); + trail.widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 0)); // Setup color gradient (orange to blue with fade out) const gradient = new ParticleGradient( diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index ab36510275..28fdc77fed 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -14,7 +14,6 @@ import { MeshTopology } from "../graphic/enums/MeshTopology"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; import { CurveKey, ParticleCurve } from "../particle/modules/ParticleCurve"; import { GradientAlphaKey, GradientColorKey, ParticleGradient } from "../particle/modules/ParticleGradient"; -import { ShaderData } from "../shader/ShaderData"; import { ShaderProperty } from "../shader/ShaderProperty"; import { TrailTextureMode } from "./enums/TrailTextureMode"; @@ -148,8 +147,11 @@ export class TrailRenderer extends Renderer { shaderData.setFloat(TrailRenderer._newestBirthTimeProp, newestBirthTime); } - this._updateWidthCurve(shaderData); - this._updateColorGradient(shaderData); + const { widthCurve, colorGradient } = this; + shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurve._getTypeArray()); + shaderData.setInt(TrailRenderer._widthCurveCountProp, widthCurve.keys.length); + shaderData.setFloatArray(TrailRenderer._colorKeysProp, colorGradient._getColorTypeArray()); + shaderData.setFloatArray(TrailRenderer._alphaKeysProp, colorGradient._getAlphaTypeArray()); } protected override _render(context: RenderContext): void { @@ -243,7 +245,13 @@ export class TrailRenderer extends Renderer { const vertexCount = newCapacity * 2 + 2; // +2 vertices for bridge point // Create new vertex buffer (no index buffer needed - using drawArrays) - const newVertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, vertexCount * byteStride, BufferUsage.Dynamic, false); + const newVertexBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + vertexCount * byteStride, + BufferUsage.Dynamic, + false + ); const newVertices = new Float32Array(vertexCount * floatStride); // Migrate existing vertex data if any @@ -258,10 +266,7 @@ export class TrailRenderer extends Renderer { const nextFreeElement = firstFreeElement + 1; if (nextFreeElement < this._currentPointCapacity) { const freeEndOffset = (nextFreeElement + increaseCount) * 2 * floatStride; - newVertices.set( - new Float32Array(lastVertices.buffer, nextFreeElement * 2 * floatStride * 4), - freeEndOffset - ); + newVertices.set(new Float32Array(lastVertices.buffer, nextFreeElement * 2 * floatStride * 4), freeEndOffset); } // Update pointers @@ -506,16 +511,4 @@ export class TrailRenderer extends Renderer { this._firstNewElement = firstFree; } - - private _updateWidthCurve(shaderData: ShaderData): void { - const curve = this.widthCurve; - shaderData.setFloatArray(TrailRenderer._widthCurveProp, curve._getTypeArray()); - shaderData.setInt(TrailRenderer._widthCurveCountProp, curve.keys.length); - } - - private _updateColorGradient(shaderData: ShaderData): void { - const gradient = this.colorGradient; - shaderData.setFloatArray(TrailRenderer._colorKeysProp, gradient._getColorTypeArray()); - shaderData.setFloatArray(TrailRenderer._alphaKeysProp, gradient._getAlphaTypeArray()); - } } From ea58aa75ee197c56ba6a5d18c635d42508f3edbf Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 12:38:15 +0800 Subject: [PATCH 37/85] feat: simplify width curve evaluation in TrailRenderer for improved clarity --- .../core/src/shaderlib/extra/trail.vs.glsl | 27 +++++++++---------- packages/core/src/trail/TrailRenderer.ts | 2 -- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index a89867dd71..af7bb5ae4e 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -18,7 +18,6 @@ uniform mat4 camera_ProjMat; // Width curve uniforms (4 keyframes max: x=time, y=value) uniform vec2 renderer_WidthCurve[4]; -uniform int renderer_WidthCurveCount; // Color gradient uniforms (4 keyframes max) uniform vec4 renderer_ColorKeys[4]; // x=time, yzw=rgb @@ -28,21 +27,21 @@ uniform vec2 renderer_AlphaKeys[4]; // x=time, y=alpha varying vec2 v_uv; varying vec4 v_color; -// Evaluate width curve at normalized age -float evaluateCurve(in vec2 keys[4], in int count, in float t) { - if (count <= 0) return 1.0; - if (count == 1) return keys[0].y; - +// Evaluate width curve at normalized age (same as particle system) +float evaluateCurve(in vec2 keys[4], in float t) { + float value; for (int i = 1; i < 4; i++) { - if (i >= count) break; - if (t <= keys[i].x) { - float t0 = keys[i - 1].x; - float t1 = keys[i].x; - float factor = (t - t0) / (t1 - t0); - return mix(keys[i - 1].y, keys[i].y, factor); + vec2 key = keys[i]; + float time = key.x; + if (time >= t) { + vec2 lastKey = keys[i - 1]; + float lastTime = lastKey.x; + float age = (t - lastTime) / (time - lastTime); + value = mix(lastKey.y, key.y, age); + break; } } - return keys[count - 1].y; + return value; } // Evaluate color gradient at normalized age (fixed 4 iterations) @@ -118,7 +117,7 @@ void main() { right = right / rightLen; // Evaluate width curve using relative position - float widthMultiplier = evaluateCurve(renderer_WidthCurve, renderer_WidthCurveCount, relativePosition); + float widthMultiplier = evaluateCurve(renderer_WidthCurve, relativePosition); float width = renderer_Width * widthMultiplier; // Apply offset diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 28fdc77fed..c6c84de300 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -28,7 +28,6 @@ export class TrailRenderer extends Renderer { private static _textureModeProp = ShaderProperty.getByName("renderer_TextureMode"); private static _textureScaleProp = ShaderProperty.getByName("renderer_TextureScale"); private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); - private static _widthCurveCountProp = ShaderProperty.getByName("renderer_WidthCurveCount"); private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); private static _oldestBirthTimeProp = ShaderProperty.getByName("renderer_OldestBirthTime"); @@ -149,7 +148,6 @@ export class TrailRenderer extends Renderer { const { widthCurve, colorGradient } = this; shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurve._getTypeArray()); - shaderData.setInt(TrailRenderer._widthCurveCountProp, widthCurve.keys.length); shaderData.setFloatArray(TrailRenderer._colorKeysProp, colorGradient._getColorTypeArray()); shaderData.setFloatArray(TrailRenderer._alphaKeysProp, colorGradient._getAlphaTypeArray()); } From 8c7bececeffb548e7eee6fc3287458dba7515e0f Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 13:56:31 +0800 Subject: [PATCH 38/85] feat: refactor TrailRenderer to use packed uniforms for time and trail parameters --- .../core/src/shaderlib/extra/trail.vs.glsl | 34 ++++--- packages/core/src/trail/TrailRenderer.ts | 98 +++++++++++++------ 2 files changed, 88 insertions(+), 44 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index af7bb5ae4e..3256115a13 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -4,13 +4,10 @@ attribute vec4 a_PositionBirthTime; // xyz: World position, w: Birth time attribute vec4 a_CornerTangent; // x: Corner (-1 or 1), yzw: Tangent direction // Uniforms -uniform float renderer_CurrentTime; -uniform float renderer_Lifetime; -uniform float renderer_Width; -uniform int renderer_TextureMode; // 0: Stretch, 1: Tile -uniform float renderer_TextureScale; -uniform float renderer_OldestBirthTime; // Birth time of oldest (tail) point -uniform float renderer_NewestBirthTime; // Birth time of newest (head) point +// x: CurrentTime, y: Lifetime, z: OldestBirthTime, w: NewestBirthTime +uniform vec4 renderer_TimeParams; +// x: Width, y: TextureMode (0: Stretch, 1: Tile), z: TextureScale +uniform vec4 renderer_TrailParams; uniform vec3 camera_Position; uniform mat4 camera_ViewMat; @@ -82,9 +79,18 @@ void main() { float corner = a_CornerTangent.x; vec3 tangent = a_CornerTangent.yzw; + // Extract packed uniforms + float currentTime = renderer_TimeParams.x; + float lifetime = renderer_TimeParams.y; + float oldestBirthTime = renderer_TimeParams.z; + float newestBirthTime = renderer_TimeParams.w; + float trailWidth = renderer_TrailParams.x; + float textureMode = renderer_TrailParams.y; + float textureScale = renderer_TrailParams.z; + // Calculate normalized age (0 = new, 1 = about to die) for lifetime check - float age = renderer_CurrentTime - birthTime; - float normalizedAge = clamp(age / renderer_Lifetime, 0.0, 1.0); + float age = currentTime - birthTime; + float normalizedAge = clamp(age / lifetime, 0.0, 1.0); // Discard vertices that have exceeded their lifetime if (normalizedAge >= 1.0) { @@ -94,10 +100,10 @@ void main() { // Calculate relative position in trail (0 = head/newest, 1 = tail/oldest) // Used for width curve, color gradient, and UV in Stretch mode - float timeRange = renderer_NewestBirthTime - renderer_OldestBirthTime; + float timeRange = newestBirthTime - oldestBirthTime; float relativePosition = 0.0; if (timeRange > 0.0001) { - relativePosition = (renderer_NewestBirthTime - birthTime) / timeRange; + relativePosition = (newestBirthTime - birthTime) / timeRange; } // Calculate billboard offset (View alignment) @@ -118,7 +124,7 @@ void main() { // Evaluate width curve using relative position float widthMultiplier = evaluateCurve(renderer_WidthCurve, relativePosition); - float width = renderer_Width * widthMultiplier; + float width = trailWidth * widthMultiplier; // Apply offset vec3 worldPosition = position + right * width * 0.5 * corner; @@ -129,12 +135,12 @@ void main() { float u = corner * 0.5 + 0.5; // 0 for bottom, 1 for top float v; - if (renderer_TextureMode == 0) { + if (textureMode < 0.5) { // Stretch mode: UV.v based on relative position in trail v = relativePosition; } else { // Tile mode: scale by tile scale (use normalizedAge for tiling effect) - v = normalizedAge * renderer_TextureScale; + v = normalizedAge * textureScale; } v_uv = vec2(u, v); diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index c6c84de300..594644e5e2 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -1,7 +1,7 @@ -import { BoundingBox, Color, Vector3 } from "@galacean/engine-math"; +import { BoundingBox, Color, Vector3, Vector4 } from "@galacean/engine-math"; import { Entity } from "../Entity"; -import { Renderer } from "../Renderer"; import { RenderContext } from "../RenderPipeline/RenderContext"; +import { Renderer } from "../Renderer"; import { deepClone, ignoreClone } from "../clone/CloneManager"; import { Buffer } from "../graphic/Buffer"; import { Primitive } from "../graphic/Primitive"; @@ -22,31 +22,68 @@ import { TrailTextureMode } from "./enums/TrailTextureMode"; * Renders a trail behind a moving object. */ export class TrailRenderer extends Renderer { - private static _currentTimeProp = ShaderProperty.getByName("renderer_CurrentTime"); - private static _lifetimeProp = ShaderProperty.getByName("renderer_Lifetime"); - private static _widthProp = ShaderProperty.getByName("renderer_Width"); - private static _textureModeProp = ShaderProperty.getByName("renderer_TextureMode"); - private static _textureScaleProp = ShaderProperty.getByName("renderer_TextureScale"); + private static _timeParamsProp = ShaderProperty.getByName("renderer_TimeParams"); + private static _trailParamsProp = ShaderProperty.getByName("renderer_TrailParams"); private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); - private static _oldestBirthTimeProp = ShaderProperty.getByName("renderer_OldestBirthTime"); - private static _newestBirthTimeProp = ShaderProperty.getByName("renderer_NewestBirthTime"); private static readonly VERTEX_STRIDE = 32; private static readonly VERTEX_FLOAT_STRIDE = 8; private static readonly _pointIncreaseCount = 128; private static _tempVector3 = new Vector3(); - /** How long the trail points last (in seconds). */ - time = 5.0; - /** The width of the trail. */ - width = 1.0; /** The minimum distance between trail points. */ minVertexDistance = 0.1; - /** Controls how the texture is applied to the trail. */ - textureMode = TrailTextureMode.Stretch; - /** The texture scale for Tile texture mode. */ - textureScale = 1.0; + + // x: currentTime, y: lifetime, z: oldestBirthTime, w: newestBirthTime + private _timeParams = new Vector4(0, 5.0, 0, 0); + // x: width, y: textureMode, z: textureScale + private _trailParams = new Vector4(1.0, TrailTextureMode.Stretch, 1.0, 0); + + /** + * How long the trail points last (in seconds). + */ + get time(): number { + return this._timeParams.y; + } + + set time(value: number) { + this._timeParams.y = value; + } + + /** + * The width of the trail. + */ + get width(): number { + return this._trailParams.x; + } + + set width(value: number) { + this._trailParams.x = value; + } + + /** + * Controls how the texture is applied to the trail. + */ + get textureMode(): TrailTextureMode { + return this._trailParams.y; + } + + set textureMode(value: TrailTextureMode) { + this._trailParams.y = value; + } + + /** + * The texture scale for Tile texture mode. + */ + get textureScale(): number { + return this._trailParams.z; + } + + set textureScale(value: number) { + this._trailParams.z = value; + } + /** Width multiplier curve over lifetime, evaluated from the newest point to the oldest point. */ @deepClone widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 1)); @@ -56,6 +93,7 @@ export class TrailRenderer extends Renderer { [new GradientColorKey(0, new Color(1, 1, 1, 1)), new GradientColorKey(1, new Color(1, 1, 1, 1))], [new GradientAlphaKey(0, 1), new GradientAlphaKey(1, 1)] ); + /** Whether the trail is currently emitting new points. */ emitting = true; @@ -127,27 +165,27 @@ export class TrailRenderer extends Renderer { } const shaderData = this.shaderData; - shaderData.setFloat(TrailRenderer._currentTimeProp, this._playTime); - shaderData.setFloat(TrailRenderer._lifetimeProp, this.time); - shaderData.setFloat(TrailRenderer._widthProp, this.width); - shaderData.setInt(TrailRenderer._textureModeProp, this.textureMode); - shaderData.setFloat(TrailRenderer._textureScaleProp, this.textureScale); + const timeParams = this._timeParams; - // Calculate oldest and newest birth times for UV stretch mode + // Update dynamic time params (currentTime, oldestBirthTime, newestBirthTime) + timeParams.x = this._playTime; const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _currentPointCapacity: capacity } = this; if (firstActive !== firstFree) { const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const vertices = this._vertices; - const oldestBirthTime = vertices[firstActive * 2 * floatStride + 3]; - // Newest point is at (firstFree - 1), with wrap handling + timeParams.z = vertices[firstActive * 2 * floatStride + 3]; const newestIndex = firstFree > 0 ? firstFree - 1 : capacity - 1; - const newestBirthTime = vertices[newestIndex * 2 * floatStride + 3]; - shaderData.setFloat(TrailRenderer._oldestBirthTimeProp, oldestBirthTime); - shaderData.setFloat(TrailRenderer._newestBirthTimeProp, newestBirthTime); + timeParams.w = vertices[newestIndex * 2 * floatStride + 3]; + } else { + timeParams.z = 0; + timeParams.w = 0; } - const { widthCurve, colorGradient } = this; - shaderData.setFloatArray(TrailRenderer._widthCurveProp, widthCurve._getTypeArray()); + shaderData.setVector4(TrailRenderer._timeParamsProp, timeParams); + shaderData.setVector4(TrailRenderer._trailParamsProp, this._trailParams); + + const { colorGradient } = this; + shaderData.setFloatArray(TrailRenderer._widthCurveProp, this.widthCurve._getTypeArray()); shaderData.setFloatArray(TrailRenderer._colorKeysProp, colorGradient._getColorTypeArray()); shaderData.setFloatArray(TrailRenderer._alphaKeysProp, colorGradient._getAlphaTypeArray()); } From eab4d2ee171c5cabb55eaf42e36b0e2a8dcc2b0d Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 14:01:44 +0800 Subject: [PATCH 39/85] feat: consolidate static constants in TrailRenderer for improved clarity and consistency --- packages/core/src/trail/TrailRenderer.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 594644e5e2..49b198a953 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -22,14 +22,16 @@ import { TrailTextureMode } from "./enums/TrailTextureMode"; * Renders a trail behind a moving object. */ export class TrailRenderer extends Renderer { + private static readonly VERTEX_STRIDE = 32; + private static readonly VERTEX_FLOAT_STRIDE = 8; + private static readonly POINT_INCREASE_COUNT = 128; + private static _timeParamsProp = ShaderProperty.getByName("renderer_TimeParams"); private static _trailParamsProp = ShaderProperty.getByName("renderer_TrailParams"); private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); - private static readonly VERTEX_STRIDE = 32; - private static readonly VERTEX_FLOAT_STRIDE = 8; - private static readonly _pointIncreaseCount = 128; + private static _tempVector3 = new Vector3(); /** The minimum distance between trail points. */ @@ -267,7 +269,7 @@ export class TrailRenderer extends Renderer { this._subPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); this._subPrimitive2 = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); - this._resizeBuffer(TrailRenderer._pointIncreaseCount); + this._resizeBuffer(TrailRenderer.POINT_INCREASE_COUNT); } private _resizeBuffer(increaseCount: number): void { @@ -398,7 +400,7 @@ export class TrailRenderer extends Renderer { // due to the initial alignment of 'freeElement' and 'firstRetiredElement'. const nextFreeElement = (this._firstFreeElement + 1) % this._currentPointCapacity; if (nextFreeElement === this._firstRetiredElement) { - this._resizeBuffer(TrailRenderer._pointIncreaseCount); + this._resizeBuffer(TrailRenderer.POINT_INCREASE_COUNT); } this._addPoint(worldPosition); From 7700d680af758deb33c555cae719d67a201feb88 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 14:05:54 +0800 Subject: [PATCH 40/85] feat: improve comments for clarity in TrailRenderer parameters --- packages/core/src/trail/TrailRenderer.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 49b198a953..19c9e2f0e5 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -37,13 +37,11 @@ export class TrailRenderer extends Renderer { /** The minimum distance between trail points. */ minVertexDistance = 0.1; - // x: currentTime, y: lifetime, z: oldestBirthTime, w: newestBirthTime - private _timeParams = new Vector4(0, 5.0, 0, 0); - // x: width, y: textureMode, z: textureScale - private _trailParams = new Vector4(1.0, TrailTextureMode.Stretch, 1.0, 0); + private _timeParams = new Vector4(0, 5.0, 0, 0); // x: currentTime, y: lifetime, z: oldestBirthTime, w: newestBirthTime + private _trailParams = new Vector4(1.0, TrailTextureMode.Stretch, 1.0, 0); // x: width, y: textureMode, z: textureScale /** - * How long the trail points last (in seconds). + * How long the trail takes to fade out (in seconds). */ get time(): number { return this._timeParams.y; @@ -89,6 +87,7 @@ export class TrailRenderer extends Renderer { /** Width multiplier curve over lifetime, evaluated from the newest point to the oldest point. */ @deepClone widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 1)); + /** Color gradient over lifetime, evaluated from the newest point to the oldest point. */ @deepClone colorGradient = new ParticleGradient( From 2ce991884112aeb99efc262ad78a7a95b2741ed9 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 14:11:37 +0800 Subject: [PATCH 41/85] refactor: opt code --- packages/core/src/trail/TrailRenderer.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 19c9e2f0e5..de8ea853d3 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -87,7 +87,7 @@ export class TrailRenderer extends Renderer { /** Width multiplier curve over lifetime, evaluated from the newest point to the oldest point. */ @deepClone widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 1)); - + /** Color gradient over lifetime, evaluated from the newest point to the oldest point. */ @deepClone colorGradient = new ParticleGradient( @@ -127,9 +127,7 @@ export class TrailRenderer extends Renderer { @ignoreClone private _playTime = 0; @ignoreClone - private _boundsMin = new Vector3(); - @ignoreClone - private _boundsMax = new Vector3(); + private _localBounds = new BoundingBox(); @ignoreClone private _boundsDirty = true; @@ -246,7 +244,7 @@ export class TrailRenderer extends Renderer { this._recalculateBounds(); } - const { _boundsMin: min, _boundsMax: max } = this; + const { min, max } = this._localBounds; worldBounds.min.set(min.x - halfWidth, min.y - halfWidth, min.z - halfWidth); worldBounds.max.set(max.x + halfWidth, max.y + halfWidth, max.z + halfWidth); } @@ -471,7 +469,7 @@ export class TrailRenderer extends Renderer { } private _expandBounds(position: Vector3): void { - const { _boundsMin: min, _boundsMax: max } = this; + const { min, max } = this._localBounds; if (this._boundsDirty) { min.copyFrom(position); @@ -488,7 +486,7 @@ export class TrailRenderer extends Renderer { const vertices = this._vertices; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const activeCount = this._getActivePointCount(); - const { _boundsMin: min, _boundsMax: max } = this; + const { min, max } = this._localBounds; if (activeCount === 0) { min.set(0, 0, 0); From e4a5ecfbc67bab1205d97714b79f26383076355e Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 14:19:27 +0800 Subject: [PATCH 42/85] feat: improve comments for clarity in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index de8ea853d3..949500f966 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -34,14 +34,14 @@ export class TrailRenderer extends Renderer { private static _tempVector3 = new Vector3(); - /** The minimum distance between trail points. */ + /** The minimum distance the object must move before a new trail segment is added. */ minVertexDistance = 0.1; private _timeParams = new Vector4(0, 5.0, 0, 0); // x: currentTime, y: lifetime, z: oldestBirthTime, w: newestBirthTime private _trailParams = new Vector4(1.0, TrailTextureMode.Stretch, 1.0, 0); // x: width, y: textureMode, z: textureScale /** - * How long the trail takes to fade out (in seconds). + * The fade-out duration in seconds. */ get time(): number { return this._timeParams.y; @@ -63,7 +63,7 @@ export class TrailRenderer extends Renderer { } /** - * Controls how the texture is applied to the trail. + * The texture mapping mode for the trail. */ get textureMode(): TrailTextureMode { return this._trailParams.y; @@ -74,7 +74,7 @@ export class TrailRenderer extends Renderer { } /** - * The texture scale for Tile texture mode. + * The texture scale when using Tile texture mode. */ get textureScale(): number { return this._trailParams.z; @@ -84,18 +84,17 @@ export class TrailRenderer extends Renderer { this._trailParams.z = value; } - /** Width multiplier curve over lifetime, evaluated from the newest point to the oldest point. */ + /** The curve describing the trail width from start to end. */ @deepClone widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 1)); - - /** Color gradient over lifetime, evaluated from the newest point to the oldest point. */ + /** The gradient describing the trail color from start to end. */ @deepClone colorGradient = new ParticleGradient( [new GradientColorKey(0, new Color(1, 1, 1, 1)), new GradientColorKey(1, new Color(1, 1, 1, 1))], [new GradientAlphaKey(0, 1), new GradientAlphaKey(1, 1)] ); - /** Whether the trail is currently emitting new points. */ + /** Whether the trail is being created as the object moves. */ emitting = true; @ignoreClone From 18ea3a86bcb960c5eb4afda147c9c205ea4e6a67 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 14:40:50 +0800 Subject: [PATCH 43/85] feat: rename subPrimitive variables for clarity in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 30 +++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 949500f966..7d93bee4f0 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -87,6 +87,7 @@ export class TrailRenderer extends Renderer { /** The curve describing the trail width from start to end. */ @deepClone widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 1)); + /** The gradient describing the trail color from start to end. */ @deepClone colorGradient = new ParticleGradient( @@ -100,9 +101,9 @@ export class TrailRenderer extends Renderer { @ignoreClone private _primitive: Primitive; @ignoreClone - private _subPrimitive: SubPrimitive; + private _mainSubPrimitive: SubPrimitive; @ignoreClone - private _subPrimitive2: SubPrimitive; + private _wrapSubPrimitive: SubPrimitive; @ignoreClone private _vertexBuffer: Buffer; @ignoreClone @@ -205,24 +206,24 @@ export class TrailRenderer extends Renderer { const renderElement = renderElementPool.get(); renderElement.set(this.priority, this._distanceForSort); - // First segment - const subPrimitive = this._subPrimitive; - subPrimitive.start = firstActive * 2; - subPrimitive.count = + // Main segment (always rendered) + const mainSubPrimitive = this._mainSubPrimitive; + mainSubPrimitive.start = firstActive * 2; + mainSubPrimitive.count = firstActive >= firstFree ? (this._currentPointCapacity - firstActive + 1) * 2 // Wrapped: includes bridge : (firstFree - firstActive) * 2; const subRenderElement = subRenderElementPool.get(); - subRenderElement.set(this, material, primitive, subPrimitive); + subRenderElement.set(this, material, primitive, mainSubPrimitive); renderElement.addSubRenderElement(subRenderElement); - // Second segment (wrapped case only) + // Wrap segment (only when buffer wraps around) if (firstActive >= firstFree && firstFree > 0) { - const subPrimitive2 = this._subPrimitive2; - subPrimitive2.start = 0; - subPrimitive2.count = firstFree * 2; + const wrapSubPrimitive = this._wrapSubPrimitive; + wrapSubPrimitive.start = 0; + wrapSubPrimitive.count = firstFree * 2; const subRenderElement2 = subRenderElementPool.get(); - subRenderElement2.set(this, material, primitive, subPrimitive2); + subRenderElement2.set(this, material, primitive, wrapSubPrimitive); renderElement.addSubRenderElement(subRenderElement2); } @@ -262,8 +263,9 @@ export class TrailRenderer extends Renderer { // a_CornerTangent: x = corner (-1 or 1), yzw = tangent direction primitive.addVertexElement(new VertexElement("a_PositionBirthTime", 0, VertexElementFormat.Vector4, 0)); primitive.addVertexElement(new VertexElement("a_CornerTangent", 16, VertexElementFormat.Vector4, 0)); - this._subPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); - this._subPrimitive2 = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); + + this._mainSubPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); + this._wrapSubPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); this._resizeBuffer(TrailRenderer.POINT_INCREASE_COUNT); } From 558d83d85882f4d380694b16e096cf79ecea373d Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 14:51:33 +0800 Subject: [PATCH 44/85] fix: correct vertex count calculation and clean up vertex buffer binding in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 7d93bee4f0..720a2403e3 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -263,7 +263,7 @@ export class TrailRenderer extends Renderer { // a_CornerTangent: x = corner (-1 or 1), yzw = tangent direction primitive.addVertexElement(new VertexElement("a_PositionBirthTime", 0, VertexElementFormat.Vector4, 0)); primitive.addVertexElement(new VertexElement("a_CornerTangent", 16, VertexElementFormat.Vector4, 0)); - + this._mainSubPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); this._wrapSubPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); @@ -278,7 +278,7 @@ export class TrailRenderer extends Renderer { const newCapacity = this._currentPointCapacity + increaseCount; // Buffer layout: [capacity points] + [1 bridge point] // Bridge point is copy of point 0, placed at position capacity to connect wrap-around - const vertexCount = newCapacity * 2 + 2; // +2 vertices for bridge point + const vertexCount = (newCapacity + 1) * 2; // Create new vertex buffer (no index buffer needed - using drawArrays) const newVertexBuffer = new Buffer( @@ -326,14 +326,9 @@ export class TrailRenderer extends Renderer { this._vertices = newVertices; this._currentPointCapacity = newCapacity; - // Update primitive vertex buffer binding (no index buffer) - const primitive = this._primitive; + // Update primitive vertex buffer binding const vertexBufferBinding = new VertexBufferBinding(newVertexBuffer, byteStride); - if (primitive.vertexBufferBindings.length > 0) { - primitive.setVertexBufferBinding(0, vertexBufferBinding); - } else { - primitive.vertexBufferBindings.push(vertexBufferBinding); - } + this._primitive.setVertexBufferBinding(0, vertexBufferBinding); } /** From 7a2c617559ebcbed932dd577d7d31f80a5e9ef68 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 15:14:36 +0800 Subject: [PATCH 45/85] feat: update point stride constants for improved clarity in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 61 +++++++++++++----------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 720a2403e3..c0baaa16cf 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -24,6 +24,8 @@ import { TrailTextureMode } from "./enums/TrailTextureMode"; export class TrailRenderer extends Renderer { private static readonly VERTEX_STRIDE = 32; private static readonly VERTEX_FLOAT_STRIDE = 8; + private static readonly POINT_FLOAT_STRIDE = 16; // 2 vertices per point + private static readonly POINT_BYTE_STRIDE = 64; // 2 vertices per point private static readonly POINT_INCREASE_COUNT = 128; private static _timeParamsProp = ShaderProperty.getByName("renderer_TimeParams"); @@ -170,11 +172,11 @@ export class TrailRenderer extends Renderer { timeParams.x = this._playTime; const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _currentPointCapacity: capacity } = this; if (firstActive !== firstFree) { - const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; const vertices = this._vertices; - timeParams.z = vertices[firstActive * 2 * floatStride + 3]; + timeParams.z = vertices[firstActive * pointStride + 3]; const newestIndex = firstFree > 0 ? firstFree - 1 : capacity - 1; - timeParams.w = vertices[newestIndex * 2 * floatStride + 3]; + timeParams.w = vertices[newestIndex * pointStride + 3]; } else { timeParams.z = 0; timeParams.w = 0; @@ -272,23 +274,23 @@ export class TrailRenderer extends Renderer { private _resizeBuffer(increaseCount: number): void { const engine = this.engine; - const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; - const byteStride = TrailRenderer.VERTEX_STRIDE; + const pointFloatStride = TrailRenderer.POINT_FLOAT_STRIDE; + const pointByteStride = TrailRenderer.POINT_BYTE_STRIDE; const newCapacity = this._currentPointCapacity + increaseCount; // Buffer layout: [capacity points] + [1 bridge point] // Bridge point is copy of point 0, placed at position capacity to connect wrap-around - const vertexCount = (newCapacity + 1) * 2; + const pointCount = newCapacity + 1; // Create new vertex buffer (no index buffer needed - using drawArrays) const newVertexBuffer = new Buffer( engine, BufferBindFlag.VertexBuffer, - vertexCount * byteStride, + pointCount * pointByteStride, BufferUsage.Dynamic, false ); - const newVertices = new Float32Array(vertexCount * floatStride); + const newVertices = new Float32Array(pointCount * pointFloatStride); // Migrate existing vertex data if any const lastVertices = this._vertices; @@ -296,13 +298,13 @@ export class TrailRenderer extends Renderer { const firstFreeElement = this._firstFreeElement; // Copy data before firstFreeElement - newVertices.set(new Float32Array(lastVertices.buffer, 0, firstFreeElement * 2 * floatStride)); + newVertices.set(new Float32Array(lastVertices.buffer, 0, firstFreeElement * pointFloatStride)); // Copy data after firstFreeElement (shift by increaseCount) const nextFreeElement = firstFreeElement + 1; if (nextFreeElement < this._currentPointCapacity) { - const freeEndOffset = (nextFreeElement + increaseCount) * 2 * floatStride; - newVertices.set(new Float32Array(lastVertices.buffer, nextFreeElement * 2 * floatStride * 4), freeEndOffset); + const freeEndOffset = (nextFreeElement + increaseCount) * pointFloatStride; + newVertices.set(new Float32Array(lastVertices.buffer, nextFreeElement * pointFloatStride * 4), freeEndOffset); } // Update pointers @@ -327,7 +329,7 @@ export class TrailRenderer extends Renderer { this._currentPointCapacity = newCapacity; // Update primitive vertex buffer binding - const vertexBufferBinding = new VertexBufferBinding(newVertexBuffer, byteStride); + const vertexBufferBinding = new VertexBufferBinding(newVertexBuffer, TrailRenderer.VERTEX_STRIDE); this._primitive.setVertexBufferBinding(0, vertexBufferBinding); } @@ -338,10 +340,10 @@ export class TrailRenderer extends Renderer { private _retireActivePoints(frameCount: number): void { const { _playTime: currentTime, time: lifetime, _vertices: vertices, _currentPointCapacity: capacity } = this; const firstActiveOld = this._firstActiveElement; - const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; while (this._firstActiveElement !== this._firstFreeElement) { - const offset = this._firstActiveElement * 2 * floatStride + 3; + const offset = this._firstActiveElement * pointStride + 3; const birthTime = vertices[offset]; if (currentTime - birthTime < lifetime) break; // Record the frame when this point was retired (reuse birthTime field) @@ -364,11 +366,11 @@ export class TrailRenderer extends Renderer { */ private _freeRetiredPoints(frameCount: number): void { const capacity = this._currentPointCapacity; - const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; const vertices = this._vertices; while (this._firstRetiredElement !== this._firstActiveElement) { - const retireFrame = vertices[this._firstRetiredElement * 2 * floatStride + 3]; + const retireFrame = vertices[this._firstRetiredElement * pointStride + 3]; // WebGL doesn't support mapBufferRange, so this optimization is disabled. // When mapBufferRange is available, change condition to check if GPU finished rendering. @@ -404,6 +406,7 @@ export class TrailRenderer extends Renderer { private _addPoint(position: Vector3): void { const pointIndex = this._firstFreeElement; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; const vertices = this._vertices; const playTime = this._playTime; @@ -415,7 +418,7 @@ export class TrailRenderer extends Renderer { // First point has placeholder tangent, update it when second point is added if (this._getActivePointCount() === 1) { const firstPointIndex = this._firstActiveElement; - const offset0 = firstPointIndex * 2 * floatStride + 5; + const offset0 = firstPointIndex * pointStride + 5; const offset1 = offset0 + floatStride; tangent.copyToArray(vertices, offset0); tangent.copyToArray(vertices, offset1); @@ -428,7 +431,7 @@ export class TrailRenderer extends Renderer { } // Write vertex data for top vertex (corner = -1) - const topOffset = pointIndex * 2 * floatStride; + const topOffset = pointIndex * pointStride; position.copyToArray(vertices, topOffset); vertices[topOffset + 3] = playTime; vertices[topOffset + 4] = -1; @@ -443,7 +446,7 @@ export class TrailRenderer extends Renderer { // Also write to bridge position when writing point 0 if (pointIndex === 0) { - const bridgeTopOffset = this._currentPointCapacity * 2 * floatStride; + const bridgeTopOffset = this._currentPointCapacity * pointStride; const bridgeBottomOffset = bridgeTopOffset + floatStride; position.copyToArray(vertices, bridgeTopOffset); vertices[bridgeTopOffset + 3] = playTime; @@ -480,7 +483,7 @@ export class TrailRenderer extends Renderer { private _recalculateBounds(): void { const vertices = this._vertices; - const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; + const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; const activeCount = this._getActivePointCount(); const { min, max } = this._localBounds; @@ -491,14 +494,14 @@ export class TrailRenderer extends Renderer { return; } - const firstOffset = this._firstActiveElement * 2 * floatStride; + const firstOffset = this._firstActiveElement * pointStride; min.copyFromArray(vertices, firstOffset); max.copyFrom(min); const pointPosition = TrailRenderer._tempVector3; const capacity = this._currentPointCapacity; for (let i = 1, pointIndex = (this._firstActiveElement + 1) % capacity; i < activeCount; i++) { - pointPosition.copyFromArray(vertices, pointIndex * 2 * floatStride); + pointPosition.copyFromArray(vertices, pointIndex * pointStride); Vector3.min(min, pointPosition, min); Vector3.max(max, pointPosition, max); pointIndex = (pointIndex + 1) % capacity; @@ -514,8 +517,8 @@ export class TrailRenderer extends Renderer { if (firstNew === firstFree) return; - const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; - const byteStride = TrailRenderer.VERTEX_STRIDE; + const pointFloatStride = TrailRenderer.POINT_FLOAT_STRIDE; + const pointByteStride = TrailRenderer.POINT_BYTE_STRIDE; const { buffer: vertexData } = this._vertices; const capacity = this._currentPointCapacity; const wrapped = firstNew >= firstFree; @@ -523,20 +526,20 @@ export class TrailRenderer extends Renderer { // First segment: wrapped includes bridge (+1 point), non-wrapped ends at firstFree const endPoint = wrapped ? capacity + 1 : firstFree; buffer.setData( - new Float32Array(vertexData, firstNew * 2 * floatStride * 4, (endPoint - firstNew) * 2 * floatStride), - firstNew * 2 * byteStride + new Float32Array(vertexData, firstNew * pointFloatStride * 4, (endPoint - firstNew) * pointFloatStride), + firstNew * pointByteStride ); if (wrapped) { // Second segment if (firstFree > 0) { - buffer.setData(new Float32Array(vertexData, 0, firstFree * 2 * floatStride), 0); + buffer.setData(new Float32Array(vertexData, 0, firstFree * pointFloatStride), 0); } } else if (firstNew === 0) { // Upload bridge separately if point 0 was updated buffer.setData( - new Float32Array(vertexData, capacity * 2 * floatStride * 4, 2 * floatStride), - capacity * 2 * byteStride + new Float32Array(vertexData, capacity * pointFloatStride * 4, pointFloatStride), + capacity * pointByteStride ); } From c0d31f20ea5c0c0590924ced64fe9fd622d26cd9 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 15:39:54 +0800 Subject: [PATCH 46/85] fix: streamline vertex buffer creation and clarify comments in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index c0baaa16cf..f692ec44aa 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -282,7 +282,6 @@ export class TrailRenderer extends Renderer { // Bridge point is copy of point 0, placed at position capacity to connect wrap-around const pointCount = newCapacity + 1; - // Create new vertex buffer (no index buffer needed - using drawArrays) const newVertexBuffer = new Buffer( engine, BufferBindFlag.VertexBuffer, @@ -300,12 +299,10 @@ export class TrailRenderer extends Renderer { // Copy data before firstFreeElement newVertices.set(new Float32Array(lastVertices.buffer, 0, firstFreeElement * pointFloatStride)); - // Copy data after firstFreeElement (shift by increaseCount) + // Copy data after firstFreeElement (shift by increaseCount), including bridge point const nextFreeElement = firstFreeElement + 1; - if (nextFreeElement < this._currentPointCapacity) { - const freeEndOffset = (nextFreeElement + increaseCount) * pointFloatStride; - newVertices.set(new Float32Array(lastVertices.buffer, nextFreeElement * pointFloatStride * 4), freeEndOffset); - } + const freeEndOffset = (nextFreeElement + increaseCount) * pointFloatStride; + newVertices.set(new Float32Array(lastVertices.buffer, nextFreeElement * pointFloatStride * 4), freeEndOffset); // Update pointers if (this._firstNewElement > firstFreeElement) { From 864b9522db512d8e93a6208005775263b77104a8 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 15:44:05 +0800 Subject: [PATCH 47/85] refactor: simplify comments and streamline vertex data migration in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 26 +++++++----------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index f692ec44aa..a6e3471f15 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -291,41 +291,30 @@ export class TrailRenderer extends Renderer { ); const newVertices = new Float32Array(pointCount * pointFloatStride); - // Migrate existing vertex data if any + // Migrate existing vertex data const lastVertices = this._vertices; if (lastVertices) { const firstFreeElement = this._firstFreeElement; - - // Copy data before firstFreeElement newVertices.set(new Float32Array(lastVertices.buffer, 0, firstFreeElement * pointFloatStride)); - // Copy data after firstFreeElement (shift by increaseCount), including bridge point + // Shift data after firstFreeElement by increaseCount (includes bridge point) const nextFreeElement = firstFreeElement + 1; const freeEndOffset = (nextFreeElement + increaseCount) * pointFloatStride; newVertices.set(new Float32Array(lastVertices.buffer, nextFreeElement * pointFloatStride * 4), freeEndOffset); - // Update pointers - if (this._firstNewElement > firstFreeElement) { - this._firstNewElement += increaseCount; - } - if (this._firstActiveElement > firstFreeElement) { - this._firstActiveElement += increaseCount; - } - if (this._firstRetiredElement > firstFreeElement) { - this._firstRetiredElement += increaseCount; - } + if (this._firstNewElement > firstFreeElement) this._firstNewElement += increaseCount; + if (this._firstActiveElement > firstFreeElement) this._firstActiveElement += increaseCount; + if (this._firstRetiredElement > firstFreeElement) this._firstRetiredElement += increaseCount; this._bufferResized = true; } - // Destroy old vertex buffer this._vertexBuffer?.destroy(); this._vertexBuffer = newVertexBuffer; this._vertices = newVertices; this._currentPointCapacity = newCapacity; - // Update primitive vertex buffer binding const vertexBufferBinding = new VertexBufferBinding(newVertexBuffer, TrailRenderer.VERTEX_STRIDE); this._primitive.setVertexBufferBinding(0, vertexBufferBinding); } @@ -427,21 +416,20 @@ export class TrailRenderer extends Renderer { tangent.set(0, 0, 1); } - // Write vertex data for top vertex (corner = -1) + // Write top vertex (corner = -1) and bottom vertex (corner = 1) const topOffset = pointIndex * pointStride; position.copyToArray(vertices, topOffset); vertices[topOffset + 3] = playTime; vertices[topOffset + 4] = -1; tangent.copyToArray(vertices, topOffset + 5); - // Write vertex data for bottom vertex (corner = 1) const bottomOffset = topOffset + floatStride; position.copyToArray(vertices, bottomOffset); vertices[bottomOffset + 3] = playTime; vertices[bottomOffset + 4] = 1; tangent.copyToArray(vertices, bottomOffset + 5); - // Also write to bridge position when writing point 0 + // Write to bridge position when writing point 0 if (pointIndex === 0) { const bridgeTopOffset = this._currentPointCapacity * pointStride; const bridgeBottomOffset = bridgeTopOffset + floatStride; From 3151a42f3eb6f67e3552864ed1932d7baf013ebb Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 16:09:09 +0800 Subject: [PATCH 48/85] refactor: rename _tryAddNewPoint to _emitNewPoint for clarity in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index a6e3471f15..bea6e5b844 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -162,7 +162,7 @@ export class TrailRenderer extends Renderer { this._retireActivePoints(time.frameCount); if (this.emitting) { - this._tryAddNewPoint(); + this._emitNewPoint(); } const shaderData = this.shaderData; @@ -189,15 +189,14 @@ export class TrailRenderer extends Renderer { shaderData.setFloatArray(TrailRenderer._widthCurveProp, this.widthCurve._getTypeArray()); shaderData.setFloatArray(TrailRenderer._colorKeysProp, colorGradient._getColorTypeArray()); shaderData.setFloatArray(TrailRenderer._alphaKeysProp, colorGradient._getAlphaTypeArray()); - } - - protected override _render(context: RenderContext): void { - // Need at least 2 points to form a trail segment - if (this._getActivePointCount() < 2) return; if (this._firstNewElement !== this._firstFreeElement || this._vertexBuffer.isContentLost) { this._uploadNewVertices(); } + } + + protected override _render(context: RenderContext): void { + if (this._getActivePointCount() < 2) return; const material = this.getMaterial(); if (!material || material.destroyed || material.shader.destroyed) return; @@ -368,7 +367,7 @@ export class TrailRenderer extends Renderer { } } - private _tryAddNewPoint(): void { + private _emitNewPoint(): void { const worldPosition = this.entity.transform.worldPosition; if (this._hasLastPosition && Vector3.distance(worldPosition, this._lastPosition) < this.minVertexDistance) { From 23dbb364eb0b33c2d305e637574cea03d24a3d1f Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 16:24:29 +0800 Subject: [PATCH 49/85] fix: optimize time parameter updates and streamline vertex upload logic in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 27 +++++++++--------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index bea6e5b844..68a856bbc0 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -164,23 +164,13 @@ export class TrailRenderer extends Renderer { if (this.emitting) { this._emitNewPoint(); } + if (this._firstNewElement !== this._firstFreeElement || this._vertexBuffer.isContentLost) { + this._uploadNewVertices(); + } const shaderData = this.shaderData; const timeParams = this._timeParams; - - // Update dynamic time params (currentTime, oldestBirthTime, newestBirthTime) timeParams.x = this._playTime; - const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _currentPointCapacity: capacity } = this; - if (firstActive !== firstFree) { - const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; - const vertices = this._vertices; - timeParams.z = vertices[firstActive * pointStride + 3]; - const newestIndex = firstFree > 0 ? firstFree - 1 : capacity - 1; - timeParams.w = vertices[newestIndex * pointStride + 3]; - } else { - timeParams.z = 0; - timeParams.w = 0; - } shaderData.setVector4(TrailRenderer._timeParamsProp, timeParams); shaderData.setVector4(TrailRenderer._trailParamsProp, this._trailParams); @@ -189,10 +179,6 @@ export class TrailRenderer extends Renderer { shaderData.setFloatArray(TrailRenderer._widthCurveProp, this.widthCurve._getTypeArray()); shaderData.setFloatArray(TrailRenderer._colorKeysProp, colorGradient._getColorTypeArray()); shaderData.setFloatArray(TrailRenderer._alphaKeysProp, colorGradient._getAlphaTypeArray()); - - if (this._firstNewElement !== this._firstFreeElement || this._vertexBuffer.isContentLost) { - this._uploadNewVertices(); - } } protected override _render(context: RenderContext): void { @@ -338,9 +324,15 @@ export class TrailRenderer extends Renderer { if (this._firstActiveElement !== firstActiveOld) { this._boundsDirty = true; + // Update oldest birth time + if (this._firstActiveElement !== this._firstFreeElement) { + this._timeParams.z = vertices[this._firstActiveElement * pointStride + 3]; + } } if (this._firstActiveElement === this._firstFreeElement) { this._hasLastPosition = false; + this._timeParams.z = 0; + this._timeParams.w = 0; } } @@ -444,6 +436,7 @@ export class TrailRenderer extends Renderer { this._expandBounds(position); this._firstFreeElement = (this._firstFreeElement + 1) % this._currentPointCapacity; + this._timeParams.w = playTime; } private _getActivePointCount(): number { From 75285334173e2db9f2151e3c3bb27068e2c84d02 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 30 Dec 2025 16:45:13 +0800 Subject: [PATCH 50/85] refactor: improve rendering logic and encapsulate sub-render element creation in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 52 +++++++++++++----------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 68a856bbc0..ebf36486ed 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -1,6 +1,8 @@ import { BoundingBox, Color, Vector3, Vector4 } from "@galacean/engine-math"; import { Entity } from "../Entity"; +import { Material } from "../material/Material"; import { RenderContext } from "../RenderPipeline/RenderContext"; +import { RenderElement } from "../RenderPipeline/RenderElement"; import { Renderer } from "../Renderer"; import { deepClone, ignoreClone } from "../clone/CloneManager"; import { Buffer } from "../graphic/Buffer"; @@ -182,36 +184,26 @@ export class TrailRenderer extends Renderer { } protected override _render(context: RenderContext): void { - if (this._getActivePointCount() < 2) return; + if (this._getActivePointCount() < 2) { + return; + } const material = this.getMaterial(); - if (!material || material.destroyed || material.shader.destroyed) return; + if (!material || material.destroyed || material.shader.destroyed) { + return; + } - const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _primitive: primitive } = this; - const { _renderElementPool: renderElementPool, _subRenderElementPool: subRenderElementPool } = this._engine; + const { _firstActiveElement: firstActive, _firstFreeElement: firstFree } = this; - const renderElement = renderElementPool.get(); + const renderElement = this._engine._renderElementPool.get(); renderElement.set(this.priority, this._distanceForSort); - // Main segment (always rendered) - const mainSubPrimitive = this._mainSubPrimitive; - mainSubPrimitive.start = firstActive * 2; - mainSubPrimitive.count = - firstActive >= firstFree - ? (this._currentPointCapacity - firstActive + 1) * 2 // Wrapped: includes bridge - : (firstFree - firstActive) * 2; - const subRenderElement = subRenderElementPool.get(); - subRenderElement.set(this, material, primitive, mainSubPrimitive); - renderElement.addSubRenderElement(subRenderElement); + const wrapped = firstActive > firstFree; + const mainCount = (wrapped ? this._currentPointCapacity - firstActive + 1 : firstFree - firstActive) * 2; + this._addSubRenderElement(renderElement, material, this._mainSubPrimitive, firstActive * 2, mainCount); - // Wrap segment (only when buffer wraps around) - if (firstActive >= firstFree && firstFree > 0) { - const wrapSubPrimitive = this._wrapSubPrimitive; - wrapSubPrimitive.start = 0; - wrapSubPrimitive.count = firstFree * 2; - const subRenderElement2 = subRenderElementPool.get(); - subRenderElement2.set(this, material, primitive, wrapSubPrimitive); - renderElement.addSubRenderElement(subRenderElement2); + if (wrapped && firstFree > 0) { + this._addSubRenderElement(renderElement, material, this._wrapSubPrimitive, 0, firstFree * 2); } context.camera._renderPipeline.pushRenderElement(context, renderElement); @@ -522,4 +514,18 @@ export class TrailRenderer extends Renderer { this._firstNewElement = firstFree; } + + private _addSubRenderElement( + renderElement: RenderElement, + material: Material, + subPrimitive: SubPrimitive, + start: number, + count: number + ): void { + subPrimitive.start = start; + subPrimitive.count = count; + const subRenderElement = this._engine._subRenderElementPool.get(); + subRenderElement.set(this, material, this._primitive, subPrimitive); + renderElement.addSubRenderElement(subRenderElement); + } } From 557c533d2142a2b5febad27d3bc55ef8202b4262 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 02:49:19 +0800 Subject: [PATCH 51/85] feat: add segment bounds management for improved trail rendering efficiency --- packages/core/src/trail/TrailRenderer.ts | 195 ++++++++++++++++++----- 1 file changed, 153 insertions(+), 42 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index ebf36486ed..eecd3db4f0 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -30,6 +30,11 @@ export class TrailRenderer extends Renderer { private static readonly POINT_BYTE_STRIDE = 64; // 2 vertices per point private static readonly POINT_INCREASE_COUNT = 128; + // Segment bounds array layout: posX, posY, posZ, birthTime = 4 floats per segment + private static readonly SEGMENT_BOUNDS_FLOAT_STRIDE = 4; + private static readonly SEGMENT_BOUNDS_TIME_OFFSET = 3; + private static readonly SEGMENT_BOUNDS_INCREASE_COUNT = 64; + private static _timeParamsProp = ShaderProperty.getByName("renderer_TimeParams"); private static _trailParamsProp = ShaderProperty.getByName("renderer_TrailParams"); private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); @@ -130,10 +135,17 @@ export class TrailRenderer extends Renderer { private _hasLastPosition = false; @ignoreClone private _playTime = 0; + // Segment bounds for efficient bounds calculation (similar to particle world mode) + @ignoreClone + private _segmentBoundsArray: Float32Array; + @ignoreClone + private _segmentBoundsCount = 0; @ignoreClone - private _localBounds = new BoundingBox(); + private _firstActiveSegmentBounds = 0; @ignoreClone - private _boundsDirty = true; + private _firstFreeSegmentBounds = 0; + @ignoreClone + private _hasPendingSegmentBounds = false; /** * @internal @@ -152,7 +164,8 @@ export class TrailRenderer extends Renderer { this._firstFreeElement = 0; this._firstRetiredElement = 0; this._hasLastPosition = false; - this._boundsDirty = true; + this._firstActiveSegmentBounds = this._firstFreeSegmentBounds; + this._hasPendingSegmentBounds = false; } protected override _update(context: RenderContext): void { @@ -162,6 +175,7 @@ export class TrailRenderer extends Renderer { this._playTime += time.deltaTime; this._freeRetiredPoints(time.frameCount); this._retireActivePoints(time.frameCount); + this._retireSegmentBounds(); if (this.emitting) { this._emitNewPoint(); @@ -210,22 +224,60 @@ export class TrailRenderer extends Renderer { } protected override _updateBounds(worldBounds: BoundingBox): void { + // Pre-generate or update pending segment bounds for current position (same condition as _emitNewPoint) + if (this.emitting) { + const worldPosition = this.entity.transform.worldPosition; + if (!this._hasLastPosition || Vector3.distance(worldPosition, this._lastPosition) >= this.minVertexDistance) { + this._generateSegmentBounds(worldPosition, false); + } + } + const halfWidth = this.width * 0.5; + const firstActive = this._firstActiveSegmentBounds; + const firstFree = this._firstFreeSegmentBounds; + const hasPending = this._hasPendingSegmentBounds; - if (this._firstActiveElement === this._firstFreeElement) { + // No active segment bounds - use entity position as fallback + if (firstActive === firstFree && !hasPending) { const worldPosition = this.entity.transform.worldPosition; worldBounds.min.set(worldPosition.x - halfWidth, worldPosition.y - halfWidth, worldPosition.z - halfWidth); worldBounds.max.set(worldPosition.x + halfWidth, worldPosition.y + halfWidth, worldPosition.z + halfWidth); return; } - if (this._boundsDirty) { - this._recalculateBounds(); + // Merge all segment bounds + const boundsArray = this._segmentBoundsArray; + const floatStride = TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; + const count = this._segmentBoundsCount; + + // Initialize with first active segment bounds + const firstOffset = firstActive * floatStride; + worldBounds.min.copyFromArray(boundsArray, firstOffset); + worldBounds.max.copyFromArray(boundsArray, firstOffset); + + // Merge remaining confirmed segment bounds + if (firstActive < firstFree) { + for (let i = firstActive + 1; i < firstFree; i++) { + this._mergeSegmentBounds(i, worldBounds); + } + } else if (firstActive > firstFree) { + for (let i = firstActive + 1; i < count; i++) { + this._mergeSegmentBounds(i, worldBounds); + } + for (let i = 0; i < firstFree; i++) { + this._mergeSegmentBounds(i, worldBounds); + } + } + + // Include pending segment bounds (skip if already used for initialization) + if (hasPending && firstActive !== firstFree) { + this._mergeSegmentBounds(firstFree, worldBounds); } - const { min, max } = this._localBounds; - worldBounds.min.set(min.x - halfWidth, min.y - halfWidth, min.z - halfWidth); - worldBounds.max.set(max.x + halfWidth, max.y + halfWidth, max.z + halfWidth); + // Expand by half width for trail thickness + const { min, max } = worldBounds; + min.set(min.x - halfWidth, min.y - halfWidth, min.z - halfWidth); + max.set(max.x + halfWidth, max.y + halfWidth, max.z + halfWidth); } protected override _onDestroy(): void { @@ -315,7 +367,6 @@ export class TrailRenderer extends Renderer { } if (this._firstActiveElement !== firstActiveOld) { - this._boundsDirty = true; // Update oldest birth time if (this._firstActiveElement !== this._firstFreeElement) { this._timeParams.z = vertices[this._firstActiveElement * pointStride + 3]; @@ -367,6 +418,9 @@ export class TrailRenderer extends Renderer { this._resizeBuffer(TrailRenderer.POINT_INCREASE_COUNT); } + // Confirm segment bounds in sync with trail points + this._generateSegmentBounds(worldPosition, true); + this._addPoint(worldPosition); this._lastPosition.copyFrom(worldPosition); this._hasLastPosition = true; @@ -426,7 +480,6 @@ export class TrailRenderer extends Renderer { tangent.copyToArray(vertices, bridgeBottomOffset + 5); } - this._expandBounds(position); this._firstFreeElement = (this._firstFreeElement + 1) % this._currentPointCapacity; this._timeParams.w = playTime; } @@ -436,47 +489,105 @@ export class TrailRenderer extends Renderer { return firstFree >= firstActive ? firstFree - firstActive : this._currentPointCapacity - firstActive + firstFree; } - private _expandBounds(position: Vector3): void { - const { min, max } = this._localBounds; + /** + * Generate or update segment bounds for the given position. + * @param position - The world position to record + * @param confirm - If true, confirm the segment bounds and advance the pointer (called when emitting point). + * If false, just update the pending segment bounds (can be overwritten multiple times per frame). + */ + private _generateSegmentBounds(position: Vector3, confirm: boolean): void { + const floatStride = TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; + const firstFree = this._firstFreeSegmentBounds; + const count = this._segmentBoundsCount; + + // Only check resize when confirming a new segment (not for pending updates) + if (confirm && !this._hasPendingSegmentBounds) { + let nextFree = firstFree + 1; + if (nextFree >= count) { + nextFree = 0; + } + if (nextFree === this._firstActiveSegmentBounds) { + this._resizeSegmentBoundsArray(); + } + } + + // Write segment bounds data at firstFree position + const offset = firstFree * floatStride; + const boundsArray = this._segmentBoundsArray; + position.copyToArray(boundsArray, offset); + boundsArray[offset + 3] = this._playTime; + + if (confirm) { + // Advance pointer to confirm this segment bounds + let nextFree = firstFree + 1; + if (nextFree >= count) { + nextFree = 0; + } + this._firstFreeSegmentBounds = nextFree; + this._hasPendingSegmentBounds = false; + } else { + // Mark as pending (can be overwritten or confirmed later) + this._hasPendingSegmentBounds = true; + } + } - if (this._boundsDirty) { - min.copyFrom(position); - max.copyFrom(position); - this._boundsDirty = false; - return; + /** + * Retire segment bounds that have expired based on lifetime. + */ + private _retireSegmentBounds(): void { + const floatStride = TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; + const timeOffset = TrailRenderer.SEGMENT_BOUNDS_TIME_OFFSET; + const boundsArray = this._segmentBoundsArray; + const firstFree = this._firstFreeSegmentBounds; + const count = this._segmentBoundsCount; + const lifetime = this.time; + const currentTime = this._playTime; + + while (this._firstActiveSegmentBounds !== firstFree) { + const index = this._firstActiveSegmentBounds * floatStride; + const age = currentTime - boundsArray[index + timeOffset]; + if (age <= lifetime) { + break; + } + if (++this._firstActiveSegmentBounds >= count) { + this._firstActiveSegmentBounds = 0; + } } + } - Vector3.min(min, position, min); - Vector3.max(max, position, max); + private _mergeSegmentBounds(index: number, bounds: BoundingBox): void { + const boundsArray = this._segmentBoundsArray; + const offset = index * TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; + const x = boundsArray[offset]; + const y = boundsArray[offset + 1]; + const z = boundsArray[offset + 2]; + const { min, max } = bounds; + min.set(Math.min(min.x, x), Math.min(min.y, y), Math.min(min.z, z)); + max.set(Math.max(max.x, x), Math.max(max.y, y), Math.max(max.z, z)); } - private _recalculateBounds(): void { - const vertices = this._vertices; - const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; - const activeCount = this._getActivePointCount(); - const { min, max } = this._localBounds; + private _resizeSegmentBoundsArray(): void { + const floatStride = TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; + const increaseCount = TrailRenderer.SEGMENT_BOUNDS_INCREASE_COUNT; - if (activeCount === 0) { - min.set(0, 0, 0); - max.set(0, 0, 0); - this._boundsDirty = false; - return; - } + this._segmentBoundsCount += increaseCount; + const lastBoundsArray = this._segmentBoundsArray; + const boundsArray = new Float32Array(this._segmentBoundsCount * floatStride); - const firstOffset = this._firstActiveElement * pointStride; - min.copyFromArray(vertices, firstOffset); - max.copyFrom(min); + if (lastBoundsArray) { + const firstFree = this._firstFreeSegmentBounds; + boundsArray.set(new Float32Array(lastBoundsArray.buffer, 0, firstFree * floatStride)); - const pointPosition = TrailRenderer._tempVector3; - const capacity = this._currentPointCapacity; - for (let i = 1, pointIndex = (this._firstActiveElement + 1) % capacity; i < activeCount; i++) { - pointPosition.copyFromArray(vertices, pointIndex * pointStride); - Vector3.min(min, pointPosition, min); - Vector3.max(max, pointPosition, max); - pointIndex = (pointIndex + 1) % capacity; + const nextFree = firstFree + 1; + const freeEndOffset = (nextFree + increaseCount) * floatStride; + boundsArray.set(new Float32Array(lastBoundsArray.buffer, nextFree * floatStride * 4), freeEndOffset); + + if (this._firstActiveSegmentBounds > firstFree) { + this._firstActiveSegmentBounds += increaseCount; + } } - this._boundsDirty = false; + this._segmentBoundsArray = boundsArray; } private _uploadNewVertices(): void { From 1dea411752f1251240014693ace74bb89b352872 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 03:22:31 +0800 Subject: [PATCH 52/85] refactor: update segment bounds management for improved frame handling in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 56 +++++++++++------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index eecd3db4f0..24ce0301ea 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -145,7 +145,7 @@ export class TrailRenderer extends Renderer { @ignoreClone private _firstFreeSegmentBounds = 0; @ignoreClone - private _hasPendingSegmentBounds = false; + private _lastSegmentBoundsFrameCount = -1; /** * @internal @@ -165,7 +165,7 @@ export class TrailRenderer extends Renderer { this._firstRetiredElement = 0; this._hasLastPosition = false; this._firstActiveSegmentBounds = this._firstFreeSegmentBounds; - this._hasPendingSegmentBounds = false; + this._lastSegmentBoundsFrameCount = -1; } protected override _update(context: RenderContext): void { @@ -224,21 +224,20 @@ export class TrailRenderer extends Renderer { } protected override _updateBounds(worldBounds: BoundingBox): void { - // Pre-generate or update pending segment bounds for current position (same condition as _emitNewPoint) + // Generate segment bounds for current position (same condition as _emitNewPoint) if (this.emitting) { const worldPosition = this.entity.transform.worldPosition; if (!this._hasLastPosition || Vector3.distance(worldPosition, this._lastPosition) >= this.minVertexDistance) { - this._generateSegmentBounds(worldPosition, false); + this._generateSegmentBounds(worldPosition); } } const halfWidth = this.width * 0.5; const firstActive = this._firstActiveSegmentBounds; const firstFree = this._firstFreeSegmentBounds; - const hasPending = this._hasPendingSegmentBounds; // No active segment bounds - use entity position as fallback - if (firstActive === firstFree && !hasPending) { + if (firstActive === firstFree) { const worldPosition = this.entity.transform.worldPosition; worldBounds.min.set(worldPosition.x - halfWidth, worldPosition.y - halfWidth, worldPosition.z - halfWidth); worldBounds.max.set(worldPosition.x + halfWidth, worldPosition.y + halfWidth, worldPosition.z + halfWidth); @@ -247,15 +246,14 @@ export class TrailRenderer extends Renderer { // Merge all segment bounds const boundsArray = this._segmentBoundsArray; - const floatStride = TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; const count = this._segmentBoundsCount; // Initialize with first active segment bounds - const firstOffset = firstActive * floatStride; + const firstOffset = firstActive * TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; worldBounds.min.copyFromArray(boundsArray, firstOffset); worldBounds.max.copyFromArray(boundsArray, firstOffset); - // Merge remaining confirmed segment bounds + // Merge remaining segment bounds if (firstActive < firstFree) { for (let i = firstActive + 1; i < firstFree; i++) { this._mergeSegmentBounds(i, worldBounds); @@ -269,11 +267,6 @@ export class TrailRenderer extends Renderer { } } - // Include pending segment bounds (skip if already used for initialization) - if (hasPending && firstActive !== firstFree) { - this._mergeSegmentBounds(firstFree, worldBounds); - } - // Expand by half width for trail thickness const { min, max } = worldBounds; min.set(min.x - halfWidth, min.y - halfWidth, min.z - halfWidth); @@ -418,8 +411,8 @@ export class TrailRenderer extends Renderer { this._resizeBuffer(TrailRenderer.POINT_INCREASE_COUNT); } - // Confirm segment bounds in sync with trail points - this._generateSegmentBounds(worldPosition, true); + // Generate segment bounds in sync with trail points + this._generateSegmentBounds(worldPosition); this._addPoint(worldPosition); this._lastPosition.copyFrom(worldPosition); @@ -491,17 +484,23 @@ export class TrailRenderer extends Renderer { /** * Generate or update segment bounds for the given position. - * @param position - The world position to record - * @param confirm - If true, confirm the segment bounds and advance the pointer (called when emitting point). - * If false, just update the pending segment bounds (can be overwritten multiple times per frame). + * Uses frame count to determine whether to add new or overwrite existing. */ - private _generateSegmentBounds(position: Vector3, confirm: boolean): void { + private _generateSegmentBounds(position: Vector3): void { + const frameCount = this.engine.time.frameCount; + const isSameFrame = frameCount === this._lastSegmentBoundsFrameCount; const floatStride = TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; - const firstFree = this._firstFreeSegmentBounds; const count = this._segmentBoundsCount; + let firstFree = this._firstFreeSegmentBounds; - // Only check resize when confirming a new segment (not for pending updates) - if (confirm && !this._hasPendingSegmentBounds) { + if (isSameFrame) { + // Same frame: overwrite previous position (go back one slot) + firstFree = firstFree - 1; + if (firstFree < 0) { + firstFree = count - 1; + } + } else { + // New frame: check resize before adding new segment let nextFree = firstFree + 1; if (nextFree >= count) { nextFree = 0; @@ -511,23 +510,20 @@ export class TrailRenderer extends Renderer { } } - // Write segment bounds data at firstFree position + // Write segment bounds data const offset = firstFree * floatStride; const boundsArray = this._segmentBoundsArray; position.copyToArray(boundsArray, offset); boundsArray[offset + 3] = this._playTime; - if (confirm) { - // Advance pointer to confirm this segment bounds + if (!isSameFrame) { + // Advance pointer for new segment let nextFree = firstFree + 1; if (nextFree >= count) { nextFree = 0; } this._firstFreeSegmentBounds = nextFree; - this._hasPendingSegmentBounds = false; - } else { - // Mark as pending (can be overwritten or confirmed later) - this._hasPendingSegmentBounds = true; + this._lastSegmentBoundsFrameCount = frameCount; } } From fe30e39e1cc13cdc46219296a6d08422314eb161 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 03:27:20 +0800 Subject: [PATCH 53/85] refactor: streamline segment bounds generation logic in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 34 +++++++++--------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 24ce0301ea..5b817ed6b7 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -1,6 +1,5 @@ import { BoundingBox, Color, Vector3, Vector4 } from "@galacean/engine-math"; import { Entity } from "../Entity"; -import { Material } from "../material/Material"; import { RenderContext } from "../RenderPipeline/RenderContext"; import { RenderElement } from "../RenderPipeline/RenderElement"; import { Renderer } from "../Renderer"; @@ -14,6 +13,7 @@ import { BufferBindFlag } from "../graphic/enums/BufferBindFlag"; import { BufferUsage } from "../graphic/enums/BufferUsage"; import { MeshTopology } from "../graphic/enums/MeshTopology"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; +import { Material } from "../material/Material"; import { CurveKey, ParticleCurve } from "../particle/modules/ParticleCurve"; import { GradientAlphaKey, GradientColorKey, ParticleGradient } from "../particle/modules/ParticleGradient"; import { ShaderProperty } from "../shader/ShaderProperty"; @@ -489,42 +489,34 @@ export class TrailRenderer extends Renderer { private _generateSegmentBounds(position: Vector3): void { const frameCount = this.engine.time.frameCount; const isSameFrame = frameCount === this._lastSegmentBoundsFrameCount; - const floatStride = TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; - const count = this._segmentBoundsCount; - let firstFree = this._firstFreeSegmentBounds; + let writeIndex: number; if (isSameFrame) { // Same frame: overwrite previous position (go back one slot) - firstFree = firstFree - 1; - if (firstFree < 0) { - firstFree = count - 1; + writeIndex = this._firstFreeSegmentBounds - 1; + if (writeIndex < 0) { + writeIndex = this._segmentBoundsCount - 1; } } else { - // New frame: check resize before adding new segment - let nextFree = firstFree + 1; - if (nextFree >= count) { + // New frame: check resize and advance pointer + writeIndex = this._firstFreeSegmentBounds; + let nextFree = writeIndex + 1; + if (nextFree >= this._segmentBoundsCount) { nextFree = 0; } if (nextFree === this._firstActiveSegmentBounds) { this._resizeSegmentBoundsArray(); + nextFree = writeIndex + 1; // Recalculate after resize (count increased, no wrap needed) } + this._firstFreeSegmentBounds = nextFree; + this._lastSegmentBoundsFrameCount = frameCount; } // Write segment bounds data - const offset = firstFree * floatStride; + const offset = writeIndex * TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; const boundsArray = this._segmentBoundsArray; position.copyToArray(boundsArray, offset); boundsArray[offset + 3] = this._playTime; - - if (!isSameFrame) { - // Advance pointer for new segment - let nextFree = firstFree + 1; - if (nextFree >= count) { - nextFree = 0; - } - this._firstFreeSegmentBounds = nextFree; - this._lastSegmentBoundsFrameCount = frameCount; - } } /** From f6cfd791e8bba9d2d5135c3a34ce6cc967c330eb Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 03:54:54 +0800 Subject: [PATCH 54/85] fix: update oldest birth time handling in time parameters for TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 5b817ed6b7..3d1b04a066 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -46,7 +46,7 @@ export class TrailRenderer extends Renderer { /** The minimum distance the object must move before a new trail segment is added. */ minVertexDistance = 0.1; - private _timeParams = new Vector4(0, 5.0, 0, 0); // x: currentTime, y: lifetime, z: oldestBirthTime, w: newestBirthTime + private _timeParams = new Vector4(0, 5.0, -1, 0); // x: currentTime, y: lifetime, z: oldestBirthTime, w: newestBirthTime private _trailParams = new Vector4(1.0, TrailTextureMode.Stretch, 1.0, 0); // x: width, y: textureMode, z: textureScale /** @@ -367,7 +367,7 @@ export class TrailRenderer extends Renderer { } if (this._firstActiveElement === this._firstFreeElement) { this._hasLastPosition = false; - this._timeParams.z = 0; + this._timeParams.z = -1; this._timeParams.w = 0; } } @@ -474,7 +474,13 @@ export class TrailRenderer extends Renderer { } this._firstFreeElement = (this._firstFreeElement + 1) % this._currentPointCapacity; - this._timeParams.w = playTime; + + // Update time params + const timeParams = this._timeParams; + if (timeParams.z === -1) { + timeParams.z = playTime; // First point: set oldest birth time + } + timeParams.w = playTime; // Always update newest birth time } private _getActivePointCount(): number { From 9e10b9c58b3358f3095e58b2c57fdd8f71733002 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 14:56:30 +0800 Subject: [PATCH 55/85] refactor: reorganize shader parameters and improve point management in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 155 +++++++++++++---------- 1 file changed, 89 insertions(+), 66 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 3d1b04a066..59c53ca3b1 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -46,53 +46,6 @@ export class TrailRenderer extends Renderer { /** The minimum distance the object must move before a new trail segment is added. */ minVertexDistance = 0.1; - private _timeParams = new Vector4(0, 5.0, -1, 0); // x: currentTime, y: lifetime, z: oldestBirthTime, w: newestBirthTime - private _trailParams = new Vector4(1.0, TrailTextureMode.Stretch, 1.0, 0); // x: width, y: textureMode, z: textureScale - - /** - * The fade-out duration in seconds. - */ - get time(): number { - return this._timeParams.y; - } - - set time(value: number) { - this._timeParams.y = value; - } - - /** - * The width of the trail. - */ - get width(): number { - return this._trailParams.x; - } - - set width(value: number) { - this._trailParams.x = value; - } - - /** - * The texture mapping mode for the trail. - */ - get textureMode(): TrailTextureMode { - return this._trailParams.y; - } - - 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; - } - /** The curve describing the trail width from start to end. */ @deepClone widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 1)); @@ -107,6 +60,13 @@ export class TrailRenderer extends Renderer { /** Whether the trail is being created as the object moves. */ emitting = true; + // Shader parameters + @ignoreClone + private _timeParams = new Vector4(0, 5.0, -1, 0); // x: currentTime, y: lifetime, z: oldestBirthTime, w: newestBirthTime + @ignoreClone + private _trailParams = new Vector4(1.0, TrailTextureMode.Stretch, 1.0, 0); // x: width, y: textureMode, z: textureScale + + // Geometry and rendering @ignoreClone private _primitive: Primitive; @ignoreClone @@ -117,6 +77,8 @@ export class TrailRenderer extends Renderer { private _vertexBuffer: Buffer; @ignoreClone private _vertices: Float32Array; + + // Point management (circular buffer state) @ignoreClone private _firstActiveElement = 0; @ignoreClone @@ -129,13 +91,20 @@ export class TrailRenderer extends Renderer { private _currentPointCapacity = 0; @ignoreClone private _bufferResized = false; + + // Position tracking @ignoreClone private _lastPosition = new Vector3(); @ignoreClone private _hasLastPosition = false; + + // Time tracking @ignoreClone private _playTime = 0; - // Segment bounds for efficient bounds calculation (similar to particle world mode) + @ignoreClone + private _lastPlayTimeUpdateFrameCount = -1; + + // Segment bounds (for efficient bounds calculation) @ignoreClone private _segmentBoundsArray: Float32Array; @ignoreClone @@ -147,6 +116,50 @@ export class TrailRenderer extends Renderer { @ignoreClone private _lastSegmentBoundsFrameCount = -1; + /** + * The fade-out duration in seconds. + */ + get time(): number { + return this._timeParams.y; + } + + set time(value: number) { + this._timeParams.y = value; + } + + /** + * The width of the trail. + */ + get width(): number { + return this._trailParams.x; + } + + set width(value: number) { + this._trailParams.x = value; + } + + /** + * The texture mapping mode for the trail. + */ + get textureMode(): TrailTextureMode { + return this._trailParams.y; + } + + 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; + } + /** * @internal */ @@ -172,13 +185,15 @@ export class TrailRenderer extends Renderer { super._update(context); const time = this.engine.time; - this._playTime += time.deltaTime; - this._freeRetiredPoints(time.frameCount); - this._retireActivePoints(time.frameCount); - this._retireSegmentBounds(); + const playTime = this._updateAndGetPlayTime(); + const frameCount = time.frameCount; + + this._freeRetiredPoints(frameCount); + this._retireActivePoints(playTime, frameCount); + this._retireSegmentBounds(playTime); if (this.emitting) { - this._emitNewPoint(); + this._emitNewPoint(playTime); } if (this._firstNewElement !== this._firstFreeElement || this._vertexBuffer.isContentLost) { this._uploadNewVertices(); @@ -186,7 +201,7 @@ export class TrailRenderer extends Renderer { const shaderData = this.shaderData; const timeParams = this._timeParams; - timeParams.x = this._playTime; + timeParams.x = playTime; shaderData.setVector4(TrailRenderer._timeParamsProp, timeParams); shaderData.setVector4(TrailRenderer._trailParamsProp, this._trailParams); @@ -228,7 +243,7 @@ export class TrailRenderer extends Renderer { if (this.emitting) { const worldPosition = this.entity.transform.worldPosition; if (!this._hasLastPosition || Vector3.distance(worldPosition, this._lastPosition) >= this.minVertexDistance) { - this._generateSegmentBounds(worldPosition); + this._generateSegmentBounds(worldPosition, this._updateAndGetPlayTime()); } } @@ -279,6 +294,16 @@ export class TrailRenderer extends Renderer { this._primitive?.destroy(); } + private _updateAndGetPlayTime(): number { + const time = this.engine.time; + const frameCount = time.frameCount; + if (frameCount !== this._lastPlayTimeUpdateFrameCount) { + this._playTime += time.deltaTime; + this._lastPlayTimeUpdateFrameCount = frameCount; + } + return this._playTime; + } + private _initGeometry(): void { const primitive = new Primitive(this.engine); this._primitive = primitive; @@ -345,8 +370,8 @@ export class TrailRenderer extends Renderer { * Move expired points from active to retired state. * Points in retired state are waiting for GPU to finish rendering before they can be freed. */ - private _retireActivePoints(frameCount: number): void { - const { _playTime: currentTime, time: lifetime, _vertices: vertices, _currentPointCapacity: capacity } = this; + private _retireActivePoints(currentTime: number, frameCount: number): void { + const { time: lifetime, _vertices: vertices, _currentPointCapacity: capacity } = this; const firstActiveOld = this._firstActiveElement; const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; @@ -395,7 +420,7 @@ export class TrailRenderer extends Renderer { } } - private _emitNewPoint(): void { + private _emitNewPoint(playTime: number): void { const worldPosition = this.entity.transform.worldPosition; if (this._hasLastPosition && Vector3.distance(worldPosition, this._lastPosition) < this.minVertexDistance) { @@ -412,19 +437,18 @@ export class TrailRenderer extends Renderer { } // Generate segment bounds in sync with trail points - this._generateSegmentBounds(worldPosition); + this._generateSegmentBounds(worldPosition, playTime); - this._addPoint(worldPosition); + this._addPoint(worldPosition, playTime); this._lastPosition.copyFrom(worldPosition); this._hasLastPosition = true; } - private _addPoint(position: Vector3): void { + private _addPoint(position: Vector3, playTime: number): void { const pointIndex = this._firstFreeElement; const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; const vertices = this._vertices; - const playTime = this._playTime; const tangent = TrailRenderer._tempVector3; if (this._hasLastPosition) { @@ -492,7 +516,7 @@ export class TrailRenderer extends Renderer { * Generate or update segment bounds for the given position. * Uses frame count to determine whether to add new or overwrite existing. */ - private _generateSegmentBounds(position: Vector3): void { + private _generateSegmentBounds(position: Vector3, playTime: number): void { const frameCount = this.engine.time.frameCount; const isSameFrame = frameCount === this._lastSegmentBoundsFrameCount; let writeIndex: number; @@ -522,20 +546,19 @@ export class TrailRenderer extends Renderer { const offset = writeIndex * TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; const boundsArray = this._segmentBoundsArray; position.copyToArray(boundsArray, offset); - boundsArray[offset + 3] = this._playTime; + boundsArray[offset + 3] = playTime; } /** * Retire segment bounds that have expired based on lifetime. */ - private _retireSegmentBounds(): void { + private _retireSegmentBounds(currentTime: number): void { const floatStride = TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; const timeOffset = TrailRenderer.SEGMENT_BOUNDS_TIME_OFFSET; const boundsArray = this._segmentBoundsArray; const firstFree = this._firstFreeSegmentBounds; const count = this._segmentBoundsCount; const lifetime = this.time; - const currentTime = this._playTime; while (this._firstActiveSegmentBounds !== firstFree) { const index = this._firstActiveSegmentBounds * floatStride; From 8984ad3c6b6f91a6fd28be03a2334d447455afbe Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 15:20:29 +0800 Subject: [PATCH 56/85] refactor: remove unused segment bounds management in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 182 +++++------------------ 1 file changed, 39 insertions(+), 143 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 59c53ca3b1..1c980fc1a1 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -30,11 +30,6 @@ export class TrailRenderer extends Renderer { private static readonly POINT_BYTE_STRIDE = 64; // 2 vertices per point private static readonly POINT_INCREASE_COUNT = 128; - // Segment bounds array layout: posX, posY, posZ, birthTime = 4 floats per segment - private static readonly SEGMENT_BOUNDS_FLOAT_STRIDE = 4; - private static readonly SEGMENT_BOUNDS_TIME_OFFSET = 3; - private static readonly SEGMENT_BOUNDS_INCREASE_COUNT = 64; - private static _timeParamsProp = ShaderProperty.getByName("renderer_TimeParams"); private static _trailParamsProp = ShaderProperty.getByName("renderer_TrailParams"); private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); @@ -104,18 +99,6 @@ export class TrailRenderer extends Renderer { @ignoreClone private _lastPlayTimeUpdateFrameCount = -1; - // Segment bounds (for efficient bounds calculation) - @ignoreClone - private _segmentBoundsArray: Float32Array; - @ignoreClone - private _segmentBoundsCount = 0; - @ignoreClone - private _firstActiveSegmentBounds = 0; - @ignoreClone - private _firstFreeSegmentBounds = 0; - @ignoreClone - private _lastSegmentBoundsFrameCount = -1; - /** * The fade-out duration in seconds. */ @@ -177,8 +160,6 @@ export class TrailRenderer extends Renderer { this._firstFreeElement = 0; this._firstRetiredElement = 0; this._hasLastPosition = false; - this._firstActiveSegmentBounds = this._firstFreeSegmentBounds; - this._lastSegmentBoundsFrameCount = -1; } protected override _update(context: RenderContext): void { @@ -190,7 +171,6 @@ export class TrailRenderer extends Renderer { this._freeRetiredPoints(frameCount); this._retireActivePoints(playTime, frameCount); - this._retireSegmentBounds(playTime); if (this.emitting) { this._emitNewPoint(playTime); @@ -239,19 +219,11 @@ export class TrailRenderer extends Renderer { } protected override _updateBounds(worldBounds: BoundingBox): void { - // Generate segment bounds for current position (same condition as _emitNewPoint) - if (this.emitting) { - const worldPosition = this.entity.transform.worldPosition; - if (!this._hasLastPosition || Vector3.distance(worldPosition, this._lastPosition) >= this.minVertexDistance) { - this._generateSegmentBounds(worldPosition, this._updateAndGetPlayTime()); - } - } - const halfWidth = this.width * 0.5; - const firstActive = this._firstActiveSegmentBounds; - const firstFree = this._firstFreeSegmentBounds; + const firstActive = this._firstActiveElement; + const firstFree = this._firstFreeElement; - // No active segment bounds - use entity position as fallback + // No active points - use entity position as fallback if (firstActive === firstFree) { const worldPosition = this.entity.transform.worldPosition; worldBounds.min.set(worldPosition.x - halfWidth, worldPosition.y - halfWidth, worldPosition.z - halfWidth); @@ -259,35 +231,57 @@ export class TrailRenderer extends Renderer { return; } - // Merge all segment bounds - const boundsArray = this._segmentBoundsArray; - const count = this._segmentBoundsCount; + // Merge all active points from vertex data + const vertices = this._vertices; + const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; + const capacity = this._currentPointCapacity; - // Initialize with first active segment bounds - const firstOffset = firstActive * TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; - worldBounds.min.copyFromArray(boundsArray, firstOffset); - worldBounds.max.copyFromArray(boundsArray, firstOffset); + // Initialize with first active point + const firstOffset = firstActive * pointStride; + worldBounds.min.set(vertices[firstOffset], vertices[firstOffset + 1], vertices[firstOffset + 2]); + worldBounds.max.copyFrom(worldBounds.min); - // Merge remaining segment bounds + // Merge remaining active points + const { min, max } = worldBounds; if (firstActive < firstFree) { for (let i = firstActive + 1; i < firstFree; i++) { - this._mergeSegmentBounds(i, worldBounds); + const offset = i * pointStride; + this._mergePointPosition(vertices, offset, min, max); } - } else if (firstActive > firstFree) { - for (let i = firstActive + 1; i < count; i++) { - this._mergeSegmentBounds(i, worldBounds); + } else { + for (let i = firstActive + 1; i < capacity; i++) { + const offset = i * pointStride; + this._mergePointPosition(vertices, offset, min, max); } for (let i = 0; i < firstFree; i++) { - this._mergeSegmentBounds(i, worldBounds); + const offset = i * pointStride; + this._mergePointPosition(vertices, offset, min, max); + } + } + + // Pre-generate: merge current position if it would create a new point + if (this.emitting) { + const worldPosition = this.entity.transform.worldPosition; + if (!this._hasLastPosition || Vector3.distance(worldPosition, this._lastPosition) >= this.minVertexDistance) { + const { x, y, z } = worldPosition; + min.set(Math.min(min.x, x), Math.min(min.y, y), Math.min(min.z, z)); + max.set(Math.max(max.x, x), Math.max(max.y, y), Math.max(max.z, z)); } } // Expand by half width for trail thickness - const { min, max } = worldBounds; min.set(min.x - halfWidth, min.y - halfWidth, min.z - halfWidth); max.set(max.x + halfWidth, max.y + halfWidth, max.z + halfWidth); } + private _mergePointPosition(vertices: Float32Array, offset: number, min: Vector3, max: Vector3): void { + const x = vertices[offset]; + const y = vertices[offset + 1]; + const z = vertices[offset + 2]; + min.set(Math.min(min.x, x), Math.min(min.y, y), Math.min(min.z, z)); + max.set(Math.max(max.x, x), Math.max(max.y, y), Math.max(max.z, z)); + } + protected override _onDestroy(): void { super._onDestroy(); this._vertexBuffer?.destroy(); @@ -436,9 +430,6 @@ export class TrailRenderer extends Renderer { this._resizeBuffer(TrailRenderer.POINT_INCREASE_COUNT); } - // Generate segment bounds in sync with trail points - this._generateSegmentBounds(worldPosition, playTime); - this._addPoint(worldPosition, playTime); this._lastPosition.copyFrom(worldPosition); this._hasLastPosition = true; @@ -512,101 +503,6 @@ export class TrailRenderer extends Renderer { return firstFree >= firstActive ? firstFree - firstActive : this._currentPointCapacity - firstActive + firstFree; } - /** - * Generate or update segment bounds for the given position. - * Uses frame count to determine whether to add new or overwrite existing. - */ - private _generateSegmentBounds(position: Vector3, playTime: number): void { - const frameCount = this.engine.time.frameCount; - const isSameFrame = frameCount === this._lastSegmentBoundsFrameCount; - let writeIndex: number; - - if (isSameFrame) { - // Same frame: overwrite previous position (go back one slot) - writeIndex = this._firstFreeSegmentBounds - 1; - if (writeIndex < 0) { - writeIndex = this._segmentBoundsCount - 1; - } - } else { - // New frame: check resize and advance pointer - writeIndex = this._firstFreeSegmentBounds; - let nextFree = writeIndex + 1; - if (nextFree >= this._segmentBoundsCount) { - nextFree = 0; - } - if (nextFree === this._firstActiveSegmentBounds) { - this._resizeSegmentBoundsArray(); - nextFree = writeIndex + 1; // Recalculate after resize (count increased, no wrap needed) - } - this._firstFreeSegmentBounds = nextFree; - this._lastSegmentBoundsFrameCount = frameCount; - } - - // Write segment bounds data - const offset = writeIndex * TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; - const boundsArray = this._segmentBoundsArray; - position.copyToArray(boundsArray, offset); - boundsArray[offset + 3] = playTime; - } - - /** - * Retire segment bounds that have expired based on lifetime. - */ - private _retireSegmentBounds(currentTime: number): void { - const floatStride = TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; - const timeOffset = TrailRenderer.SEGMENT_BOUNDS_TIME_OFFSET; - const boundsArray = this._segmentBoundsArray; - const firstFree = this._firstFreeSegmentBounds; - const count = this._segmentBoundsCount; - const lifetime = this.time; - - while (this._firstActiveSegmentBounds !== firstFree) { - const index = this._firstActiveSegmentBounds * floatStride; - const age = currentTime - boundsArray[index + timeOffset]; - if (age <= lifetime) { - break; - } - if (++this._firstActiveSegmentBounds >= count) { - this._firstActiveSegmentBounds = 0; - } - } - } - - private _mergeSegmentBounds(index: number, bounds: BoundingBox): void { - const boundsArray = this._segmentBoundsArray; - const offset = index * TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; - const x = boundsArray[offset]; - const y = boundsArray[offset + 1]; - const z = boundsArray[offset + 2]; - const { min, max } = bounds; - min.set(Math.min(min.x, x), Math.min(min.y, y), Math.min(min.z, z)); - max.set(Math.max(max.x, x), Math.max(max.y, y), Math.max(max.z, z)); - } - - private _resizeSegmentBoundsArray(): void { - const floatStride = TrailRenderer.SEGMENT_BOUNDS_FLOAT_STRIDE; - const increaseCount = TrailRenderer.SEGMENT_BOUNDS_INCREASE_COUNT; - - this._segmentBoundsCount += increaseCount; - const lastBoundsArray = this._segmentBoundsArray; - const boundsArray = new Float32Array(this._segmentBoundsCount * floatStride); - - if (lastBoundsArray) { - const firstFree = this._firstFreeSegmentBounds; - boundsArray.set(new Float32Array(lastBoundsArray.buffer, 0, firstFree * floatStride)); - - const nextFree = firstFree + 1; - const freeEndOffset = (nextFree + increaseCount) * floatStride; - boundsArray.set(new Float32Array(lastBoundsArray.buffer, nextFree * floatStride * 4), freeEndOffset); - - if (this._firstActiveSegmentBounds > firstFree) { - this._firstActiveSegmentBounds += increaseCount; - } - } - - this._segmentBoundsArray = boundsArray; - } - private _uploadNewVertices(): void { const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _vertexBuffer: buffer } = this; const firstNew = buffer.isContentLost || this._bufferResized ? firstActive : this._firstNewElement; From 2b164b023513497137e63749112c11c47a0b966f Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 15:34:21 +0800 Subject: [PATCH 57/85] refactor: optimize world bounds calculation in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 48 ++++++++++-------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 1c980fc1a1..1948c7b698 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -222,41 +222,31 @@ export class TrailRenderer extends Renderer { const halfWidth = this.width * 0.5; const firstActive = this._firstActiveElement; const firstFree = this._firstFreeElement; - - // No active points - use entity position as fallback - if (firstActive === firstFree) { - const worldPosition = this.entity.transform.worldPosition; - worldBounds.min.set(worldPosition.x - halfWidth, worldPosition.y - halfWidth, worldPosition.z - halfWidth); - worldBounds.max.set(worldPosition.x + halfWidth, worldPosition.y + halfWidth, worldPosition.z + halfWidth); - return; - } + const { min, max } = worldBounds; // Merge all active points from vertex data - const vertices = this._vertices; - const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; - const capacity = this._currentPointCapacity; + if (firstActive !== firstFree) { + const vertices = this._vertices; + const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; + const capacity = this._currentPointCapacity; - // Initialize with first active point - const firstOffset = firstActive * pointStride; - worldBounds.min.set(vertices[firstOffset], vertices[firstOffset + 1], vertices[firstOffset + 2]); - worldBounds.max.copyFrom(worldBounds.min); + min.set(Infinity, Infinity, Infinity); + max.set(-Infinity, -Infinity, -Infinity); - // Merge remaining active points - const { min, max } = worldBounds; - if (firstActive < firstFree) { - for (let i = firstActive + 1; i < firstFree; i++) { - const offset = i * pointStride; - this._mergePointPosition(vertices, offset, min, max); + const wrapped = firstActive > firstFree; + for (let i = firstActive, end = wrapped ? capacity : firstFree; i < end; i++) { + this._mergePointPosition(vertices, i * pointStride, min, max); } - } else { - for (let i = firstActive + 1; i < capacity; i++) { - const offset = i * pointStride; - this._mergePointPosition(vertices, offset, min, max); - } - for (let i = 0; i < firstFree; i++) { - const offset = i * pointStride; - this._mergePointPosition(vertices, offset, min, max); + if (wrapped) { + for (let i = 0; i < firstFree; i++) { + this._mergePointPosition(vertices, i * pointStride, min, max); + } } + } else { + // No active points - initialize with entity position + const { x, y, z } = this.entity.transform.worldPosition; + min.set(x, y, z); + max.set(x, y, z); } // Pre-generate: merge current position if it would create a new point From bda2efbecefc20a6aa48375d621cecd016ecff66 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 15:39:55 +0800 Subject: [PATCH 58/85] refactor: simplify point position initialization in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 1948c7b698..36b5c72e7e 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -228,13 +228,12 @@ export class TrailRenderer extends Renderer { if (firstActive !== firstFree) { const vertices = this._vertices; const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; - const capacity = this._currentPointCapacity; min.set(Infinity, Infinity, Infinity); max.set(-Infinity, -Infinity, -Infinity); const wrapped = firstActive > firstFree; - for (let i = firstActive, end = wrapped ? capacity : firstFree; i < end; i++) { + for (let i = firstActive, end = wrapped ? this._currentPointCapacity : firstFree; i < end; i++) { this._mergePointPosition(vertices, i * pointStride, min, max); } if (wrapped) { @@ -244,9 +243,9 @@ export class TrailRenderer extends Renderer { } } else { // No active points - initialize with entity position - const { x, y, z } = this.entity.transform.worldPosition; - min.set(x, y, z); - max.set(x, y, z); + const worldPosition = this.entity.transform.worldPosition; + min.copyFrom(worldPosition); + max.copyFrom(worldPosition); } // Pre-generate: merge current position if it would create a new point From 99cb1af38533764fd6492f5f635dd8e16ba91337 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 16:26:45 +0800 Subject: [PATCH 59/85] refactor: move emitting property declaration and improve point initialization logic in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 36b5c72e7e..189e281038 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -38,6 +38,9 @@ export class TrailRenderer extends Renderer { private static _tempVector3 = new Vector3(); + /** Whether the trail is being created as the object moves. */ + emitting = true; + /** The minimum distance the object must move before a new trail segment is added. */ minVertexDistance = 0.1; @@ -52,9 +55,6 @@ export class TrailRenderer extends Renderer { [new GradientAlphaKey(0, 1), new GradientAlphaKey(1, 1)] ); - /** Whether the trail is being created as the object moves. */ - emitting = true; - // Shader parameters @ignoreClone private _timeParams = new Vector4(0, 5.0, -1, 0); // x: currentTime, y: lifetime, z: oldestBirthTime, w: newestBirthTime @@ -242,19 +242,18 @@ export class TrailRenderer extends Renderer { } } } else { - // No active points - initialize with entity position - const worldPosition = this.entity.transform.worldPosition; - min.copyFrom(worldPosition); - max.copyFrom(worldPosition); + // No active points - initialize with last position or entity position + const position = this._hasLastPosition ? this._lastPosition : this.entity.transform.worldPosition; + min.copyFrom(position); + max.copyFrom(position); } // Pre-generate: merge current position if it would create a new point if (this.emitting) { const worldPosition = this.entity.transform.worldPosition; - if (!this._hasLastPosition || Vector3.distance(worldPosition, this._lastPosition) >= this.minVertexDistance) { - const { x, y, z } = worldPosition; - min.set(Math.min(min.x, x), Math.min(min.y, y), Math.min(min.z, z)); - max.set(Math.max(max.x, x), Math.max(max.y, y), Math.max(max.z, z)); + if (this._hasLastPosition && Vector3.distance(worldPosition, this._lastPosition) >= this.minVertexDistance) { + Vector3.min(min, worldPosition, min); + Vector3.max(max, worldPosition, max); } } @@ -374,7 +373,6 @@ export class TrailRenderer extends Renderer { } } if (this._firstActiveElement === this._firstFreeElement) { - this._hasLastPosition = false; this._timeParams.z = -1; this._timeParams.w = 0; } From 4878fa3d4d32c0f8f6f8e693f186d2ab30d221e7 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 16:50:59 +0800 Subject: [PATCH 60/85] refactor: streamline vertex buffer binding and improve point retirement logic in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 30 +++++++++--------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 189e281038..9269224666 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -344,14 +344,9 @@ export class TrailRenderer extends Renderer { this._vertices = newVertices; this._currentPointCapacity = newCapacity; - const vertexBufferBinding = new VertexBufferBinding(newVertexBuffer, TrailRenderer.VERTEX_STRIDE); - this._primitive.setVertexBufferBinding(0, vertexBufferBinding); + this._primitive.setVertexBufferBinding(0, new VertexBufferBinding(newVertexBuffer, TrailRenderer.VERTEX_STRIDE)); } - /** - * Move expired points from active to retired state. - * Points in retired state are waiting for GPU to finish rendering before they can be freed. - */ private _retireActivePoints(currentTime: number, frameCount: number): void { const { time: lifetime, _vertices: vertices, _currentPointCapacity: capacity } = this; const firstActiveOld = this._firstActiveElement; @@ -359,30 +354,27 @@ export class TrailRenderer extends Renderer { while (this._firstActiveElement !== this._firstFreeElement) { const offset = this._firstActiveElement * pointStride + 3; - const birthTime = vertices[offset]; - if (currentTime - birthTime < lifetime) break; + const age = currentTime - vertices[offset]; + // Use Math.fround to ensure CPU/GPU precision consistency + if (Math.fround(age) < lifetime) { + break; + } // Record the frame when this point was retired (reuse birthTime field) vertices[offset] = frameCount; this._firstActiveElement = (this._firstActiveElement + 1) % capacity; } - if (this._firstActiveElement !== firstActiveOld) { - // Update oldest birth time - if (this._firstActiveElement !== this._firstFreeElement) { - this._timeParams.z = vertices[this._firstActiveElement * pointStride + 3]; - } - } + // Update time params after retiring points if (this._firstActiveElement === this._firstFreeElement) { + // No active points remaining this._timeParams.z = -1; this._timeParams.w = 0; + } else if (this._firstActiveElement !== firstActiveOld) { + // Some points retired, update oldest birth time + this._timeParams.z = vertices[this._firstActiveElement * pointStride + 3]; } } - /** - * Free retired points that GPU has finished rendering. - * WebGL doesn't support mapBufferRange, so this optimization is currently disabled. - * The condition `frameCount - retireFrame < 0` will never be true, effectively skipping the check. - */ private _freeRetiredPoints(frameCount: number): void { const capacity = this._currentPointCapacity; const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; From 209faae164d27f81d2cc120d9d244d4732a3eabc Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 17:14:50 +0800 Subject: [PATCH 61/85] refactor: optimize tangent handling and improve active point count calculation in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 9269224666..4ef18604c0 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -427,11 +427,9 @@ export class TrailRenderer extends Renderer { // First point has placeholder tangent, update it when second point is added if (this._getActivePointCount() === 1) { - const firstPointIndex = this._firstActiveElement; - const offset0 = firstPointIndex * pointStride + 5; - const offset1 = offset0 + floatStride; - tangent.copyToArray(vertices, offset0); - tangent.copyToArray(vertices, offset1); + const firstPointOffset = this._firstActiveElement * pointStride; + tangent.copyToArray(vertices, firstPointOffset + 5); // Top vertex tangent + tangent.copyToArray(vertices, firstPointOffset + floatStride + 5); // Bottom vertex tangent // Mark first point for re-upload since its tangent changed this._firstNewElement = this._firstActiveElement; } @@ -478,13 +476,14 @@ export class TrailRenderer extends Renderer { } private _getActivePointCount(): number { - const { _firstActiveElement: firstActive, _firstFreeElement: firstFree } = this; - return firstFree >= firstActive ? firstFree - firstActive : this._currentPointCapacity - firstActive + firstFree; + const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _currentPointCapacity: capacity } = this; + return firstFree >= firstActive ? firstFree - firstActive : capacity - firstActive + firstFree; } private _uploadNewVertices(): void { const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _vertexBuffer: buffer } = this; - const firstNew = buffer.isContentLost || this._bufferResized ? firstActive : this._firstNewElement; + const needFullUpload = buffer.isContentLost || this._bufferResized; + const firstNew = needFullUpload ? firstActive : this._firstNewElement; this._bufferResized = false; if (firstNew === firstFree) return; From 09a8fc6fa1bb9f22320b2dce6470d1f29fdf6f2c Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 17:17:36 +0800 Subject: [PATCH 62/85] refactor: reorganize uniform declarations for clarity in TrailRenderer --- .../core/src/shaderlib/extra/trail.vs.glsl | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index 3256115a13..ffc2dc059b 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -1,26 +1,15 @@ -// Trail vertex attributes (per-vertex) -// Each segment has 2 vertices (top and bottom) attribute vec4 a_PositionBirthTime; // xyz: World position, w: Birth time attribute vec4 a_CornerTangent; // x: Corner (-1 or 1), yzw: Tangent direction -// Uniforms -// x: CurrentTime, y: Lifetime, z: OldestBirthTime, w: NewestBirthTime -uniform vec4 renderer_TimeParams; -// x: Width, y: TextureMode (0: Stretch, 1: Tile), z: TextureScale -uniform vec4 renderer_TrailParams; - +uniform vec4 renderer_TimeParams; // x: CurrentTime, y: Lifetime, z: OldestBirthTime, w: NewestBirthTime +uniform vec4 renderer_TrailParams; // x: Width, y: TextureMode (0: Stretch, 1: Tile), z: TextureScale uniform vec3 camera_Position; uniform mat4 camera_ViewMat; uniform mat4 camera_ProjMat; +uniform vec2 renderer_WidthCurve[4]; // Width curve (4 keyframes max: x=time, y=value) +uniform vec4 renderer_ColorKeys[4]; // Color gradient (x=time, yzw=rgb) +uniform vec2 renderer_AlphaKeys[4]; // Alpha gradient (x=time, y=alpha) -// Width curve uniforms (4 keyframes max: x=time, y=value) -uniform vec2 renderer_WidthCurve[4]; - -// Color gradient uniforms (4 keyframes max) -uniform vec4 renderer_ColorKeys[4]; // x=time, yzw=rgb -uniform vec2 renderer_AlphaKeys[4]; // x=time, y=alpha - -// Varyings varying vec2 v_uv; varying vec4 v_color; From 8d731172e8ea3fa278de2e07efbdee0d91f3b0bf Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 17:21:10 +0800 Subject: [PATCH 63/85] refactor: opt code --- .../core/src/shaderlib/extra/trail.vs.glsl | 51 +++++-------------- 1 file changed, 14 insertions(+), 37 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index ffc2dc059b..5dd83d7d98 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -62,45 +62,33 @@ vec4 evaluateGradient(in vec4 colorKeys[4], in vec2 alphaKeys[4], in float t) { } void main() { - // Extract position and birth time vec3 position = a_PositionBirthTime.xyz; float birthTime = a_PositionBirthTime.w; float corner = a_CornerTangent.x; vec3 tangent = a_CornerTangent.yzw; - - // Extract packed uniforms - float currentTime = renderer_TimeParams.x; - float lifetime = renderer_TimeParams.y; - float oldestBirthTime = renderer_TimeParams.z; float newestBirthTime = renderer_TimeParams.w; - float trailWidth = renderer_TrailParams.x; - float textureMode = renderer_TrailParams.y; - float textureScale = renderer_TrailParams.z; - // Calculate normalized age (0 = new, 1 = about to die) for lifetime check - float age = currentTime - birthTime; - float normalizedAge = clamp(age / lifetime, 0.0, 1.0); + // age: time since birth, normalizedAge: 0=new, 1=expired + float age = renderer_TimeParams.x - birthTime; + float normalizedAge = clamp(age / renderer_TimeParams.y, 0.0, 1.0); - // Discard vertices that have exceeded their lifetime + // Discard expired vertices if (normalizedAge >= 1.0) { - gl_Position = vec4(2.0, 2.0, 2.0, 1.0); // Move outside clip space + gl_Position = vec4(2.0, 2.0, 2.0, 1.0); return; } - // Calculate relative position in trail (0 = head/newest, 1 = tail/oldest) - // Used for width curve, color gradient, and UV in Stretch mode - float timeRange = newestBirthTime - oldestBirthTime; + // relativePosition: 0=head(newest), 1=tail(oldest) + float timeRange = newestBirthTime - renderer_TimeParams.z; float relativePosition = 0.0; if (timeRange > 0.0001) { relativePosition = (newestBirthTime - birthTime) / timeRange; } - // Calculate billboard offset (View alignment) + // Billboard: expand perpendicular to tangent and view direction vec3 toCamera = normalize(camera_Position - position); vec3 right = cross(tangent, toCamera); float rightLen = length(right); - - // Handle edge case when tangent is parallel to camera direction if (rightLen < 0.001) { right = cross(tangent, vec3(0.0, 1.0, 0.0)); rightLen = length(right); @@ -111,29 +99,18 @@ void main() { } right = right / rightLen; - // Evaluate width curve using relative position float widthMultiplier = evaluateCurve(renderer_WidthCurve, relativePosition); - float width = trailWidth * widthMultiplier; - - // Apply offset + float width = renderer_TrailParams.x * widthMultiplier; vec3 worldPosition = position + right * width * 0.5 * corner; gl_Position = camera_ProjMat * camera_ViewMat * vec4(worldPosition, 1.0); - // Calculate UV based on texture mode - float u = corner * 0.5 + 0.5; // 0 for bottom, 1 for top - float v; - - if (textureMode < 0.5) { - // Stretch mode: UV.v based on relative position in trail - v = relativePosition; - } else { - // Tile mode: scale by tile scale (use normalizedAge for tiling effect) - v = normalizedAge * textureScale; - } - + // UV: u=corner side, v=position along trail or tiled + float u = corner * 0.5 + 0.5; + float v = renderer_TrailParams.y < 0.5 + ? relativePosition + : normalizedAge * renderer_TrailParams.z; v_uv = vec2(u, v); - // Evaluate color gradient using relative position v_color = evaluateGradient(renderer_ColorKeys, renderer_AlphaKeys, relativePosition); } From 9decb80d09ccd8cd91e7cb842593e9e1ee30da39 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 1 Jan 2026 22:44:56 +0800 Subject: [PATCH 64/85] refactor: enhance gradient handling in TrailRenderer and shader modules --- .../core/src/shaderlib/extra/trail.vs.glsl | 55 ++----------------- .../particle/color_over_lifetime_module.glsl | 45 --------------- .../shaderlib/particle/particle_common.glsl | 36 ++++++++++++ packages/core/src/trail/TrailRenderer.ts | 10 ++++ 4 files changed, 51 insertions(+), 95 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index 5dd83d7d98..22c1260283 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -6,60 +6,15 @@ uniform vec4 renderer_TrailParams; // x: Width, y: TextureMode (0: Stretch, 1: uniform vec3 camera_Position; uniform mat4 camera_ViewMat; uniform mat4 camera_ProjMat; -uniform vec2 renderer_WidthCurve[4]; // Width curve (4 keyframes max: x=time, y=value) +uniform vec2 renderer_WidthCurve[4]; // Width curve (4 keyframes max: x=time, y=value) uniform vec4 renderer_ColorKeys[4]; // Color gradient (x=time, yzw=rgb) uniform vec2 renderer_AlphaKeys[4]; // Alpha gradient (x=time, y=alpha) +uniform vec4 renderer_GradientMaxTime; // x: colorMaxTime, y: alphaMaxTime varying vec2 v_uv; varying vec4 v_color; -// Evaluate width curve at normalized age (same as particle system) -float evaluateCurve(in vec2 keys[4], in float t) { - float value; - for (int i = 1; i < 4; i++) { - vec2 key = keys[i]; - float time = key.x; - if (time >= t) { - vec2 lastKey = keys[i - 1]; - float lastTime = lastKey.x; - float age = (t - lastTime) / (time - lastTime); - value = mix(lastKey.y, key.y, age); - break; - } - } - return value; -} - -// Evaluate color gradient at normalized age (fixed 4 iterations) -vec4 evaluateGradient(in vec4 colorKeys[4], in vec2 alphaKeys[4], in float t) { - vec4 result = vec4(colorKeys[0].yzw, alphaKeys[0].y); - - // Evaluate color keys - for (int i = 1; i < 4; i++) { - vec4 key = colorKeys[i]; - if (t <= key.x) { - float t0 = colorKeys[i - 1].x; - float factor = (t - t0) / (key.x - t0); - result.rgb = mix(colorKeys[i - 1].yzw, key.yzw, factor); - break; - } - result.rgb = key.yzw; - } - - // Evaluate alpha keys - for (int i = 1; i < 4; i++) { - vec2 key = alphaKeys[i]; - if (t <= key.x) { - float t0 = alphaKeys[i - 1].x; - float factor = (t - t0) / (key.x - t0); - result.a = mix(alphaKeys[i - 1].y, key.y, factor); - break; - } - result.a = key.y; - } - - return result; -} +#include void main() { vec3 position = a_PositionBirthTime.xyz; @@ -99,7 +54,7 @@ void main() { } right = right / rightLen; - float widthMultiplier = evaluateCurve(renderer_WidthCurve, relativePosition); + float widthMultiplier = evaluateParticleCurve(renderer_WidthCurve, relativePosition); float width = renderer_TrailParams.x * widthMultiplier; vec3 worldPosition = position + right * width * 0.5 * corner; @@ -112,5 +67,5 @@ void main() { : normalizedAge * renderer_TrailParams.z; v_uv = vec2(u, v); - v_color = evaluateGradient(renderer_ColorKeys, renderer_AlphaKeys, relativePosition); + v_color = evaluateParticleGradient(renderer_ColorKeys, renderer_GradientMaxTime.x, renderer_AlphaKeys, renderer_GradientMaxTime.y, relativePosition); } diff --git a/packages/core/src/shaderlib/particle/color_over_lifetime_module.glsl b/packages/core/src/shaderlib/particle/color_over_lifetime_module.glsl index cba09fb300..56127ce487 100644 --- a/packages/core/src/shaderlib/particle/color_over_lifetime_module.glsl +++ b/packages/core/src/shaderlib/particle/color_over_lifetime_module.glsl @@ -1,4 +1,3 @@ - #if defined(RENDERER_COL_GRADIENT) || defined(RENDERER_COL_RANDOM_GRADIENTS) uniform vec4 renderer_COLMaxGradientColor[4]; // x:time y:r z:g w:b uniform vec2 renderer_COLMaxGradientAlpha[4]; // x:time y:alpha @@ -12,50 +11,6 @@ #endif - -#if defined(RENDERER_COL_GRADIENT) || defined(RENDERER_COL_RANDOM_GRADIENTS) - vec4 evaluateParticleGradient(in vec4 colorKeys[4], in float colorKeysMaxTime, in vec2 alphaKeys[4], in float alphaKeysMaxTime, in float normalizedAge){ - vec4 value; - float alphaAge = min(normalizedAge, alphaKeysMaxTime); - for(int i = 0; i < 4; i++){ - vec2 key = alphaKeys[i]; - float time = key.x; - if(alphaAge <= time){ - if(i == 0){ - value.a = alphaKeys[0].y; - } - else { - vec2 lastKey = alphaKeys[i-1]; - float lastTime = lastKey.x; - float age = (alphaAge - lastTime) / (time - lastTime); - value.a = mix(lastKey.y, key.y, age); - } - break; - } - } - - float colorAge = min(normalizedAge, colorKeysMaxTime); - for(int i = 0; i < 4; i++){ - vec4 key = colorKeys[i]; - float time = key.x; - if(colorAge <= time){ - if(i == 0){ - value.rgb = colorKeys[0].yzw; - } - else { - vec4 lastKey = colorKeys[i-1]; - float lastTime = lastKey.x; - float age = (colorAge - lastTime) / (time-lastTime); - value.rgb = mix(lastKey.yzw, key.yzw, age); - } - break; - } - } - return value; - } -#endif - - vec4 computeParticleColor(in vec4 color, in float normalizedAge) { #if defined(RENDERER_COL_GRADIENT) || defined(RENDERER_COL_RANDOM_GRADIENTS) vec4 gradientColor = evaluateParticleGradient(renderer_COLMaxGradientColor, renderer_COLGradientKeysMaxTime.z, renderer_COLMaxGradientAlpha, renderer_COLGradientKeysMaxTime.w, normalizedAge); diff --git a/packages/core/src/shaderlib/particle/particle_common.glsl b/packages/core/src/shaderlib/particle/particle_common.glsl index 7fe4aa8177..7034d51479 100644 --- a/packages/core/src/shaderlib/particle/particle_common.glsl +++ b/packages/core/src/shaderlib/particle/particle_common.glsl @@ -71,4 +71,40 @@ float evaluateParticleCurveCumulative(in vec2 keys[4], in float normalizedAge, o } } return cumulativeValue; +} + +vec4 evaluateParticleGradient(in vec4 colorKeys[4], in float colorMaxTime, in vec2 alphaKeys[4], in float alphaMaxTime, in float t) { + vec4 value; + + float alphaT = min(t, alphaMaxTime); + for (int i = 0; i < 4; i++) { + vec2 key = alphaKeys[i]; + if (alphaT <= key.x) { + if (i == 0) { + value.a = alphaKeys[0].y; + } else { + vec2 lastKey = alphaKeys[i - 1]; + float age = (alphaT - lastKey.x) / (key.x - lastKey.x); + value.a = mix(lastKey.y, key.y, age); + } + break; + } + } + + float colorT = min(t, colorMaxTime); + for (int i = 0; i < 4; i++) { + vec4 key = colorKeys[i]; + if (colorT <= key.x) { + if (i == 0) { + value.rgb = colorKeys[0].yzw; + } else { + vec4 lastKey = colorKeys[i - 1]; + float age = (colorT - lastKey.x) / (key.x - lastKey.x); + value.rgb = mix(lastKey.yzw, key.yzw, age); + } + break; + } + } + + return value; } \ No newline at end of file diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 4ef18604c0..757cc7eb98 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -35,6 +35,7 @@ export class TrailRenderer extends Renderer { private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); + private static _gradientMaxTimeProp = ShaderProperty.getByName("renderer_GradientMaxTime"); private static _tempVector3 = new Vector3(); @@ -60,6 +61,8 @@ export class TrailRenderer extends Renderer { private _timeParams = new Vector4(0, 5.0, -1, 0); // x: currentTime, y: lifetime, z: oldestBirthTime, w: newestBirthTime @ignoreClone private _trailParams = new Vector4(1.0, TrailTextureMode.Stretch, 1.0, 0); // x: width, y: textureMode, z: textureScale + @ignoreClone + private _gradientMaxTime = new Vector4(); // x: colorMaxTime, y: alphaMaxTime // Geometry and rendering @ignoreClone @@ -190,6 +193,13 @@ export class TrailRenderer extends Renderer { shaderData.setFloatArray(TrailRenderer._widthCurveProp, this.widthCurve._getTypeArray()); shaderData.setFloatArray(TrailRenderer._colorKeysProp, colorGradient._getColorTypeArray()); shaderData.setFloatArray(TrailRenderer._alphaKeysProp, colorGradient._getAlphaTypeArray()); + + const colorKeys = colorGradient.colorKeys; + const alphaKeys = colorGradient.alphaKeys; + const gradientMaxTime = this._gradientMaxTime; + gradientMaxTime.x = colorKeys.length ? colorKeys[colorKeys.length - 1].time : 0; + gradientMaxTime.y = alphaKeys.length ? alphaKeys[alphaKeys.length - 1].time : 0; + shaderData.setVector4(TrailRenderer._gradientMaxTimeProp, gradientMaxTime); } protected override _render(context: RenderContext): void { From bbf45ef2b64de0fd2c4007c481b4f1dd754c9fe4 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Fri, 2 Jan 2026 00:03:59 +0800 Subject: [PATCH 65/85] refactor: update TrailRenderer to use distance-based calculations and streamline shader parameters --- .../core/src/shaderlib/extra/trail.vs.glsl | 66 ++++++++-------- packages/core/src/trail/TrailRenderer.ts | 79 +++++++++++-------- 2 files changed, 75 insertions(+), 70 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index 22c1260283..226eaa9d71 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -1,8 +1,9 @@ attribute vec4 a_PositionBirthTime; // xyz: World position, w: Birth time 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_TimeParams; // x: CurrentTime, y: Lifetime, z: OldestBirthTime, w: NewestBirthTime -uniform vec4 renderer_TrailParams; // x: Width, y: TextureMode (0: Stretch, 1: Tile), z: TextureScale +uniform vec4 renderer_TrailParams; // x: Width, y: TextureMode (0: Stretch, 1: Tile), z: TextureScale +uniform vec4 renderer_TimeDistParams; // x: CurrentTime, y: Lifetime, z: HeadDistance, w: TailDistance uniform vec3 camera_Position; uniform mat4 camera_ViewMat; uniform mat4 camera_ProjMat; @@ -21,51 +22,46 @@ void main() { float birthTime = a_PositionBirthTime.w; float corner = a_CornerTangent.x; vec3 tangent = a_CornerTangent.yzw; - float newestBirthTime = renderer_TimeParams.w; // age: time since birth, normalizedAge: 0=new, 1=expired - float age = renderer_TimeParams.x - birthTime; - float normalizedAge = clamp(age / renderer_TimeParams.y, 0.0, 1.0); + float age = renderer_TimeDistParams.x - birthTime; + float normalizedAge = age / renderer_TimeDistParams.y; // Discard expired vertices if (normalizedAge >= 1.0) { gl_Position = vec4(2.0, 2.0, 2.0, 1.0); - return; - } - - // relativePosition: 0=head(newest), 1=tail(oldest) - float timeRange = newestBirthTime - renderer_TimeParams.z; - float relativePosition = 0.0; - if (timeRange > 0.0001) { - relativePosition = (newestBirthTime - birthTime) / timeRange; - } + } else { + // Distance-based relative position: 0=head(newest), 1=tail(oldest) + float distFromHead = renderer_TimeDistParams.z - a_Distance; + float totalDist = renderer_TimeDistParams.z - renderer_TimeDistParams.w; + float relativePos = totalDist > 0.0 ? distFromHead / totalDist : 0.0; - // Billboard: expand perpendicular to tangent and view direction - vec3 toCamera = normalize(camera_Position - position); - vec3 right = cross(tangent, toCamera); - float rightLen = length(right); - if (rightLen < 0.001) { - right = cross(tangent, vec3(0.0, 1.0, 0.0)); - rightLen = length(right); + // Billboard: expand perpendicular to tangent and view direction + vec3 toCamera = normalize(camera_Position - position); + vec3 right = cross(tangent, toCamera); + float rightLen = length(right); if (rightLen < 0.001) { - right = cross(tangent, vec3(1.0, 0.0, 0.0)); + right = cross(tangent, vec3(0.0, 1.0, 0.0)); rightLen = length(right); + if (rightLen < 0.001) { + right = cross(tangent, vec3(1.0, 0.0, 0.0)); + rightLen = length(right); + } } - } - right = right / rightLen; + right = right / rightLen; - float widthMultiplier = evaluateParticleCurve(renderer_WidthCurve, relativePosition); - float width = renderer_TrailParams.x * widthMultiplier; - vec3 worldPosition = position + right * width * 0.5 * corner; + float widthMultiplier = evaluateParticleCurve(renderer_WidthCurve, relativePos); + float width = renderer_TrailParams.x * widthMultiplier; + vec3 worldPosition = position + right * width * 0.5 * corner; - gl_Position = camera_ProjMat * camera_ViewMat * vec4(worldPosition, 1.0); + gl_Position = camera_ProjMat * camera_ViewMat * vec4(worldPosition, 1.0); - // UV: u=corner side, v=position along trail or tiled - float u = corner * 0.5 + 0.5; - float v = renderer_TrailParams.y < 0.5 - ? relativePosition - : normalizedAge * renderer_TrailParams.z; - v_uv = vec2(u, v); + // 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); - v_color = evaluateParticleGradient(renderer_ColorKeys, renderer_GradientMaxTime.x, renderer_AlphaKeys, renderer_GradientMaxTime.y, relativePosition); + v_color = evaluateParticleGradient(renderer_ColorKeys, renderer_GradientMaxTime.x, renderer_AlphaKeys, renderer_GradientMaxTime.y, relativePos); + } } diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 757cc7eb98..1467f5f2f9 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -24,14 +24,15 @@ import { TrailTextureMode } from "./enums/TrailTextureMode"; * Renders a trail behind a moving object. */ export class TrailRenderer extends Renderer { - private static readonly VERTEX_STRIDE = 32; - private static readonly VERTEX_FLOAT_STRIDE = 8; - private static readonly POINT_FLOAT_STRIDE = 16; // 2 vertices per point - private static readonly POINT_BYTE_STRIDE = 64; // 2 vertices per point + private static readonly VERTEX_STRIDE = 36; // 9 floats * 4 bytes + private static readonly VERTEX_FLOAT_STRIDE = 9; // pos(3) + birthTime(1) + corner(1) + tangent(3) + distance(1) + private static readonly DISTANCE_OFFSET = 8; // offset of distance field in vertex + private static readonly POINT_FLOAT_STRIDE = 18; // 2 vertices per point + private static readonly POINT_BYTE_STRIDE = 72; // 2 vertices per point private static readonly POINT_INCREASE_COUNT = 128; - private static _timeParamsProp = ShaderProperty.getByName("renderer_TimeParams"); private static _trailParamsProp = ShaderProperty.getByName("renderer_TrailParams"); + private static _timeDistParamsProp = ShaderProperty.getByName("renderer_TimeDistParams"); private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); @@ -58,10 +59,10 @@ export class TrailRenderer extends Renderer { // Shader parameters @ignoreClone - private _timeParams = new Vector4(0, 5.0, -1, 0); // x: currentTime, y: lifetime, z: oldestBirthTime, w: newestBirthTime - @ignoreClone private _trailParams = new Vector4(1.0, TrailTextureMode.Stretch, 1.0, 0); // x: width, y: textureMode, z: textureScale @ignoreClone + private _timeDistParams = new Vector4(0, 5.0, 0, 0); // x: currentTime, y: lifetime, z: headDistance, w: tailDistance + @ignoreClone private _gradientMaxTime = new Vector4(); // x: colorMaxTime, y: alphaMaxTime // Geometry and rendering @@ -95,6 +96,8 @@ export class TrailRenderer extends Renderer { private _lastPosition = new Vector3(); @ignoreClone private _hasLastPosition = false; + @ignoreClone + private _cumulativeDistance = 0; // Total distance traveled since trail start // Time tracking @ignoreClone @@ -106,11 +109,11 @@ export class TrailRenderer extends Renderer { * The fade-out duration in seconds. */ get time(): number { - return this._timeParams.y; + return this._timeDistParams.y; } set time(value: number) { - this._timeParams.y = value; + this._timeDistParams.y = value; } /** @@ -183,11 +186,21 @@ export class TrailRenderer extends Renderer { } const shaderData = this.shaderData; - const timeParams = this._timeParams; - timeParams.x = playTime; + const timeDistParams = this._timeDistParams; + const { _vertices: vertices, _currentPointCapacity: capacity } = this; + const activeCount = this._getActivePointCount(); + timeDistParams.x = playTime; + // z: headDistance (newest point), w: tailDistance (oldest point) + if (activeCount > 0) { + const headIndex = (this._firstFreeElement - 1 + capacity) % capacity; + timeDistParams.z = vertices[headIndex * TrailRenderer.POINT_FLOAT_STRIDE + TrailRenderer.DISTANCE_OFFSET]; + timeDistParams.w = vertices[this._firstActiveElement * TrailRenderer.POINT_FLOAT_STRIDE + TrailRenderer.DISTANCE_OFFSET]; + } else { + timeDistParams.z = timeDistParams.w = 0; + } - shaderData.setVector4(TrailRenderer._timeParamsProp, timeParams); shaderData.setVector4(TrailRenderer._trailParamsProp, this._trailParams); + shaderData.setVector4(TrailRenderer._timeDistParamsProp, timeDistParams); const { colorGradient } = this; shaderData.setFloatArray(TrailRenderer._widthCurveProp, this.widthCurve._getTypeArray()); @@ -299,11 +312,13 @@ export class TrailRenderer extends Renderer { private _initGeometry(): void { const primitive = new Primitive(this.engine); this._primitive = primitive; - // Vertex layout (2 x vec4 = 32 bytes): - // a_PositionBirthTime: xyz = position, w = birthTime - // a_CornerTangent: x = corner (-1 or 1), yzw = tangent direction + // Vertex layout (9 floats = 36 bytes): + // a_PositionBirthTime: xyz = position, w = birthTime (offset 0) + // a_CornerTangent: x = corner (-1 or 1), yzw = tangent direction (offset 16) + // a_Distance: distance from trail head in world units (offset 32) primitive.addVertexElement(new VertexElement("a_PositionBirthTime", 0, VertexElementFormat.Vector4, 0)); primitive.addVertexElement(new VertexElement("a_CornerTangent", 16, VertexElementFormat.Vector4, 0)); + primitive.addVertexElement(new VertexElement("a_Distance", 32, VertexElementFormat.Float, 0)); this._mainSubPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); this._wrapSubPrimitive = new SubPrimitive(0, 0, MeshTopology.TriangleStrip); @@ -359,7 +374,6 @@ export class TrailRenderer extends Renderer { private _retireActivePoints(currentTime: number, frameCount: number): void { const { time: lifetime, _vertices: vertices, _currentPointCapacity: capacity } = this; - const firstActiveOld = this._firstActiveElement; const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; while (this._firstActiveElement !== this._firstFreeElement) { @@ -373,16 +387,6 @@ export class TrailRenderer extends Renderer { vertices[offset] = frameCount; this._firstActiveElement = (this._firstActiveElement + 1) % capacity; } - - // Update time params after retiring points - if (this._firstActiveElement === this._firstFreeElement) { - // No active points remaining - this._timeParams.z = -1; - this._timeParams.w = 0; - } else if (this._firstActiveElement !== firstActiveOld) { - // Some points retired, update oldest birth time - this._timeParams.z = vertices[this._firstActiveElement * pointStride + 3]; - } } private _freeRetiredPoints(frameCount: number): void { @@ -429,10 +433,12 @@ export class TrailRenderer extends Renderer { const floatStride = TrailRenderer.VERTEX_FLOAT_STRIDE; const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; const vertices = this._vertices; + const capacity = this._currentPointCapacity; const tangent = TrailRenderer._tempVector3; if (this._hasLastPosition) { Vector3.subtract(position, this._lastPosition, tangent); + const segmentLength = tangent.length(); tangent.normalize(); // First point has placeholder tangent, update it when second point is added @@ -443,46 +449,49 @@ export class TrailRenderer extends Renderer { // Mark first point for re-upload since its tangent changed this._firstNewElement = this._firstActiveElement; } + + // Update cumulative distance + this._cumulativeDistance += segmentLength; } else { // First point uses placeholder tangent (will be corrected when second point arrives) tangent.set(0, 0, 1); } // Write top vertex (corner = -1) and bottom vertex (corner = 1) + // Store absolute cumulative distance (written once, never updated) + const distOffset = TrailRenderer.DISTANCE_OFFSET; + const cumulativeDist = this._cumulativeDistance; const topOffset = pointIndex * pointStride; position.copyToArray(vertices, topOffset); vertices[topOffset + 3] = playTime; vertices[topOffset + 4] = -1; tangent.copyToArray(vertices, topOffset + 5); + vertices[topOffset + distOffset] = cumulativeDist; const bottomOffset = topOffset + floatStride; position.copyToArray(vertices, bottomOffset); vertices[bottomOffset + 3] = playTime; vertices[bottomOffset + 4] = 1; tangent.copyToArray(vertices, bottomOffset + 5); + vertices[bottomOffset + distOffset] = cumulativeDist; // Write to bridge position when writing point 0 if (pointIndex === 0) { - const bridgeTopOffset = this._currentPointCapacity * pointStride; + const bridgeTopOffset = capacity * pointStride; const bridgeBottomOffset = bridgeTopOffset + floatStride; position.copyToArray(vertices, bridgeTopOffset); vertices[bridgeTopOffset + 3] = playTime; vertices[bridgeTopOffset + 4] = -1; tangent.copyToArray(vertices, bridgeTopOffset + 5); + vertices[bridgeTopOffset + distOffset] = cumulativeDist; position.copyToArray(vertices, bridgeBottomOffset); vertices[bridgeBottomOffset + 3] = playTime; vertices[bridgeBottomOffset + 4] = 1; tangent.copyToArray(vertices, bridgeBottomOffset + 5); + vertices[bridgeBottomOffset + distOffset] = cumulativeDist; } - this._firstFreeElement = (this._firstFreeElement + 1) % this._currentPointCapacity; - - // Update time params - const timeParams = this._timeParams; - if (timeParams.z === -1) { - timeParams.z = playTime; // First point: set oldest birth time - } - timeParams.w = playTime; // Always update newest birth time + this._firstFreeElement = (this._firstFreeElement + 1) % capacity; } private _getActivePointCount(): number { From be0abf846fb0be68884c6fdd387f81da36b66f9b Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Fri, 2 Jan 2026 00:19:11 +0800 Subject: [PATCH 66/85] refactor: update TrailRenderer image asset with new version and size --- e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg b/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg index ede3eeaaaa..dba9e1b8c4 100644 --- a/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg +++ b/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ebbc5a9ff36dc8a0889bd766d67d4c9291294af80da6d1f2d98bd65e99ba3fc -size 18741 +oid sha256:effe61b1e2ed57bb38c25477e9f079c9d4de658d9d1e52466fae089749e95858 +size 19407 From b67786e6360d8ace47bb912fb0ffd0389ca4981a Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Fri, 2 Jan 2026 02:06:22 +0800 Subject: [PATCH 67/85] refactor: optimize TrailRenderer bounds calculation and add unit tests --- packages/core/src/trail/TrailRenderer.ts | 19 +- tests/src/core/Trail.test.ts | 286 +++++++++++++++++++++++ 2 files changed, 298 insertions(+), 7 deletions(-) create mode 100644 tests/src/core/Trail.test.ts diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 1467f5f2f9..05c446cea7 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -242,13 +242,14 @@ export class TrailRenderer extends Renderer { } protected override _updateBounds(worldBounds: BoundingBox): void { - const halfWidth = this.width * 0.5; const firstActive = this._firstActiveElement; const firstFree = this._firstFreeElement; const { min, max } = worldBounds; + let hasTrailGeometry = firstActive !== firstFree; + // Merge all active points from vertex data - if (firstActive !== firstFree) { + if (hasTrailGeometry) { const vertices = this._vertices; const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; @@ -272,17 +273,21 @@ export class TrailRenderer extends Renderer { } // Pre-generate: merge current position if it would create a new point - if (this.emitting) { + if (this.emitting && this._hasLastPosition) { const worldPosition = this.entity.transform.worldPosition; - if (this._hasLastPosition && Vector3.distance(worldPosition, this._lastPosition) >= this.minVertexDistance) { + if (Vector3.distance(worldPosition, this._lastPosition) >= this.minVertexDistance) { Vector3.min(min, worldPosition, min); Vector3.max(max, worldPosition, max); + hasTrailGeometry = true; } } - // Expand by half width for trail thickness - min.set(min.x - halfWidth, min.y - halfWidth, min.z - halfWidth); - max.set(max.x + halfWidth, max.y + halfWidth, max.z + halfWidth); + // Only expand by half width when there's actual/upcoming trail geometry + if (hasTrailGeometry) { + const halfWidth = this.width * 0.5; + min.set(min.x - halfWidth, min.y - halfWidth, min.z - halfWidth); + max.set(max.x + halfWidth, max.y + halfWidth, max.z + halfWidth); + } } private _mergePointPosition(vertices: Float32Array, offset: number, min: Vector3, max: Vector3): void { diff --git a/tests/src/core/Trail.test.ts b/tests/src/core/Trail.test.ts new file mode 100644 index 0000000000..f647db25c5 --- /dev/null +++ b/tests/src/core/Trail.test.ts @@ -0,0 +1,286 @@ +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { + TrailRenderer, + TrailMaterial, + TrailTextureMode, + CurveKey, + ParticleCurve, + ParticleGradient, + GradientColorKey, + GradientAlphaKey, + BlendMode, + Camera +} from "@galacean/engine-core"; +import { Color, Vector3 } from "@galacean/engine-math"; +import { describe, it, expect, beforeEach } from "vitest"; + +describe("Trail", async () => { + const canvas = document.createElement("canvas"); + const engine = await WebGLEngine.create({ canvas: canvas }); + const scene = engine.sceneManager.activeScene; + + engine.run(); + + beforeEach(() => { + const rootEntity = scene.createRootEntity("root"); + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, -10); + cameraEntity.transform.lookAt(new Vector3()); + }); + + describe("TrailRenderer", () => { + it("Constructor", () => { + const rootEntity = scene.getRootEntity(); + const trailRenderer = rootEntity.addComponent(TrailRenderer); + + expect(trailRenderer instanceof TrailRenderer).to.eq(true); + 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); + }); + + it("set emitting", () => { + const rootEntity = scene.getRootEntity(); + const trailEntity = rootEntity.createChild("trail"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + + trailRenderer.emitting = false; + expect(trailRenderer.emitting).to.eq(false); + + trailRenderer.emitting = true; + expect(trailRenderer.emitting).to.eq(true); + }); + + it("set minVertexDistance", () => { + const rootEntity = scene.getRootEntity(); + const trailEntity = rootEntity.createChild("trail"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + + trailRenderer.minVertexDistance = 0.5; + expect(trailRenderer.minVertexDistance).to.eq(0.5); + + trailRenderer.minVertexDistance = 0.2; + expect(trailRenderer.minVertexDistance).to.eq(0.2); + }); + + it("set time", () => { + const rootEntity = scene.getRootEntity(); + const trailEntity = rootEntity.createChild("trail"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + + trailRenderer.time = 2.0; + expect(trailRenderer.time).to.eq(2.0); + + trailRenderer.time = 10.0; + 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"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + + trailRenderer.textureMode = TrailTextureMode.Tile; + expect(trailRenderer.textureMode).to.eq(TrailTextureMode.Tile); + + trailRenderer.textureMode = TrailTextureMode.Stretch; + expect(trailRenderer.textureMode).to.eq(TrailTextureMode.Stretch); + }); + + it("set textureScale", () => { + const rootEntity = scene.getRootEntity(); + const trailEntity = rootEntity.createChild("trail"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + + trailRenderer.textureScale = 2.0; + expect(trailRenderer.textureScale).to.eq(2.0); + + trailRenderer.textureScale = 0.5; + expect(trailRenderer.textureScale).to.eq(0.5); + }); + + it("set widthCurve", () => { + const rootEntity = scene.getRootEntity(); + const trailEntity = rootEntity.createChild("trail"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + + const widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 0)); + trailRenderer.widthCurve = widthCurve; + + expect(trailRenderer.widthCurve).to.eq(widthCurve); + expect(trailRenderer.widthCurve.keys.length).to.eq(2); + expect(trailRenderer.widthCurve.keys[0].time).to.eq(0); + expect(trailRenderer.widthCurve.keys[0].value).to.eq(1); + expect(trailRenderer.widthCurve.keys[1].time).to.eq(1); + expect(trailRenderer.widthCurve.keys[1].value).to.eq(0); + }); + + it("set colorGradient", () => { + const rootEntity = scene.getRootEntity(); + const trailEntity = rootEntity.createChild("trail"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + + const gradient = new ParticleGradient( + [new GradientColorKey(0, new Color(1, 0, 0, 1)), new GradientColorKey(1, new Color(0, 0, 1, 1))], + [new GradientAlphaKey(0, 1), new GradientAlphaKey(1, 0)] + ); + trailRenderer.colorGradient = gradient; + + expect(trailRenderer.colorGradient).to.eq(gradient); + expect(trailRenderer.colorGradient.colorKeys.length).to.eq(2); + expect(trailRenderer.colorGradient.alphaKeys.length).to.eq(2); + }); + + it("clear", () => { + const rootEntity = scene.getRootEntity(); + const trailEntity = rootEntity.createChild("trail"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + + // Call clear and verify it doesn't throw + expect(() => trailRenderer.clear()).not.to.throw(); + }); + + it("setMaterial", () => { + const rootEntity = scene.getRootEntity(); + const trailEntity = rootEntity.createChild("trail"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + const material = new TrailMaterial(engine); + + trailRenderer.setMaterial(material); + expect(trailRenderer.getMaterial()).to.eq(material); + }); + + it("destroy", () => { + const rootEntity = scene.getRootEntity(); + const trailEntity = rootEntity.createChild("trail"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + + trailRenderer.destroy(); + expect(trailRenderer.destroyed).to.eq(true); + }); + + it("bounds", () => { + const rootEntity = scene.getRootEntity(); + const trailEntity = rootEntity.createChild("trail"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + trailRenderer.setMaterial(new TrailMaterial(engine)); + trailRenderer.width = 2.0; + trailRenderer.minVertexDistance = 0.1; + + const halfWidth = trailRenderer.width * 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 }); + expect(trailRenderer.bounds.max).to.deep.include({ x: 0, y: 0, z: 0 }); + + // Move entity to (5, 0, 0) - distance > minVertexDistance, creates trail point + engine.update(); //@todo:删除会触发包围盒无法更新的bug + trailEntity.transform.position = new Vector3(5, 0, 0); + + // Now has trail geometry, bounds should encompass (0,0,0) to (5,0,0) expanded by halfWidth + // min: (-1, -1, -1), max: (6, 1, 1) + expect(trailRenderer.bounds.min.x).to.closeTo(-halfWidth, 0.01); + expect(trailRenderer.bounds.min.y).to.closeTo(-halfWidth, 0.01); + expect(trailRenderer.bounds.min.z).to.closeTo(-halfWidth, 0.01); + expect(trailRenderer.bounds.max.x).to.closeTo(5 + halfWidth, 0.01); + expect(trailRenderer.bounds.max.y).to.closeTo(halfWidth, 0.01); + expect(trailRenderer.bounds.max.z).to.closeTo(halfWidth, 0.01); + + // Move entity to (5, 3, 0) and update + trailEntity.transform.position = new Vector3(5, 3, 0); + + // Bounds should encompass all points: (0,0,0), (5,0,0), (5,3,0) + // min: (-1, -1, -1), max: (6, 4, 1) + expect(trailRenderer.bounds.min.x).to.closeTo(-halfWidth, 0.01); + expect(trailRenderer.bounds.min.y).to.closeTo(-halfWidth, 0.01); + expect(trailRenderer.bounds.min.z).to.closeTo(-halfWidth, 0.01); + expect(trailRenderer.bounds.max.x).to.closeTo(5 + halfWidth, 0.01); + expect(trailRenderer.bounds.max.y).to.closeTo(3 + halfWidth, 0.01); + expect(trailRenderer.bounds.max.z).to.closeTo(halfWidth, 0.01); + + // Test width change affects bounds + trailRenderer.width = 4.0; + const newHalfWidth = 2.0; + trailEntity.transform.position = new Vector3(5, 4, 0); + + // Bounds with new halfWidth (2.0), encompass (0,0,0) to (5,4,0) + // min: (-2, -2, -2), max: (7, 6, 2) + expect(trailRenderer.bounds.min.x).to.closeTo(-newHalfWidth, 0.01); + expect(trailRenderer.bounds.min.y).to.closeTo(-newHalfWidth, 0.01); + expect(trailRenderer.bounds.min.z).to.closeTo(-newHalfWidth, 0.01); + expect(trailRenderer.bounds.max.x).to.closeTo(5 + newHalfWidth, 0.01); + expect(trailRenderer.bounds.max.y).to.closeTo(4 + newHalfWidth, 0.01); + expect(trailRenderer.bounds.max.z).to.closeTo(newHalfWidth, 0.01); + }); + }); + + describe("TrailMaterial", () => { + it("Constructor", () => { + const material = new TrailMaterial(engine); + + expect(material instanceof TrailMaterial).to.eq(true); + expect(material.destroyed).to.eq(false); + }); + + it("set baseColor", () => { + const material = new TrailMaterial(engine); + const color = new Color(1, 0, 0, 1); + + material.baseColor = color; + expect(Color.equals(material.baseColor, color)).to.eq(true); + }); + + it("set blendMode", () => { + const material = new TrailMaterial(engine); + + material.blendMode = BlendMode.Additive; + expect(material.blendMode).to.eq(BlendMode.Additive); + + material.blendMode = BlendMode.Normal; + expect(material.blendMode).to.eq(BlendMode.Normal); + }); + + it("clone", () => { + const material = new TrailMaterial(engine); + material.baseColor = new Color(1, 0, 0, 1); + material.blendMode = BlendMode.Additive; + + const clonedMaterial = material.clone(); + + expect(clonedMaterial instanceof TrailMaterial).to.eq(true); + expect(clonedMaterial).not.to.eq(material); + expect(Color.equals(clonedMaterial.baseColor, material.baseColor)).to.eq(true); + expect(clonedMaterial.blendMode).to.eq(material.blendMode); + }); + + it("destroy", () => { + const material = new TrailMaterial(engine); + + material.destroy(); + expect(material.destroyed).to.eq(true); + }); + }); + + describe("TrailTextureMode", () => { + it("enum values", () => { + expect(TrailTextureMode.Stretch).to.eq(0); + expect(TrailTextureMode.Tile).to.eq(1); + }); + }); +}); From 18b6ef1e0c161fba64d0a49ba9c095f051235635 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Fri, 2 Jan 2026 02:10:11 +0800 Subject: [PATCH 68/85] refactor: format code for better readability in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 05c446cea7..89388705fe 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -194,7 +194,8 @@ export class TrailRenderer extends Renderer { if (activeCount > 0) { const headIndex = (this._firstFreeElement - 1 + capacity) % capacity; timeDistParams.z = vertices[headIndex * TrailRenderer.POINT_FLOAT_STRIDE + TrailRenderer.DISTANCE_OFFSET]; - timeDistParams.w = vertices[this._firstActiveElement * TrailRenderer.POINT_FLOAT_STRIDE + TrailRenderer.DISTANCE_OFFSET]; + timeDistParams.w = + vertices[this._firstActiveElement * TrailRenderer.POINT_FLOAT_STRIDE + TrailRenderer.DISTANCE_OFFSET]; } else { timeDistParams.z = timeDistParams.w = 0; } From 1b3bbc6e550ff983a144a159379d7c0066647919 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Fri, 2 Jan 2026 02:29:55 +0800 Subject: [PATCH 69/85] refactor: enhance TrailRenderer with multiple artistic trails and movement effects --- e2e/case/trailRenderer-basic.ts | 273 ++++++++++++++---- .../originImage/Trail_trailRenderer-basic.jpg | 4 +- 2 files changed, 224 insertions(+), 53 deletions(-) diff --git a/e2e/case/trailRenderer-basic.ts b/e2e/case/trailRenderer-basic.ts index 4e3c0a3fbc..d816d7cf3c 100644 --- a/e2e/case/trailRenderer-basic.ts +++ b/e2e/case/trailRenderer-basic.ts @@ -5,6 +5,7 @@ import { AssetType, BlendMode, + BloomEffect, Camera, Color, CurveKey, @@ -13,8 +14,11 @@ import { Logger, ParticleCurve, ParticleGradient, + PostProcess, Script, Texture2D, + TonemappingEffect, + TonemappingMode, TrailMaterial, TrailRenderer, Vector3, @@ -22,6 +26,143 @@ import { } from "@galacean/engine"; import { initScreenshot, updateForE2E } from "./.mockForE2E"; +/** + * Trail configuration interface. + */ +interface TrailConfig { + color1: Color; + color2: Color; + color3: Color; + emissive: Color; + width: number; + time: number; + speed: number; + radius: number; + freqX: number; + freqY: number; + freqZ: number; + phaseOffset: number; + verticalAmp: number; +} + +/** + * Neon aurora trail configurations. + */ +const trailConfigs: TrailConfig[] = [ + // Neon Cyan-Magenta trail (main) + { + color1: new Color(0, 1, 1, 1), + color2: new Color(1, 0, 1, 1), + color3: new Color(0.5, 0, 1, 1), + emissive: new Color(0.8, 1.2, 2.0, 1), + width: 0.7, + time: 3.0, + speed: 1.5, + radius: 5, + freqX: 1, + freqY: 1.618, + freqZ: 0.618, + phaseOffset: 0, + verticalAmp: 3 + }, + // Solar Flare - warm orange/gold + { + color1: new Color(1, 0.8, 0, 1), + color2: new Color(1, 0.4, 0, 1), + color3: new Color(1, 0.2, 0.3, 1), + emissive: new Color(1.8, 1.0, 0.4, 1), + width: 0.55, + time: 2.5, + speed: 2.0, + radius: 4, + freqX: 1.414, + freqY: 0.707, + freqZ: 1.236, + phaseOffset: Math.PI / 4, + verticalAmp: 2.5 + }, + // Electric Blue + { + color1: new Color(0.3, 0.6, 1, 1), + color2: new Color(0, 0.8, 1, 1), + color3: new Color(0.5, 0.3, 1, 1), + emissive: new Color(0.6, 1.2, 2.0, 1), + width: 0.45, + time: 2.0, + speed: 2.8, + radius: 3.5, + freqX: 0.866, + freqY: 1.5, + freqZ: 1.732, + phaseOffset: Math.PI / 2, + verticalAmp: 2 + }, + // Mystic Purple-Pink + { + color1: new Color(1, 0.3, 0.6, 1), + color2: new Color(0.8, 0, 1, 1), + color3: new Color(0.5, 0.3, 1, 1), + emissive: new Color(1.5, 0.6, 1.5, 1), + width: 0.6, + time: 2.8, + speed: 1.4, + radius: 5.5, + freqX: 0.618, + freqY: 1.272, + freqZ: 0.809, + phaseOffset: (Math.PI * 3) / 4, + verticalAmp: 2.8 + }, + // Emerald Aurora - green + { + color1: new Color(0.2, 1, 0.5, 1), + color2: new Color(0, 1, 0.8, 1), + color3: new Color(0.3, 0.8, 1, 1), + emissive: new Color(0.6, 1.8, 1.0, 1), + width: 0.5, + time: 2.3, + speed: 1.8, + radius: 4.5, + freqX: 1.272, + freqY: 0.5, + freqZ: 1.414, + phaseOffset: Math.PI, + verticalAmp: 2.2 + }, + // White Core - bright center + { + color1: new Color(1, 1, 1, 1), + color2: new Color(0.9, 0.9, 1, 1), + color3: new Color(0.7, 0.8, 1, 1), + emissive: new Color(2.0, 2.0, 2.5, 1), + width: 0.35, + time: 1.5, + speed: 3.2, + radius: 2.5, + freqX: 2, + freqY: 1, + freqZ: 1.5, + phaseOffset: Math.PI / 6, + verticalAmp: 1.5 + }, + // Deep Space Purple - outer spiral + { + color1: new Color(0.6, 0, 1, 1), + color2: new Color(0.3, 0, 1, 1), + color3: new Color(0.2, 0.2, 0.8, 1), + emissive: new Color(1.0, 0.4, 1.8, 1), + width: 0.65, + time: 3.5, + speed: 1.2, + radius: 6, + freqX: 0.5, + freqY: 0.809, + freqZ: 0.618, + phaseOffset: (Math.PI * 5) / 4, + verticalAmp: 3.5 + } +]; + // Create engine WebGLEngine.create({ canvas: "canvas" @@ -31,7 +172,7 @@ WebGLEngine.create({ const scene = engine.sceneManager.activeScene; const rootEntity = scene.createRootEntity(); - scene.background.solidColor = new Color(0.1, 0.1, 0.1, 1); + scene.background.solidColor = new Color(0, 0, 0, 1); // Create camera const cameraEntity = rootEntity.createChild("camera"); @@ -40,64 +181,70 @@ WebGLEngine.create({ const camera = cameraEntity.addComponent(Camera); camera.fieldOfView = 60; - // Create trail entity - const trailEntity = rootEntity.createChild("trail"); - trailEntity.transform.position = new Vector3(0, 0, 0); - - // Add TrailRenderer component - const trail = trailEntity.addComponent(TrailRenderer); - const material = new TrailMaterial(engine); - material.blendMode = BlendMode.Additive; - trail.setMaterial(material); - trail.time = 2.0; - trail.width = 0.5; - trail.minVertexDistance = 0.2; - - // Setup width curve (taper from head to tail) - trail.widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 0)); - - // Setup color gradient (orange to blue with fade out) - const gradient = new ParticleGradient( - [ - new GradientColorKey(0, new Color(1, 0.5, 0, 1)), - new GradientColorKey(0.5, new Color(1, 0, 0.5, 1)), - new GradientColorKey(1, new Color(0, 0.5, 1, 1)) - ], - [new GradientAlphaKey(0, 1), new GradientAlphaKey(0.7, 0.8), new GradientAlphaKey(1, 0)] - ); - trail.colorGradient = gradient; - - // Add movement script - class MoveScript extends Script { - private _time = 0; - private _radius = 4; - private _speed = 2; - private _verticalSpeed = 1.5; - - onUpdate(deltaTime: number): void { - this._time += deltaTime; - const t = this._time * this._speed; - - // Lissajous curve movement - const x = Math.sin(t) * this._radius; - const y = Math.sin(t * this._verticalSpeed) * 2; - const z = Math.cos(t * 0.7) * this._radius; - - this.entity.transform.position.set(x, y, z); - } - } + // // Enable post-processing with Bloom effect + // camera.enablePostProcess = true; + // camera.enableHDR = true; + + // const postProcessEntity = rootEntity.createChild("PostProcess"); + // const postProcess = postProcessEntity.addComponent(PostProcess); + + // // Add Bloom effect for glowing trails + // const bloomEffect = postProcess.addEffect(BloomEffect); + // bloomEffect.threshold.value = 0.35; + // bloomEffect.intensity.value = 2.2; + // bloomEffect.scatter.value = 0.75; + + // // Add Tonemapping for better HDR rendering + // const tonemappingEffect = postProcess.addEffect(TonemappingEffect); + // tonemappingEffect.mode.value = TonemappingMode.ACES; + + // Store all trail materials for texture assignment + const trailMaterials: TrailMaterial[] = []; - trailEntity.addComponent(MoveScript); + // Create multiple artistic trails + trailConfigs.forEach((config, index) => { + const trailEntity = rootEntity.createChild(`trail_${index}`); - // Load trail texture + const trail = trailEntity.addComponent(TrailRenderer); + const material = new TrailMaterial(engine); + material.blendMode = BlendMode.Additive; + 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)); + + // Color gradient + const gradient = new ParticleGradient( + [ + new GradientColorKey(0, config.color1), + new GradientColorKey(0.5, config.color2), + new GradientColorKey(1, config.color3) + ], + [new GradientAlphaKey(0, 1), new GradientAlphaKey(0.6, 0.7), new GradientAlphaKey(1, 0)] + ); + trail.colorGradient = gradient; + + // Add movement + const moveScript = trailEntity.addComponent(TrailMoveScript); + moveScript.config = config; + }); + + // Load trail texture and apply to all trails engine.resourceManager .load({ url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*-DEWQZ0ncrEAAAAASTAAAAgAeil6AQ/original", type: AssetType.Texture2D }) .then((texture) => { - // Set texture on trail material - material.baseTexture = texture; + // Set texture on all trail materials + trailMaterials.forEach((material) => { + material.baseTexture = texture; + }); // engine.run(); @@ -106,3 +253,27 @@ WebGLEngine.create({ initScreenshot(engine, camera); }); }); + +/** + * Movement script with spiral and pulse effects. + */ +class TrailMoveScript extends Script { + config: TrailConfig; + private _time = 0; + + onUpdate(deltaTime: number): void { + this._time += deltaTime; + const { speed, radius, freqX, freqY, freqZ, phaseOffset, verticalAmp } = this.config; + const t = this._time * speed + phaseOffset; + + // Spiral pulsing effect + const pulseRadius = radius * (1 + Math.sin(t * 0.3) * 0.15); + + // 3D Lissajous curve with spiral modulation + const x = Math.sin(t * freqX) * pulseRadius; + const y = Math.sin(t * freqY) * verticalAmp + Math.sin(t * 0.5) * 0.5; + const z = Math.cos(t * freqZ) * pulseRadius; + + this.entity.transform.position.set(x, y, z); + } +} diff --git a/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg b/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg index dba9e1b8c4..94ba739000 100644 --- a/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg +++ b/e2e/fixtures/originImage/Trail_trailRenderer-basic.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:effe61b1e2ed57bb38c25477e9f079c9d4de658d9d1e52466fae089749e95858 -size 19407 +oid sha256:b51b7d04cf5e0c04fe6f5bb333b96a1dc0b4fa903cb74730ccf10fe76e4c1804 +size 61042 From c9051005d096c91c681484d930acb6cb2f480424 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Fri, 2 Jan 2026 03:32:54 +0800 Subject: [PATCH 70/85] refactor: improve vertex buffer management in TrailRenderer for better performance and stability --- packages/core/src/trail/TrailRenderer.ts | 59 +++++++++++++++--------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 89388705fe..6700ddaf5a 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -12,6 +12,7 @@ import { VertexElement } from "../graphic/VertexElement"; import { BufferBindFlag } from "../graphic/enums/BufferBindFlag"; import { BufferUsage } from "../graphic/enums/BufferUsage"; import { MeshTopology } from "../graphic/enums/MeshTopology"; +import { SetDataOptions } from "../graphic/enums/SetDataOptions"; import { VertexElementFormat } from "../graphic/enums/VertexElementFormat"; import { Material } from "../material/Material"; import { CurveKey, ParticleCurve } from "../particle/modules/ParticleCurve"; @@ -181,8 +182,13 @@ export class TrailRenderer extends Renderer { if (this.emitting) { this._emitNewPoint(playTime); } - if (this._firstNewElement !== this._firstFreeElement || this._vertexBuffer.isContentLost) { - this._uploadNewVertices(); + // Add active points to vertex buffer when has new points or buffer state changed + if ( + this._firstNewElement !== this._firstFreeElement || + this._bufferResized || + this._vertexBuffer.isContentLost + ) { + this._addActivePointsToVertexBuffer(); } const shaderData = this.shaderData; @@ -505,41 +511,52 @@ export class TrailRenderer extends Renderer { return firstFree >= firstActive ? firstFree - firstActive : capacity - firstActive + firstFree; } - private _uploadNewVertices(): void { + private _addActivePointsToVertexBuffer(): void { const { _firstActiveElement: firstActive, _firstFreeElement: firstFree, _vertexBuffer: buffer } = this; - const needFullUpload = buffer.isContentLost || this._bufferResized; - const firstNew = needFullUpload ? firstActive : this._firstNewElement; - this._bufferResized = false; - if (firstNew === firstFree) return; + if (firstActive === firstFree) return; const pointFloatStride = TrailRenderer.POINT_FLOAT_STRIDE; const pointByteStride = TrailRenderer.POINT_BYTE_STRIDE; - const { buffer: vertexData } = this._vertices; const capacity = this._currentPointCapacity; - const wrapped = firstNew >= firstFree; - - // First segment: wrapped includes bridge (+1 point), non-wrapped ends at firstFree - const endPoint = wrapped ? capacity + 1 : firstFree; - buffer.setData( - new Float32Array(vertexData, firstNew * pointFloatStride * 4, (endPoint - firstNew) * pointFloatStride), - firstNew * pointByteStride - ); + const wrapped = firstActive > firstFree; + // Use Discard mode (buffer orphaning) to avoid GPU sync stalls + // Upload all active vertices as a full buffer update if (wrapped) { + // Wrapped case: [firstActive -> capacity+bridge] + [0 -> firstFree] + // First segment includes bridge point + buffer.setData( + new Float32Array(this._vertices.buffer, firstActive * pointFloatStride * 4, (capacity + 1 - firstActive) * pointFloatStride), + firstActive * pointByteStride, + 0, + undefined, + SetDataOptions.Discard + ); // Second segment if (firstFree > 0) { - buffer.setData(new Float32Array(vertexData, 0, firstFree * pointFloatStride), 0); + buffer.setData(new Float32Array(this._vertices.buffer, 0, firstFree * pointFloatStride), 0); } - } else if (firstNew === 0) { - // Upload bridge separately if point 0 was updated + } else { + // Non-wrapped case: [firstActive -> firstFree] buffer.setData( - new Float32Array(vertexData, capacity * pointFloatStride * 4, pointFloatStride), - capacity * pointByteStride + new Float32Array(this._vertices.buffer, firstActive * pointFloatStride * 4, (firstFree - firstActive) * pointFloatStride), + firstActive * pointByteStride, + 0, + undefined, + SetDataOptions.Discard ); + // Upload bridge if point 0 is active + if (firstActive === 0) { + buffer.setData( + new Float32Array(this._vertices.buffer, capacity * pointFloatStride * 4, pointFloatStride), + capacity * pointByteStride + ); + } } this._firstNewElement = firstFree; + this._bufferResized = false; } private _addSubRenderElement( From 610fcdfd1b45fba96c13640e9fe656b23d5a9244 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 4 Jan 2026 19:34:35 +0800 Subject: [PATCH 71/85] refactor: streamline vertex processing in TrailRenderer by removing expired vertex handling --- .../core/src/shaderlib/extra/trail.vs.glsl | 55 +++++++++---------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index 226eaa9d71..391bc0572b 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -27,41 +27,36 @@ void main() { float age = renderer_TimeDistParams.x - birthTime; float normalizedAge = age / renderer_TimeDistParams.y; - // Discard expired vertices - if (normalizedAge >= 1.0) { - gl_Position = vec4(2.0, 2.0, 2.0, 1.0); - } else { - // Distance-based relative position: 0=head(newest), 1=tail(oldest) - float distFromHead = renderer_TimeDistParams.z - a_Distance; - float totalDist = renderer_TimeDistParams.z - renderer_TimeDistParams.w; - float relativePos = totalDist > 0.0 ? distFromHead / totalDist : 0.0; - - // Billboard: expand perpendicular to tangent and view direction - vec3 toCamera = normalize(camera_Position - position); - vec3 right = cross(tangent, toCamera); - float rightLen = length(right); + // Distance-based relative position: 0=head(newest), 1=tail(oldest) + float distFromHead = renderer_TimeDistParams.z - a_Distance; + float totalDist = renderer_TimeDistParams.z - renderer_TimeDistParams.w; + float relativePos = totalDist > 0.0 ? distFromHead / totalDist : 0.0; + + // Billboard: expand perpendicular to tangent and view direction + vec3 toCamera = normalize(camera_Position - position); + vec3 right = cross(tangent, toCamera); + float rightLen = length(right); + if (rightLen < 0.001) { + right = cross(tangent, vec3(0.0, 1.0, 0.0)); + rightLen = length(right); if (rightLen < 0.001) { - right = cross(tangent, vec3(0.0, 1.0, 0.0)); + right = cross(tangent, vec3(1.0, 0.0, 0.0)); rightLen = length(right); - if (rightLen < 0.001) { - right = cross(tangent, vec3(1.0, 0.0, 0.0)); - rightLen = length(right); - } } - right = right / rightLen; + } + right = right / rightLen; - float widthMultiplier = evaluateParticleCurve(renderer_WidthCurve, relativePos); - float width = renderer_TrailParams.x * widthMultiplier; - vec3 worldPosition = position + right * width * 0.5 * corner; + float widthMultiplier = evaluateParticleCurve(renderer_WidthCurve, relativePos); + float width = renderer_TrailParams.x * widthMultiplier; + vec3 worldPosition = position + right * width * 0.5 * corner; - gl_Position = camera_ProjMat * camera_ViewMat * vec4(worldPosition, 1.0); + 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); + // 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); - v_color = evaluateParticleGradient(renderer_ColorKeys, renderer_GradientMaxTime.x, renderer_AlphaKeys, renderer_GradientMaxTime.y, relativePos); - } + v_color = evaluateParticleGradient(renderer_ColorKeys, renderer_GradientMaxTime.x, renderer_AlphaKeys, renderer_GradientMaxTime.y, relativePos); } From 768042bd7f2b4d44f2e5244e60e40e96325eddff Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 4 Jan 2026 21:33:26 +0800 Subject: [PATCH 72/85] refactor: rename gradientMaxTime to curveMaxTime for consistency in TrailRenderer --- packages/core/src/shaderlib/extra/trail.vs.glsl | 6 +++--- packages/core/src/trail/TrailRenderer.ts | 16 ++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index 391bc0572b..2dc5087983 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -10,7 +10,7 @@ uniform mat4 camera_ProjMat; uniform vec2 renderer_WidthCurve[4]; // Width curve (4 keyframes max: x=time, y=value) uniform vec4 renderer_ColorKeys[4]; // Color gradient (x=time, yzw=rgb) uniform vec2 renderer_AlphaKeys[4]; // Alpha gradient (x=time, y=alpha) -uniform vec4 renderer_GradientMaxTime; // x: colorMaxTime, y: alphaMaxTime +uniform vec4 renderer_CurveMaxTime; // x: colorMaxTime, y: alphaMaxTime, z: widthMaxTime varying vec2 v_uv; varying vec4 v_color; @@ -46,7 +46,7 @@ void main() { } right = right / rightLen; - float widthMultiplier = evaluateParticleCurve(renderer_WidthCurve, relativePos); + float widthMultiplier = evaluateParticleCurve(renderer_WidthCurve, min(relativePos, renderer_CurveMaxTime.z)); float width = renderer_TrailParams.x * widthMultiplier; vec3 worldPosition = position + right * width * 0.5 * corner; @@ -58,5 +58,5 @@ void main() { float v = renderer_TrailParams.y == 0.0 ? relativePos : distFromHead; v_uv = vec2(u, v * renderer_TrailParams.z); - v_color = evaluateParticleGradient(renderer_ColorKeys, renderer_GradientMaxTime.x, renderer_AlphaKeys, renderer_GradientMaxTime.y, relativePos); + v_color = evaluateParticleGradient(renderer_ColorKeys, renderer_CurveMaxTime.x, renderer_AlphaKeys, renderer_CurveMaxTime.y, relativePos); } diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 6700ddaf5a..dba62111b1 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -37,7 +37,7 @@ export class TrailRenderer extends Renderer { private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); - private static _gradientMaxTimeProp = ShaderProperty.getByName("renderer_GradientMaxTime"); + private static _curveMaxTimeProp = ShaderProperty.getByName("renderer_CurveMaxTime"); private static _tempVector3 = new Vector3(); @@ -64,7 +64,7 @@ export class TrailRenderer extends Renderer { @ignoreClone private _timeDistParams = new Vector4(0, 5.0, 0, 0); // x: currentTime, y: lifetime, z: headDistance, w: tailDistance @ignoreClone - private _gradientMaxTime = new Vector4(); // x: colorMaxTime, y: alphaMaxTime + private _curveMaxTime = new Vector4(); // x: colorMaxTime, y: alphaMaxTime, z: widthMaxTime // Geometry and rendering @ignoreClone @@ -216,10 +216,14 @@ export class TrailRenderer extends Renderer { const colorKeys = colorGradient.colorKeys; const alphaKeys = colorGradient.alphaKeys; - const gradientMaxTime = this._gradientMaxTime; - gradientMaxTime.x = colorKeys.length ? colorKeys[colorKeys.length - 1].time : 0; - gradientMaxTime.y = alphaKeys.length ? alphaKeys[alphaKeys.length - 1].time : 0; - shaderData.setVector4(TrailRenderer._gradientMaxTimeProp, gradientMaxTime); + const widthKeys = this.widthCurve.keys; + this._curveMaxTime.set( + colorKeys.length ? colorKeys[colorKeys.length - 1].time : 0, + alphaKeys.length ? alphaKeys[alphaKeys.length - 1].time : 0, + widthKeys.length ? widthKeys[widthKeys.length - 1].time : 0, + 0 + ); + shaderData.setVector4(TrailRenderer._curveMaxTimeProp, this._curveMaxTime); } protected override _render(context: RenderContext): void { From 40adcb8f11c5e87584794d828a6d0cb77c8c0b5b Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 4 Jan 2026 23:43:32 +0800 Subject: [PATCH 73/85] refactor: enhance buffer handling in TrailRenderer to improve wrap-around logic --- packages/core/src/trail/TrailRenderer.ts | 29 ++++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index dba62111b1..35f1c6144d 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -241,11 +241,17 @@ export class TrailRenderer extends Renderer { const renderElement = this._engine._renderElementPool.get(); renderElement.set(this.priority, this._distanceForSort); - const wrapped = firstActive > firstFree; - const mainCount = (wrapped ? this._currentPointCapacity - firstActive + 1 : firstFree - firstActive) * 2; + // spansBoundary: active points cross buffer end + // wrapped: spansBoundary AND point 0 has been written (need bridge + second segment) + const spansBoundary = firstActive > firstFree; + const wrapped = spansBoundary && firstFree > 0; + const mainCount = + (spansBoundary + ? this._currentPointCapacity - firstActive + (wrapped ? 1 : 0) + : firstFree - firstActive) * 2; this._addSubRenderElement(renderElement, material, this._mainSubPrimitive, firstActive * 2, mainCount); - if (wrapped && firstFree > 0) { + if (wrapped) { this._addSubRenderElement(renderElement, material, this._wrapSubPrimitive, 0, firstFree * 2); } @@ -491,7 +497,7 @@ export class TrailRenderer extends Renderer { tangent.copyToArray(vertices, bottomOffset + 5); vertices[bottomOffset + distOffset] = cumulativeDist; - // Write to bridge position when writing point 0 + // Write to bridge position when writing point 0 (bridge = copy of point 0 to connect wrap-around) if (pointIndex === 0) { const bridgeTopOffset = capacity * pointStride; const bridgeBottomOffset = bridgeTopOffset + floatStride; @@ -523,21 +529,24 @@ export class TrailRenderer extends Renderer { const pointFloatStride = TrailRenderer.POINT_FLOAT_STRIDE; const pointByteStride = TrailRenderer.POINT_BYTE_STRIDE; const capacity = this._currentPointCapacity; - const wrapped = firstActive > firstFree; + // spansBoundary: active points cross buffer end + // wrapped: spansBoundary AND point 0 has been written (bridge is valid) + const spansBoundary = firstActive > firstFree; + const wrapped = spansBoundary && firstFree > 0; // Use Discard mode (buffer orphaning) to avoid GPU sync stalls // Upload all active vertices as a full buffer update - if (wrapped) { - // Wrapped case: [firstActive -> capacity+bridge] + [0 -> firstFree] - // First segment includes bridge point + if (spansBoundary) { + // First segment: [firstActive -> capacity], +bridge only if wrapped (point 0 was written) + const firstSegmentPoints = capacity - firstActive + (wrapped ? 1 : 0); buffer.setData( - new Float32Array(this._vertices.buffer, firstActive * pointFloatStride * 4, (capacity + 1 - firstActive) * pointFloatStride), + new Float32Array(this._vertices.buffer, firstActive * pointFloatStride * 4, firstSegmentPoints * pointFloatStride), firstActive * pointByteStride, 0, undefined, SetDataOptions.Discard ); - // Second segment + // Second segment: [0 -> firstFree] if (firstFree > 0) { buffer.setData(new Float32Array(this._vertices.buffer, 0, firstFree * pointFloatStride), 0); } From 3818ceb2263e78e7751668aa28e6bf55cf63e273 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sun, 4 Jan 2026 23:53:52 +0800 Subject: [PATCH 74/85] refactor: simplify conditional checks and formatting in TrailRenderer for improved readability --- packages/core/src/trail/TrailRenderer.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 35f1c6144d..9e4d275ffe 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -183,11 +183,7 @@ export class TrailRenderer extends Renderer { this._emitNewPoint(playTime); } // Add active points to vertex buffer when has new points or buffer state changed - if ( - this._firstNewElement !== this._firstFreeElement || - this._bufferResized || - this._vertexBuffer.isContentLost - ) { + if (this._firstNewElement !== this._firstFreeElement || this._bufferResized || this._vertexBuffer.isContentLost) { this._addActivePointsToVertexBuffer(); } @@ -246,9 +242,7 @@ export class TrailRenderer extends Renderer { const spansBoundary = firstActive > firstFree; const wrapped = spansBoundary && firstFree > 0; const mainCount = - (spansBoundary - ? this._currentPointCapacity - firstActive + (wrapped ? 1 : 0) - : firstFree - firstActive) * 2; + (spansBoundary ? this._currentPointCapacity - firstActive + (wrapped ? 1 : 0) : firstFree - firstActive) * 2; this._addSubRenderElement(renderElement, material, this._mainSubPrimitive, firstActive * 2, mainCount); if (wrapped) { @@ -540,7 +534,11 @@ export class TrailRenderer extends Renderer { // First segment: [firstActive -> capacity], +bridge only if wrapped (point 0 was written) const firstSegmentPoints = capacity - firstActive + (wrapped ? 1 : 0); buffer.setData( - new Float32Array(this._vertices.buffer, firstActive * pointFloatStride * 4, firstSegmentPoints * pointFloatStride), + new Float32Array( + this._vertices.buffer, + firstActive * pointFloatStride * 4, + firstSegmentPoints * pointFloatStride + ), firstActive * pointByteStride, 0, undefined, @@ -553,7 +551,11 @@ export class TrailRenderer extends Renderer { } else { // Non-wrapped case: [firstActive -> firstFree] buffer.setData( - new Float32Array(this._vertices.buffer, firstActive * pointFloatStride * 4, (firstFree - firstActive) * pointFloatStride), + new Float32Array( + this._vertices.buffer, + firstActive * pointFloatStride * 4, + (firstFree - firstActive) * pointFloatStride + ), firstActive * pointByteStride, 0, undefined, From 58fda34c9f6099031d142b298e4b565853dc5925 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Thu, 22 Jan 2026 16:08:38 +0800 Subject: [PATCH 75/85] refactor: optimize right vector calculation in TrailRenderer for improved performance --- packages/core/src/shaderlib/extra/trail.vs.glsl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index 2dc5087983..0853d0caa9 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -35,16 +35,16 @@ void main() { // Billboard: expand perpendicular to tangent and view direction vec3 toCamera = normalize(camera_Position - position); vec3 right = cross(tangent, toCamera); - float rightLen = length(right); - if (rightLen < 0.001) { + float rightLenSq = dot(right, right); + if (rightLenSq < 0.000001) { right = cross(tangent, vec3(0.0, 1.0, 0.0)); - rightLen = length(right); - if (rightLen < 0.001) { + rightLenSq = dot(right, right); + if (rightLenSq < 0.000001) { right = cross(tangent, vec3(1.0, 0.0, 0.0)); - rightLen = length(right); + rightLenSq = dot(right, right); } } - right = right / rightLen; + right = right * inversesqrt(rightLenSq); float widthMultiplier = evaluateParticleCurve(renderer_WidthCurve, min(relativePos, renderer_CurveMaxTime.z)); float width = renderer_TrailParams.x * widthMultiplier; From 1e7cd8b671cc0ab8eddc4660416457e4818faffe Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 24 Jan 2026 15:29:21 +0800 Subject: [PATCH 76/85] refactor: clarify minVertexDistance comment to specify units in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 9e4d275ffe..f8852fe102 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -44,7 +44,7 @@ export class TrailRenderer extends Renderer { /** Whether the trail is being created as the object moves. */ emitting = true; - /** The minimum distance the object must move before a new trail segment is added. */ + /** The minimum distance the object must move before a new trail segment is added, in world units. */ minVertexDistance = 0.1; /** The curve describing the trail width from start to end. */ From 1520daa667e86ff4d8f8f02d89956ef88290a012 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 24 Jan 2026 15:39:09 +0800 Subject: [PATCH 77/85] refactor: update clone decorators for trail parameters to deepClone in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index f8852fe102..dc7e58b191 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -59,9 +59,9 @@ export class TrailRenderer extends Renderer { ); // Shader parameters - @ignoreClone + @deepClone private _trailParams = new Vector4(1.0, TrailTextureMode.Stretch, 1.0, 0); // x: width, y: textureMode, z: textureScale - @ignoreClone + @deepClone private _timeDistParams = new Vector4(0, 5.0, 0, 0); // x: currentTime, y: lifetime, z: headDistance, w: tailDistance @ignoreClone private _curveMaxTime = new Vector4(); // x: colorMaxTime, y: alphaMaxTime, z: widthMaxTime From 47a14d73ad910386bc65af017031eff13337ebd1 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 24 Jan 2026 16:07:40 +0800 Subject: [PATCH 78/85] refactor: calculate half width using max width multiplier from widthCurve in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index dc7e58b191..e7b861d480 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -295,7 +295,16 @@ export class TrailRenderer extends Renderer { // Only expand by half width when there's actual/upcoming trail geometry if (hasTrailGeometry) { - const halfWidth = this.width * 0.5; + // Find max width multiplier from widthCurve + let maxWidthMultiplier = 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; + } + } + const halfWidth = this.width * maxWidthMultiplier * 0.5; min.set(min.x - halfWidth, min.y - halfWidth, min.z - halfWidth); max.set(max.x + halfWidth, max.y + halfWidth, max.z + halfWidth); } From 122ff01bf9d1a8cc1cbef576243c3920d8b781a2 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 24 Jan 2026 16:18:59 +0800 Subject: [PATCH 79/85] refactor: improve boundary handling in point merging logic in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index e7b861d480..58820863e7 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -267,8 +267,9 @@ export class TrailRenderer extends Renderer { min.set(Infinity, Infinity, Infinity); max.set(-Infinity, -Infinity, -Infinity); - const wrapped = firstActive > firstFree; - for (let i = firstActive, end = wrapped ? this._currentPointCapacity : firstFree; i < end; i++) { + const spansBoundary = firstActive > firstFree; + const wrapped = spansBoundary && firstFree > 0; + for (let i = firstActive, end = spansBoundary ? this._currentPointCapacity : firstFree; i < end; i++) { this._mergePointPosition(vertices, i * pointStride, min, max); } if (wrapped) { From 2ebcbc7499d9c6dbfb9139f3328f2a27845887bf Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 24 Jan 2026 16:24:49 +0800 Subject: [PATCH 80/85] refactor: add dirty flag update for retired points in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 58820863e7..aa41d82f42 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -2,7 +2,7 @@ import { BoundingBox, Color, Vector3, Vector4 } from "@galacean/engine-math"; import { Entity } from "../Entity"; import { RenderContext } from "../RenderPipeline/RenderContext"; import { RenderElement } from "../RenderPipeline/RenderElement"; -import { Renderer } from "../Renderer"; +import { Renderer, RendererUpdateFlags } from "../Renderer"; import { deepClone, ignoreClone } from "../clone/CloneManager"; import { Buffer } from "../graphic/Buffer"; import { Primitive } from "../graphic/Primitive"; @@ -401,6 +401,7 @@ export class TrailRenderer extends Renderer { private _retireActivePoints(currentTime: number, frameCount: number): void { const { time: lifetime, _vertices: vertices, _currentPointCapacity: capacity } = this; const pointStride = TrailRenderer.POINT_FLOAT_STRIDE; + const firstActiveElement = this._firstActiveElement; while (this._firstActiveElement !== this._firstFreeElement) { const offset = this._firstActiveElement * pointStride + 3; @@ -413,6 +414,11 @@ export class TrailRenderer extends Renderer { vertices[offset] = frameCount; this._firstActiveElement = (this._firstActiveElement + 1) % capacity; } + + // Mark bounds dirty if any points were retired + if (this._firstActiveElement !== firstActiveElement) { + this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + } } private _freeRetiredPoints(frameCount: number): void { From 4787769b40c7226063f6666d22b9f7427e19daa4 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 24 Jan 2026 17:03:46 +0800 Subject: [PATCH 81/85] refactor: clarify comment for a_PositionBirthTime and remove unused variables in TrailRenderer --- packages/core/src/shaderlib/extra/trail.vs.glsl | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index 0853d0caa9..0650c5e7f1 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -1,4 +1,4 @@ -attribute vec4 a_PositionBirthTime; // xyz: World position, w: Birth time +attribute vec4 a_PositionBirthTime; // xyz: World position, w: Birth time (used by CPU only) attribute vec4 a_CornerTangent; // x: Corner (-1 or 1), yzw: Tangent direction attribute float a_Distance; // Absolute cumulative distance (written once per point) @@ -19,14 +19,9 @@ varying vec4 v_color; void main() { vec3 position = a_PositionBirthTime.xyz; - float birthTime = a_PositionBirthTime.w; float corner = a_CornerTangent.x; vec3 tangent = a_CornerTangent.yzw; - // age: time since birth, normalizedAge: 0=new, 1=expired - float age = renderer_TimeDistParams.x - birthTime; - float normalizedAge = age / renderer_TimeDistParams.y; - // Distance-based relative position: 0=head(newest), 1=tail(oldest) float distFromHead = renderer_TimeDistParams.z - a_Distance; float totalDist = renderer_TimeDistParams.z - renderer_TimeDistParams.w; From 698d26a0f751521c34ccfc8ef1a684fc64858218 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 24 Jan 2026 17:27:05 +0800 Subject: [PATCH 82/85] refactor: update distance parameters in TrailRenderer and shader to use vec2 --- .../core/src/shaderlib/extra/trail.vs.glsl | 6 ++--- packages/core/src/trail/TrailRenderer.ts | 27 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/core/src/shaderlib/extra/trail.vs.glsl b/packages/core/src/shaderlib/extra/trail.vs.glsl index 0650c5e7f1..cb5d0f461e 100644 --- a/packages/core/src/shaderlib/extra/trail.vs.glsl +++ b/packages/core/src/shaderlib/extra/trail.vs.glsl @@ -3,7 +3,7 @@ attribute vec4 a_CornerTangent; // x: Corner (-1 or 1), yzw: Tangent directi 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_TimeDistParams; // x: CurrentTime, y: Lifetime, z: HeadDistance, w: TailDistance +uniform vec2 renderer_DistanceParams; // x: HeadDistance, y: TailDistance uniform vec3 camera_Position; uniform mat4 camera_ViewMat; uniform mat4 camera_ProjMat; @@ -23,8 +23,8 @@ void main() { vec3 tangent = a_CornerTangent.yzw; // Distance-based relative position: 0=head(newest), 1=tail(oldest) - float distFromHead = renderer_TimeDistParams.z - a_Distance; - float totalDist = renderer_TimeDistParams.z - renderer_TimeDistParams.w; + float distFromHead = renderer_DistanceParams.x - a_Distance; + float totalDist = renderer_DistanceParams.x - renderer_DistanceParams.y; float relativePos = totalDist > 0.0 ? distFromHead / totalDist : 0.0; // Billboard: expand perpendicular to tangent and view direction diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index aa41d82f42..4ee8de3d27 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -1,4 +1,4 @@ -import { BoundingBox, Color, Vector3, Vector4 } from "@galacean/engine-math"; +import { BoundingBox, Color, Vector2, Vector3, Vector4 } from "@galacean/engine-math"; import { Entity } from "../Entity"; import { RenderContext } from "../RenderPipeline/RenderContext"; import { RenderElement } from "../RenderPipeline/RenderElement"; @@ -33,7 +33,7 @@ export class TrailRenderer extends Renderer { private static readonly POINT_INCREASE_COUNT = 128; private static _trailParamsProp = ShaderProperty.getByName("renderer_TrailParams"); - private static _timeDistParamsProp = ShaderProperty.getByName("renderer_TimeDistParams"); + private static _distanceParamsProp = ShaderProperty.getByName("renderer_DistanceParams"); private static _widthCurveProp = ShaderProperty.getByName("renderer_WidthCurve"); private static _colorKeysProp = ShaderProperty.getByName("renderer_ColorKeys"); private static _alphaKeysProp = ShaderProperty.getByName("renderer_AlphaKeys"); @@ -61,11 +61,13 @@ 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 - @deepClone - private _timeDistParams = new Vector4(0, 5.0, 0, 0); // x: currentTime, y: lifetime, z: headDistance, w: tailDistance + @ignoreClone + private _distanceParams = new Vector2(); // x: headDistance, y: tailDistance @ignoreClone private _curveMaxTime = new Vector4(); // x: colorMaxTime, y: alphaMaxTime, z: widthMaxTime + private _time = 5.0; + // Geometry and rendering @ignoreClone private _primitive: Primitive; @@ -110,11 +112,11 @@ export class TrailRenderer extends Renderer { * The fade-out duration in seconds. */ get time(): number { - return this._timeDistParams.y; + return this._time; } set time(value: number) { - this._timeDistParams.y = value; + this._time = value; } /** @@ -188,22 +190,21 @@ export class TrailRenderer extends Renderer { } const shaderData = this.shaderData; - const timeDistParams = this._timeDistParams; + const distanceParams = this._distanceParams; const { _vertices: vertices, _currentPointCapacity: capacity } = this; const activeCount = this._getActivePointCount(); - timeDistParams.x = playTime; - // z: headDistance (newest point), w: tailDistance (oldest point) + // x: headDistance (newest point), y: tailDistance (oldest point) if (activeCount > 0) { const headIndex = (this._firstFreeElement - 1 + capacity) % capacity; - timeDistParams.z = vertices[headIndex * TrailRenderer.POINT_FLOAT_STRIDE + TrailRenderer.DISTANCE_OFFSET]; - timeDistParams.w = + distanceParams.x = vertices[headIndex * TrailRenderer.POINT_FLOAT_STRIDE + TrailRenderer.DISTANCE_OFFSET]; + distanceParams.y = vertices[this._firstActiveElement * TrailRenderer.POINT_FLOAT_STRIDE + TrailRenderer.DISTANCE_OFFSET]; } else { - timeDistParams.z = timeDistParams.w = 0; + distanceParams.x = distanceParams.y = 0; } shaderData.setVector4(TrailRenderer._trailParamsProp, this._trailParams); - shaderData.setVector4(TrailRenderer._timeDistParamsProp, timeDistParams); + shaderData.setVector2(TrailRenderer._distanceParamsProp, distanceParams); const { colorGradient } = this; shaderData.setFloatArray(TrailRenderer._widthCurveProp, this.widthCurve._getTypeArray()); From 55c08ee48bac5910c8cf38f742f1d1b8c393b99b Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 24 Jan 2026 17:42:23 +0800 Subject: [PATCH 83/85] refactor: update comment for engine.update() in Trail test to clarify trail point generation --- tests/src/core/Trail.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/src/core/Trail.test.ts b/tests/src/core/Trail.test.ts index f647db25c5..a246c13768 100644 --- a/tests/src/core/Trail.test.ts +++ b/tests/src/core/Trail.test.ts @@ -189,8 +189,7 @@ describe("Trail", async () => { expect(trailRenderer.bounds.min).to.deep.include({ x: 0, y: 0, z: 0 }); expect(trailRenderer.bounds.max).to.deep.include({ x: 0, y: 0, z: 0 }); - // Move entity to (5, 0, 0) - distance > minVertexDistance, creates trail point - engine.update(); //@todo:删除会触发包围盒无法更新的bug + engine.update(); // Trail generates new vertices only after engine.update(), so we need to call it first to record initial position trailEntity.transform.position = new Vector3(5, 0, 0); // Now has trail geometry, bounds should encompass (0,0,0) to (5,0,0) expanded by halfWidth From da302d916cb4ef3990a49b03ba806502f281a7dd Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 24 Jan 2026 17:47:19 +0800 Subject: [PATCH 84/85] refactor: add cumulativeDistance and playTime properties in TrailRenderer initialization --- packages/core/src/trail/TrailRenderer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 4ee8de3d27..0d53b87b31 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -169,6 +169,8 @@ export class TrailRenderer extends Renderer { this._firstFreeElement = 0; this._firstRetiredElement = 0; this._hasLastPosition = false; + this._cumulativeDistance = 0; + this._playTime = 0; } protected override _update(context: RenderContext): void { From 3dfea8bee9e2ba77db772cb2b3150430f9840613 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Sat, 24 Jan 2026 18:04:41 +0800 Subject: [PATCH 85/85] refactor: clarify fade-out duration comment in TrailRenderer --- packages/core/src/trail/TrailRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 0d53b87b31..440ba9272f 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -109,7 +109,7 @@ export class TrailRenderer extends Renderer { private _lastPlayTimeUpdateFrameCount = -1; /** - * The fade-out duration in seconds. + * How long the trail takes to fade out, in seconds. */ get time(): number { return this._time;