three.js 汽車行駛動畫效果

来源:https://www.cnblogs.com/s0611163/archive/2023/12/06/17879849.html
-Advertisement-
Play Games

實現原理是使用TWEEN.Tween實現動畫效果 實現 汽車模型載入 使用Promise編寫模型的非同步載入方法 參數position是汽車初始位置,參數rotation是汽車初始朝向 Car.prototype.loadCar = function (position, rotation) { le ...


實現原理是使用TWEEN.Tween實現動畫效果

實現

汽車模型載入

使用Promise編寫模型的非同步載入方法
參數position是汽車初始位置,參數rotation是汽車初始朝向

Car.prototype.loadCar = function (position, rotation) {
    let onProgress = function (xhr) {

    };

    return new Promise((resolve, reject) => {
        if (!this.model) {
            let loader = new THREE.GLTFLoader();
            loader.load(this.url, gltf => {
                const model = gltf.scene || gltf.scenes[0];

                model.position.x = position.x;
                model.position.y = position.y;
                model.position.z = position.z;

                model.scale.set(0.25, 0.25, 0.25);

                model.rotation.set(rotation.x, rotation.y, rotation.z);

                this.model = model;
                this.scene.add(model);

                resolve(model);

            }, onProgress, xhr => {
                console.error(xhr);
                console.info('模型 ' + url + ' 載入失敗');
                reject(xhr);
            });
        } else {
            resolve(this.model);
        }
    });
}

調用:
第1個參數是汽車初始位置,第2個參數表示汽車初始朝向:西

await car.loadCar(positions[0], car.WEST);

汽車行駛

參數start是行駛起點位置,參數end是行駛終點位置,參數speed是速度
this.model是汽車模型,onUpdate事件中,不斷更新它的position
this.label是汽車車牌號標簽,onUpdate事件中,不斷更新它的position

Car.prototype.moveCar = function (start, end, speed) {
    let distance = this.distance(start, end);
    let time = distance / speed * 1000;

    return new Promise((resolve, reject) => {
        this.tween = new TWEEN.Tween({
            x: start.x,
            y: start.y,
            z: start.z
        }).to({
            x: end.x,
            y: end.y,
            z: end.z
        }, time).start().onUpdate(e => {
            if (this.model) {
                this.model.position.x = e.x;
                this.model.position.y = e.y;
                this.model.position.z = e.z;
            }
            if (this.label) {
                this.label.position.x = e.x;
                this.label.position.y = e.y + 1.2;
                this.label.position.z = e.z;
            }
        }).onComplete(() => {
            TWEEN.remove(this.tween);
            resolve();
        });
    });
}

汽車轉彎

參數start是動畫開始時的汽車朝向,end是動畫結束時的汽車朝向

Car.prototype.rotateCar = function (start, end) {
    return new Promise((resolve, reject) => {
        this.tween = new TWEEN.Tween({
            x: start.x,
            y: start.y,
            z: start.z
        }).to({
            x: end.x,
            y: end.y,
            z: end.z
        }, 300).start().onUpdate(e => {
            if (this.model) {
                this.model.rotation.set(e.x, e.y, e.z);
            }
        }).onComplete(() => {
            TWEEN.remove(this.tween);
            resolve();
        });
    });
}

汽車行駛多段路線

上述汽車行駛和汽車轉彎方法都是非同步方法,所以避免了回調地獄,不然下麵的多段行駛及轉彎就不好寫了

Cars.prototype.carLine1 = function () {
    if (!this.run) return;

    let car = new Car(this.scene, this.renderer, './models/車紅.glb');
    this.cars.push(car);

    let positions = [
        { x: -121, y: 1.5, z: -16 },
        { x: -130.5, y: 1.5, z: -16 },
        { x: -130.5, y: 1.5, z: 4 },
        { x: -82, y: 1.5, z: 4 },
        { x: -82, y: 1.5, z: 14.7 },
        { x: -18.8, y: 1.5, z: 14.7 },
        { x: -18.8, y: 1.5, z: 70 },
    ];

    let speed = 5;

    setTimeout(async () => {
        await car.loadCar(
            positions[0],
            car.WEST);

        car.showLabel(positions[0], "皖A67893");

        await car.moveCar(
            positions[0],
            positions[1],
            speed);

        await car.rotateCar(
            car.WEST,
            car.SOUTH);

        await car.moveCar(
            positions[1],
            positions[2],
            speed);

        await car.rotateCar(
            car.SOUTH,
            car.EAST);

        await car.moveCar(
            positions[2],
            positions[3],
            speed);

        await car.rotateCar(
            car.EAST,
            car.SOUTH);

        await car.moveCar(
            positions[3],
            positions[4],
            speed);

        await car.rotateCar(
            car.SOUTH,
            car.EAST);

        await car.moveCar(
            positions[4],
            positions[5],
            speed);

        await car.rotateCar(
            car.EAST,
            car.SOUTH);

        await car.moveCar(
            positions[5],
            positions[6],
            speed);

        car.unloadCar();

        this.carLine1(2000);
    }, 5000);
}

Cars.prototype.carLine2 = function () {
    if (!this.run) return;

    let car = new Car(this.scene, this.renderer, './models/車藍.glb');
    this.cars.push(car);

    let positions = [
        { x: -5, y: 1.5, z: 70 },
        { x: -5, y: 1.5, z: 14.7 },
        { x: 70, y: 1.5, z: 14.7 }
    ];

    let speed = 5;

    setTimeout(async () => {
        await car.loadCar(
            positions[0],
            car.NORTH);

        car.showLabel(positions[0], "皖AD887U");

        await car.moveCar(
            positions[0],
            positions[1],
            speed);

        await car.rotateCar(
            car.NORTH,
            car.EAST);

        await car.moveCar(
            positions[1],
            positions[2],
            speed);

        car.unloadCar();

        this.carLine2(3000);
    }, 6000);
}

汽車行駛多段路線改進

上述汽車行駛多段路線的代碼可以改進:

// 汽車朝向
let EAST = { x: 0, y: 1.5707963, z: 0 };
let SOUTH = { x: 0, y: 0, z: 0 };
let WEST = { x: 0, y: -1.5707963, z: 0 };
let NORTH = { x: 0, y: 3.1415926, z: 0 };

Cars.prototype.carLine1 = function () {
    if (!this.run) return;

    let car = new Car(this.scene, this.renderer, './models/車紅.glb');
    this.cars.push(car);

    let positions = [
        { x: -121, y: 1.5, z: -16 },
        { x: -130.5, y: 1.5, z: -16 },
        [WEST, SOUTH],
        { x: -130.5, y: 1.5, z: 4 },
        [SOUTH, EAST],
        { x: -82, y: 1.5, z: 4 },
        [EAST, SOUTH],
        { x: -82, y: 1.5, z: 14.7 },
        [SOUTH, EAST],
        { x: -18.8, y: 1.5, z: 14.7 },
        [EAST, SOUTH],
        { x: -18.8, y: 1.5, z: 70 },
    ];

    let speed = 5;

    setTimeout(async () => {
        await car.loadCar(
            positions[0],
            WEST);

        car.showLabel(positions[0], "皖A67893");

        for (let i = 1; i < positions.length; i++) {
            if (positions[i].length) {
                await car.rotateCar(positions[i][0], positions[i][1]);
            } else {
                let start = positions[i - 1].length ? positions[i - 2] : positions[i - 1];
                await car.moveCar(start, positions[i], speed);
            }
        }

        car.unloadCar();

        this.carLine1(2000);
    }, 5000);
}

調用

let cars = new Cars(app.scene, app.renderer);
cars.carLine1();
cars.carLine2();

顯示車牌號

Car.prototype.showLabel = function (position, text) {
    let canvasDraw = new CanvasDraw();
    let canvasTexture = canvasDraw.drawCarLabel(THREE, this.renderer, text, '#006688'); //標簽

    let spriteMaterial = new THREE.SpriteMaterial({
        map: canvasTexture,
        color: 0xffffff,
        depthTest: false,
        side: THREE.DoubleSide,
        sizeAttenuation: false,
        transparent: true,
        opacity: 0.8
    });

    let sprite = new THREE.Sprite(spriteMaterial);
    sprite.scale.set(0.2, 0.1, 0.2)
    sprite.position.x = position.x;
    sprite.position.y = position.y + 1.2;
    sprite.position.z = position.z;

    this.label = sprite;
    this.scene.add(sprite);

    return sprite;
}

完整代碼

car.js

// 汽車

let Car = (function () {

    function Car(scene, renderer, url) {
        this.scene = scene;
        this.renderer = renderer;
        this.url = url;
        this.clock = new THREE.Clock();
    }

    Car.prototype.loadCar = function (position, rotation) {
        let onProgress = function (xhr) {

        };

        return new Promise((resolve, reject) => {
            if (!this.model) {
                let loader = new THREE.GLTFLoader();
                loader.load(this.url, gltf => {
                    const model = gltf.scene || gltf.scenes[0];

                    model.position.x = position.x;
                    model.position.y = position.y;
                    model.position.z = position.z;

                    model.scale.set(0.25, 0.25, 0.25);

                    model.rotation.set(rotation.x, rotation.y, rotation.z);

                    this.model = model;
                    this.scene.add(model);

                    resolve(model);

                }, onProgress, xhr => {
                    console.error(xhr);
                    console.info('模型 ' + url + ' 載入失敗');
                    reject(xhr);
                });
            } else {
                resolve(this.model);
            }
        });
    }

    Car.prototype.unloadCar = function () {
        this.stopTween();
        this.removeModel();
        this.removeLabel();
    }

    Car.prototype.stopTween = function () {
        if (this.tween) {
            TWEEN.remove(this.tween);
        } else {
            setTimeout(() => {
                this.stopTween();
            }, 100);
        }
    }

    Car.prototype.removeModel = function () {
        if (this.model) {
            this.scene.remove(this.model);
        } else {
            setTimeout(() => {
                this.removeModel();
            }, 100);
        }
    }

    Car.prototype.removeLabel = function () {
        if (this.label) {
            this.scene.remove(this.label);
        } else {
            setTimeout(() => {
                this.removeLabel();
            }, 100);
        }
    }

    Car.prototype.moveCar = function (start, end, speed) {
        let distance = this.distance(start, end);
        let time = distance / speed * 1000;

        return new Promise((resolve, reject) => {
            this.tween = new TWEEN.Tween({
                x: start.x,
                y: start.y,
                z: start.z
            }).to({
                x: end.x,
                y: end.y,
                z: end.z
            }, time).start().onUpdate(e => {
                if (this.model) {
                    this.model.position.x = e.x;
                    this.model.position.y = e.y;
                    this.model.position.z = e.z;
                }
                if (this.label) {
                    this.label.position.x = e.x;
                    this.label.position.y = e.y + 1.2;
                    this.label.position.z = e.z;
                }
            }).onComplete(() => {
                TWEEN.remove(this.tween);
                resolve();
            });
        });
    }

    Car.prototype.rotateCar = function (start, end) {
        return new Promise((resolve, reject) => {
            this.tween = new TWEEN.Tween({
                x: start.x,
                y: start.y,
                z: start.z
            }).to({
                x: end.x,
                y: end.y,
                z: end.z
            }, 300).start().onUpdate(e => {
                if (this.model) {
                    this.model.rotation.set(e.x, e.y, e.z);
                }
            }).onComplete(() => {
                TWEEN.remove(this.tween);
                resolve();
            });
        });
    }

    Car.prototype.showLabel = function (position, text) {
        let canvasDraw = new CanvasDraw();
        let canvasTexture = canvasDraw.drawCarLabel(THREE, this.renderer, text, '#006688'); //標簽

        let spriteMaterial = new THREE.SpriteMaterial({
            map: canvasTexture,
            color: 0xffffff,
            depthTest: false,
            side: THREE.DoubleSide,
            sizeAttenuation: false,
            transparent: true,
            opacity: 0.8
        });

        let sprite = new THREE.Sprite(spriteMaterial);
        sprite.scale.set(0.2, 0.1, 0.2)
        sprite.position.x = position.x;
        sprite.position.y = position.y + 1.2;
        sprite.position.z = position.z;

        this.label = sprite;
        this.scene.add(sprite);

        return sprite;
    }

    Car.prototype.distance = function (p1, p2) {
        return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2) + Math.pow(p1.z - p2.z, 2));
    }

    return Car;

})();

cars.js

// 多個車輛

let Cars = (function () {

    // 汽車朝向
    let EAST = { x: 0, y: 1.5707963, z: 0 };
    let SOUTH = { x: 0, y: 0, z: 0 };
    let WEST = { x: 0, y: -1.5707963, z: 0 };
    let NORTH = { x: 0, y: 3.1415926, z: 0 };

    function Cars(scene, renderer) {
        this.scene = scene;
        this.renderer = renderer;
        this.cars = [];
        this.run = true;
    }

    Cars.prototype.carLine1 = function () {
        if (!this.run) return;

        let car = new Car(this.scene, this.renderer, './models/車紅.glb');
        this.cars.push(car);

        let positions = [
            { x: -121, y: 1.5, z: -16 },
            { x: -130.5, y: 1.5, z: -16 },
            [WEST, SOUTH],
            { x: -130.5, y: 1.5, z: 4 },
            [SOUTH, EAST],
            { x: -82, y: 1.5, z: 4 },
            [EAST, SOUTH],
            { x: -82, y: 1.5, z: 14.7 },
            [SOUTH, EAST],
            { x: -18.8, y: 1.5, z: 14.7 },
            [EAST, SOUTH],
            { x: -18.8, y: 1.5, z: 70 },
        ];

        let speed = 5;

        setTimeout(async () => {
            await car.loadCar(
                positions[0],
                WEST);

            car.showLabel(positions[0], "皖A67893");

            for (let i = 1; i < positions.length; i++) {
                if (positions[i].length) {
                    await car.rotateCar(positions[i][0], positions[i][1]);
                } else {
                    let start = positions[i - 1].length ? positions[i - 2] : positions[i - 1];
                    await car.moveCar(start, positions[i], speed);
                }
            }

            car.unloadCar();

            this.carLine1(2000);
        }, 5000);
    }

    Cars.prototype.carLine2 = function () {
        if (!this.run) return;

        let car = new Car(this.scene, this.renderer, './models/車藍.glb');
        this.cars.push(car);

        let positions = [
            { x: -5, y: 1.5, z: 70 },
            { x: -5, y: 1.5, z: 14.7 },
            { x: 70, y: 1.5, z: 14.7 }
        ];

        let speed = 5;

        setTimeout(async () => {
            await car.loadCar(
                positions[0],
                NORTH);

            car.showLabel(positions[0], "皖AD887U");

            await car.moveCar(
                positions[0],
                positions[1],
                speed);

            await car.rotateCar(
                NORTH,
                EAST);

            await car.moveCar(
                positions[1],
                positions[2],
                speed);

            car.unloadCar();

            this.carLine2(3000);
        }, 6000);
    }

    Cars.prototype.clear = function () {
        this.run = false;
        this.cars.forEach(car => {
            car.unloadCar();
        });
    }

    return Cars;

})();

調用

// 顯示汽車
function showCars() {
	cars = new Cars(app.scene, app.renderer);
	cars.carLine1();
	cars.carLine2();
}

// 清除汽車
function clearCars() {
	cars.clear();
}

// 顯示汽車
showCars();

總結

  1. 解耦:依賴的scene, renderer參數是通過構造函數傳到Car和Cars對象中的
  2. 汽車行駛和轉向等方法都是非同步方法,可以避免回調地獄,這樣汽車多段行駛的代碼會寫的比較清晰
  3. 在實現並完善功能的過程中不斷重構:回調地獄的實現方式-->調用moveCar和rotateCar時直接傳遞坐標,很多坐標及轉向數據和方法調用摻和在一起,看著眼花-->坐標和轉向數據和方法調用邏輯分離,看著很清晰

運行效果


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • SQL ALTER TABLE 語句 SQL ALTER TABLE 語句用於在現有表中添加、刪除或修改列,也可用於添加和刪除各種約束。 ALTER TABLE - 添加列 要在表中添加列,請使用以下語法: ALTER TABLE 表名 ADD 列名 數據類型; 以下 SQL 向 "Customer ...
  • 本文分享自華為雲社區《GaussDB資料庫SQL系列-SQL與ETL淺談》,作者:Gauss松鼠會小助手2。 一、前言 在SQL語言中,ETL(抽取、轉換和載入)是一種用於將數據從源系統抽取到目標系統的過程。ETL過程通常包括三個階段:抽取(Extract)、轉換(Transform)和載入(Loa ...
  • 數據作為新時代重要的生產要素之一,數據資產化的相關工作正在提速。自今年10月1日起,中國資產評估協會制定的《數據資產評估指導意見》正式施行。同時,《企業數據資源相關會計處理暫行規定》近期轉為正式稿,也將於明年1月1日起施行。 《暫行規定》規定:企業使用的數據資源,符合《企業會計準則第6號——無形資產 ...
  • 解耦、削峰:傳統的方式上游發送數據下游需要實時接收,如果上游在某些業務場景:例如上午十點會流量激增至頂峰,那麼下游資源可能會扛不住壓力。但如果使用消息隊列,就可以將消息暫存在消息管道中,下游可以按照自己的速度逐步處理; ...
  • 大數據框架下,常用的數據更新策略有三種: COW: copy-on-write, 寫時複製; MOR: merge-on-read, 讀時合併; MOW: merge-on-write, 寫時合併; hudi等數據湖倉框架,常用的是前兩種實現數據更新。而Doris則主要用後兩種更新數據。 COW 在 ...
  • 資料庫關係模型是數據組織的核心,以關係為基礎,表達實體和實體關係。關係模型通過域、笛卡爾積、關係模式構建二維表,形成資料庫。完整性約束確保數據有效性,實體完整性、參照完整性和用戶定義完整性保證數據質量。關係操作包括基本操作和數據語言,而關係代數提供了強大的查詢手段。資料庫關係模型為信息管理提供了結構... ...
  • 單體架構下鎖的實現方案 1. ReentrantLock全局鎖 ReentrantLock(可重入鎖),指的是一個線程再次對已持有的鎖保護的臨界資源時,重入請求將會成功。 簡單的與我們常用的Synchronized進行比較: ReentrantLock Synchronized 鎖實現機制 依賴AQ ...
  • 大家好,我是獨孤風,從本周開始,爭取每周為大家帶來一個優秀的開源項目推薦。 開源項目不僅促進了技術的發展和普及,還為全球範圍內的開發者和用戶社區建立了一個共用知識、協作和創新的平臺。站在巨人的肩膀上才能看的更遠,我們平時也應該多多關註開源項目,不僅學習其豐富的知識,也要找機會為開源事業做出自己的貢獻 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...