Cocos3.8版本 实现跟随3d物体的条带拖尾

实现类似汽车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场景中,完全显示出所有尾气条带

相关推荐
云飞云共享云桌面2 小时前
8-10位研发3D(sw、ug、creo)画图如何共享一台工作站?
运维·服务器·网络·数据库·3d·电脑
twe77582582 小时前
“交织现实与虚拟:CCP-RIE在AR/VR工业动画中的创新展现“
科技·3d·制造·动画
陶甜也3 小时前
3D智慧城市:blender建模、骨骼、动画、VUE、threeJs引入渲染,飞行视角,涟漪、人物行走
前端·3d·vue·blender·threejs·模型
Yao.Li5 小时前
PVN3D ORT CUDA Custom Ops 实现与联调记录
人工智能·3d·具身智能
答案—answer13 小时前
ThreeFlowX接入3D体积云和谷歌3D瓦片地图
3d
七77.13 小时前
【世界模型】FLASHWORLD: HIGH-QUALITY 3D SCENE GENERATION WITHIN SECONDS
3d·世界模型
兮℡檬,19 小时前
3D点云处理(open3D)
3d
拆房老料20 小时前
开源预览引擎 BaseMetas Fileview v1.4.0 发布:PDF 渲染升级 + RAR5 修复 + 压缩包优化,企业级文档预览更强了
3d·pdf·开源·开源软件
UltraLAB-F2 天前
GPU显存不足时的分配策略:渲染与仿真的显存争夺战解决方案
图像处理·算法·3d·ai·硬件架构