实现类似汽车2条尾气的效果
1.使用粒子特效Trail Module 会导致在高速情况下,粒子断层,尾部颗粒感严重,而且粒子过多会导致draw call负担
2.使用2D拖尾 MotionStreak,会在轨道扭曲的时候,有显示层级或者重叠在一起的问题
(1)应该显示4条,实际看到了1条,是因为角度问题重叠了
(2)会在模型背侧显示出来,
3.最终实现,选择Mesh渲染,+材质
1.创建材质,渐变图+builtin材质

2.实际作用的脚本,代码如下,新建一个空节点,增加Mesh Renderer组件,无需设置mesh,把刚才的材质绑定,然后把脚本绑定上,调整脚本参数,设置要跟随的target,代码中设置的是两条拖尾,left和right,可以根据需求自己修改。主要就是通过路径绘制网格渲染出来。
import {
_decorator,
Component,
Vec3,
MeshRenderer,
utils,
Node,
find,
Camera,
isValid,
} from 'cc';
const { ccclass, property } = _decorator;
@ccclass('Trail3D')
export class Trail3D extends Component {
@property({ type: Node })
target: Node = null!; // 车
@property(Camera)
camera: Camera = null!;
@property
maxPoints = 80;
@property
width = 0.3;
@property
minDistance = 0.05;
@property
maxLength = 5; // 拖尾最大长度(米)
private _points: Vec3[] = [];
private _meshRenderer: MeshRenderer = null!;
private _lastWorldPos: Vec3 = new Vec3();
private isLeft: boolean = false;
start() {
this._meshRenderer = this.getComponent(MeshRenderer)!;
if (!this.target) {
console.error('Trail3D: target 未设置');
return;
}
this._lastWorldPos.set(this.target.worldPosition);
}
setTarget(target: Node, left = false) {
this.isLeft = left;
this.target = target;
if (left) {
this._lastWorldPos.set(this.target.worldPosition.x, this.target.worldPosition.y + .4, this.target.worldPosition.z + 0.5);
}
else {
this._lastWorldPos.set(this.target.worldPosition.x, this.target.worldPosition.y + .4, this.target.worldPosition.z - 0.5);
}
}
update() {
if (!this.target || isValid(this.target) == false) {
this.node.destroy();
return;
}
let currentWorld = this.target.worldPosition.clone();
if (this.isLeft) {
currentWorld.set(this.target.worldPosition.x, this.target.worldPosition.y + .4, this.target.worldPosition.z + 0.5);
}
else {
currentWorld.set(this.target.worldPosition.x, this.target.worldPosition.y + .4, this.target.worldPosition.z - 0.5);
}
// ===== 不动就不更新(优化)=====
if (Vec3.distance(this._lastWorldPos, currentWorld) < 0.0001) {
return;
}
// ===== 距离补点(核心,解决断层)=====
const dist = Vec3.distance(this._lastWorldPos, currentWorld);
if (dist > this.minDistance) {
const count = Math.floor(dist / this.minDistance);
for (let i = 1; i <= count; i++) {
const t = i / count;
const pos = new Vec3();
Vec3.lerp(pos, this._lastWorldPos, currentWorld, t);
// ✅ 直接存世界坐标(关键)
this._points.push(pos);
}
this._lastWorldPos.set(currentWorld);
}
// ===== 按长度裁剪(核心,防止变圆)=====
let total = 0;
for (let i = this._points.length - 1; i > 0; i--) {
total += Vec3.distance(this._points[i], this._points[i - 1]);
if (total > this.maxLength) {
this._points.splice(0, i);
break;
}
}
// ===== 最大点限制(保险)=====
if (this._points.length > this.maxPoints) {
this._points.shift();
}
this._updateMesh();
}
private _updateMesh() {
if (this._points.length < 2) return;
const positions: number[] = [];
const uvs: number[] = [];
const indices: number[] = [];
const right = new Vec3();
const dir = new Vec3();
const up = new Vec3(0, 1, 0);
for (let i = 0; i < this._points.length; i++) {
const p = this._points[i];
// ===== 方向 =====
if (i === 0) {
Vec3.subtract(dir, this._points[i + 1], p);
} else {
Vec3.subtract(dir, p, this._points[i - 1]);
}
Vec3.normalize(dir, dir);
// ===== 侧方向(稳定,不翻转)=====
const camForward = this.camera.node.forward;
Vec3.cross(right, dir, camForward);
if (right.lengthSqr() < 0.0001) {
right.set(1, 0, 0);
}
Vec3.normalize(right, right);
// ===== 宽度渐变 =====
const t = i / this._points.length;
const w = this.width * t;
const left = new Vec3();
const rightPos = new Vec3();
Vec3.scaleAndAdd(left, p, right, -w);
Vec3.scaleAndAdd(rightPos, p, right, w);
positions.push(left.x, left.y, left.z);
positions.push(rightPos.x, rightPos.y, rightPos.z);
// UV 用于透明渐变
uvs.push(t, 0);
uvs.push(t, 1);
}
// ===== 索引 =====
for (let i = 0; i < this._points.length - 1; i++) {
const i0 = i * 2;
const i1 = i * 2 + 1;
const i2 = i * 2 + 2;
const i3 = i * 2 + 3;
indices.push(i0, i2, i1);
indices.push(i1, i2, i3);
}
this._rebuildMesh(positions, uvs, indices);
}
private _rebuildMesh(positions: number[], uvs: number[], indices: number[]) {
const geometry = {
positions,
uvs,
indices,
};
// ⚠️ Cocos 3.8 推荐
const mesh = utils.MeshUtils.createMesh(geometry);
this._meshRenderer.mesh = mesh;
}
}
最终效果可以在3d场景中,完全显示出所有尾气条带