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

低功耗蓝牙项目,需要一块懂省电的板

思澈 SF32LB52 芯片,BLE 协议栈深度优化,上手即开发

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

低功耗蓝牙项目,需要一块懂省电的板

思澈 SF32LB52 芯片,BLE 协议栈深度优化,上手即开发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值