20210314のNode.jsに関する記事は6件です。

npmの復習

目次

1.この記事の目的
2.概要
3.パッケージ管理システム
4.もしnpmがなかったら
5.npmの使い方
6.便利なコマンド
7.最後に
8.参考記事


1. この記事の目的

  • npmに関する知識の復習

    • もやっとした理解だったため記事を読んで理解を深め言語化する
  • 実務でも使えそうな+αの知識を紹介
     - 調べる中で、便利なコマンドなどを学べたので紹介

  • 読んでくれる人の理解の手助けになれば


2. 概要

npmとは?

「Node Package Manager」の略。
Node.jsのパッケージ管理を行うツール。

Node.js

サーバーサイドで動くJavaScript。

特徴

  • 大量のデータ処理が可能
    • C10K問題に対応
      • Apache HTTP Serverなどウェブサーバーとクライアント通信においてクライアントが約1万台に達するとウェブサーバーのハードウェア性能に余裕があるにもかかわらず、レスポンス機能が大きく下がる問題
  • メモリ消費が少ない
    • イベントループ(詳細省略)
  • 処理速度が速い
    • Google V8 JavaScript Engine
      • Googleが開発したコンパイルエンジン
      • Chromeなどで使用
      • 中間コード、インタープリタを搭載せず、最初の実行時からコンパイルされる
      • JIT(Just-In-Time Compiler)
        • ソフトウェアの実行時にコードのコンパイルを実行。実行速度の向上を図るコンパイラ
        • (AOTコンパイラ)
          • 事前コンパイラ
  • サーバーサイドでJavaScriptが使える
    • 「サーバーサイドは新しい言語」ではなく、クライアント側で使用するJavaScriptをそのままサーバーサイドで使用できるため基礎的な書き方は同じ。

パッケージ

あらかじめ用意された便利な機能をまとめたもの

代表例

  • Express
    • Node.jsのMVCフレームワーク
  • promiss、async
    • 非同期処理

など


3. パッケージ管理システム

一つの環境で各種ソフトウェアの導入や削除、依存関係を管理するシステムのこと。

機能

主に4つの機能がある!

  • リポジトリの購読

    • インターネット上のパッケージリポジトリを指定し、必要に応じてパッケージリストを更新
  • ソフトウェアパッケージのインストール・削除

    • ソースコードや設定ファイルをパッケージ化してインストール可能
  • 依存関係の解決

    • あるパッケージに必要なソフトウェアを自動的にインストールしたり更新することができる
    • 同時にインストール、更新できないソフトウェアパッケージのインストールや更新を回避できる
  • 設定管理

    • 設定スクリプトを使って自動的に設定を行える
      • npmならpackage.json、composerならcomposer.jsonとか
    • 競合する機能を持つソフトウェアで、どちらを優先して使うかを設定できる

言われてみたら「確かにな」って感じるけど、言語化するとしっくりくる。


4. もしnpmがなかったら

開発するのに色々手間暇時間がかかります。
→「手間暇かけるのが愛情」が正だとすると、別のところに愛情を使えるようになる

  • 使用するパッケージ、ライブラリなどをいちいち公式サイトなどからダウンロードしないといけない

  • 作ったnpmパッケージをチームで共有したいが共有や更新などの管理などが面倒

つまり、楽に速くコードを書くことができるようになりサービスの開発時間の短縮や他のことに時間をかけられるようになる。


5. npmの使い方

Node.jsのダウンロード

公式サイトからダウンロード。
Node.jsをダウンロードしたらnpmもダウンロードされます

バージョンの確認

ダウンロードしたら

terminal
npm --version

でnpmのバージョンを確認します
(npmのパスが通ってなかったら通してください)

使用するパッケージをインストールする

ここでは例でjQueryをインストールします

プロジェクトルート配下
npm install jquery -g

※ 「-g」などのオプションは後述

既にpackage.jsonがあるプロジェクトの場合

package.jsonに記述されているパッケージを復元(インストール)します
(package.jsonはパッケージ管理スクリプトの一つ。詳細は後述)

package.jsonがディレクトリ
npm install

パッケージの更新

  • 未更新パッケージの確認
未更新パッケージを確認
npm outdated

※パッケージの最新バージョンを使用するにはpackage.jsonに記述が必要※

パッケージを更新する際、いちいちpackage.jsonに内容を確認、記述するのが面倒。
→パッケージのバージョンを一括で最新にしてくれるパッケージnpm-check-updatesが便利!

npm-check-updatesをインストール
npm install -g npm-check-updates
npm-check-updates -u
インストール後、アップデートで最新になる
npm update

これでnpmパッケージのバージョン管理が楽になる。


オプションについて

たくさんあるけど、この資料で使う分を紹介。

-g

グローバルインストール。どのディレクトリでも使えるようにパッケージをインストールする
(グローバルインストールについては後述)

-g
npm install jquery -g

--save

package.jsonのdependenciesに依存関係が追加される。
要は「このpackage.jsonが入ったプロジェクトに必要なパッケージ」
(dependenciesについては後述)

--save
npm install jquery --save

--save-dev

package.jsonのdevDependenciesに依存関係が追加される。
要は「パッケージの開発をするときに必要なパッケージ」。テストするためのパッケージとか。
(devDependenciesについても後述)


ここでnpmの説明から離れた小休憩。

グローバルインストールとローカルインストールの違い

恥ずかしながら、二つの違いをはっきり理解できていなかったため復習。

グローバルインストール

npm installするときに「-g」オプションをつけることで実行可能。
「npm」のルート配下にあるnode_modulesにパッケージがインストールされるため、一度グローバルインストールしたパッケージはどのディレクトリのプロジェクトでも使用することができる

ローカルインストール

npm installした時に何もオプションを付けなかった時はデフォルトでローカルインストールされる。
インストールされるパッケージの場所はプロジェクトルート配下にあるnode_modules。

従って、どのプロジェクトでも使いそうなパッケージをインストールするときはグローバルインストール、既存のプロジェクトを始めるとき(package.jsonが既にあったら)ローカルインストールの方が良い…のか?
※要確認


小休憩になるかわからないがnpmから直接離れてもう一度休憩。

package.jsonのdependenciesとdevDependencies

dependencies

npmパッケージを使うときに必要なパッケージ。
これがないとこのパッケージが動かない。
必要なパッケージ=依存しているパッケージ

dependenciesはインストールされる
npm install [パッケージ名]

これでもインストールされる。

devDependencies

npmパッケージを開発する時に必要なパッケージ。
package.jsonがあるディレクトリでnpm installをしないとインストールされない。
例えば
「jQuery」のパッケージをnpm installでインストールした時は「jQuery」を開発する上でのテスト関係のパッケージなどはインストールされない。
(パッケージをテストするためのパッケージ)


パッケージのアンインストール

グローバルインストールされたパッケージのアンインストール
グローバルインストールされたパッケージ
npm uninstall jquery -g
ローカルインストールされたパッケージのアンインストール
ローカルインストールされたパッケージ
npm uninstall jquery
package.jsonのdependenciesから依存関係を削除する場合
依存関係を削除
npm uninstall jquery --save
package.jsonのdevDependenciesから依存関係を削除
devDependenciesから削除
npm uninstall jquery --save-dev
ちなみに※uninstallは省略記号も使える
uninstallを省略
npm un jquery

6. 便利なコマンド

npm dedupe

パッケージの依存関係を整理してくれる.

  • モジュールBがCに依存

  • モジュールDがCに依存

    • 上記がそれぞれ別々の記述だった場合、記述が重複してバージョンが異なると不整合が起きる
  • これらを「モジュールB、DがCに依存」というふうに書き換えてくれるのがnpm dedupe

→パッケージをいくつか個別でinstallした後にやっておくと便利?
→npmに依存関係解決してくれる機能が既にあるから今はあまり使わない?

david

モジュールの最新安定バージョンをとってきてくれるツール
※依存モジュールの依存モジュールは最新にしない

david
npm install david -g
david update

オプションに-uを付けたら安定バージョンではない、最新のバージョンをとってきてくれるツール.

audit

依存関係でトラブった時にterminalでこのコマンド叩いてくださいって言われるやつ。

依存関係でトラブった時に解決手段を表示してくれる

npm audit 

依存関係でトラブった時に解決してくれる

npm audit fix

7. 最後に

参考記事を読んで僕なりにまとめてみましたが、もやっとしたところが言語化されて理解がスッキリした…かも。笑
特にグローバルインストールとローカルインストール、dependenciesとdevDependenciesははっきり理解できてなかったからとても勉強になりました。
あと便利コマンドも役に立ちそうだ!積極的に使って身につけていこう。

僕が下記の参考記事によって助けられたように
この記事が少しでも誰かの手助けになりますように。
ありがとうございました。


8. 参考記事

Wikipedia
- npm
- パッケージ管理システム

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Bootstrapをnodejsで利用するために調べたこと

BootStrap使ってカッコつけたい

今回、掲示板アプリ作成してます。
javascriptとhtmlとcssで出来ないこともないけど、せっかくならプラスオンして新しい技術に触れたいと思い、色々調べた結果bootstrapにたどり着きました。

ドキュメントを読んでサラッと使えると思いきや、
豪快につまづいてもがいていました。

「webpack」が必要みたい。

今回の引用と参考サイトはこちらです。
神の手が差し出された瞬間です。
とってもわかりやすかった。
最新版で学ぶwebpack 5入門 Bootstrapをバンドルする方法

webpackは簡単に言えば、複数のJavaScriptを1つにまとめること。
1つにまとめることができれば、htmlやejsへの適用もシンプルになります^^
javascriptだけではなく、sassファイルやcssファイルもloaderと呼ばれるモジュールをインストールすることで,webpackで1つにまとめることが出来る優れものです。

今回、bootstrapをnode.jsに適用するまでの3ステップをメモ

1.モジュールのインストール

npm i -D webpack webpack-cli style-loader css-loader

2.bootstrapを動かすためのライブラリをインストール

npm i -S bootstrap jquery popper.js

ドキュメントにはjqueryとpopperに依存しているので、言われるがままにインスト

3.webpackの設定と実行

webpack.config.jsにファイルの処理方法を記述します。
ここでは、単純に調理方法を記載していると考えてください。

module.exports = {
  // モード値を production に設定すると最適化された状態で、
  // development に設定するとソースマップ有効でJSファイルが出力される
  mode: "production",
  module: {
    rules: [
      {
        // 対象となるファイルの拡張子(cssのみ)
        test: /\.css$/,
        // Sassファイルの読み込みとコンパイル
        use: [
          // スタイルシートをJSからlinkタグに展開する機能
          "style-loader",
          // CSSをバンドルするための機能
          "css-loader"
        ],
      },
    ],
  },
};

次に食材を用意します。

srcフォルダー内にindex.jsファイルを作成。
index.jsファイルにはimport文を用いて、Bootstrapを取り込みます。

// Bootstrapのスタイルシート側の機能を読み込む
import "bootstrap/dist/css/bootstrap.min.css";

// BootstrapのJavaScript側の機能を読み込む
import "bootstrap";

最後に食材と調理方法が準備できれば早速調理開始です。

npx webpack

と実行すると、
'dist'フォルダが生成され、一つになったjsファイルが生成されます。
これを同じようにhtmlまたはejsファイルに取り込みすれば、環境設定は完了です。
bootstrapドキュメントのレイアウトのコピーを貼り付けすれば、使えるようになります。

まとめ

かなりざっくりすぎてビックリですが、忘備録として残しておきます。
webpackの記述方法は、後に身につけたいなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2021年 MacにNode.jsをインストールする - zsh, nodebrew

環境

  • MacOS Catalina 10.15.7
  • zsh

nodebrew install

ターミナルで以下のコマンドを実行してソースのダウンロードをする

$ curl -L git.io/nodebrew | perl - setup

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0
100 24634  100 24634    0     0  12010      0  0:00:02  0:00:02 --:--:--  150k
Fetching nodebrew...
Installed nodebrew in $HOME/.nodebrew

========================================
Export a path to nodebrew:

export PATH=$HOME/.nodebrew/current/bin:$PATH
========================================

.zshrc に以下のパスを追加する

export PATH=$HOME/.nodebrew/current/bin:$PATH

追加を終えたら適用するためのコマンドを実行しnodebrewのインストールは完了

source ~/.zshrc

Node.js install

インストール可能なNode.jsを確認する

$ nodebrew ls-remote

バージョンを指定してインストールする

$ nodebrew install-binary vX.X.X

stable版をinstallしたい場合はこちら

$ nodebrew install-binary stable
Fetching: https://nodejs.org/dist/v14.16.0/node-v14.16.0-darwin-x64.tar.gz
############################################################################################### 100.0%
Installed successfully

nodeがインストールされていることを確認する

$ nodebrew ls
v14.16.0

current: none

使用するnodeのバージョンを指定する

$ nodebrew use v14.16.0
use v14.16.0

nodeが使えるか確認する

$ node -v
v14.16.0
$ npm -v
6.14.11
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2021年 MacにNode.jsをインストールする - zsh, nodebrew, yarn

環境

  • MacOS Catalina 10.15.7
  • zsh

nodebrew install

ターミナルで以下のコマンドを実行してソースのダウンロードをする

$ curl -L git.io/nodebrew | perl - setup

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0
100 24634  100 24634    0     0  12010      0  0:00:02  0:00:02 --:--:--  150k
Fetching nodebrew...
Installed nodebrew in $HOME/.nodebrew

========================================
Export a path to nodebrew:

export PATH=$HOME/.nodebrew/current/bin:$PATH
========================================

.zshrc に以下のパスを追加する

export PATH=$HOME/.nodebrew/current/bin:$PATH

追加を終えたら適用するためのコマンドを実行しnodebrewのインストールは完了

source ~/.zshrc

Node.js install

インストール可能なNode.jsを確認する

$ nodebrew ls-remote

バージョンを指定してインストールする

$ nodebrew install-binary vX.X.X

stable版をinstallしたい場合はこちら

$ nodebrew install-binary stable
Fetching: https://nodejs.org/dist/v14.16.0/node-v14.16.0-darwin-x64.tar.gz
############################################################################################### 100.0%
Installed successfully

nodeがインストールされていることを確認する

$ nodebrew ls
v14.16.0

current: none

使用するnodeのバージョンを指定する

$ nodebrew use v14.16.0
use v14.16.0

nodeが使えるか確認する

$ node -v
v14.16.0
$ npm -v
6.14.11

yarn install

yarnをインストールする

$ npm install -g yarn

インストールされているか確認する

$ yarn -v
1.22.10
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Fire Storageを監視してサイレントプッシュ通知を送信

概要

Fire Storageに画像ファイルのアップロードがなされるとCloud Functionsを通してサイレントプッシュ通知を送信します。

今更感はありますが、ググってもpayloadの記述に統一性がなかったりして割と苦戦しました:sweat:

Stack OverFlowの方でも同じ悩みを抱えていた人がいたので参考になれば幸いです。

なお、本記事ではFirebase CLIや証明書周りについては触れません。
下準備ができている前提で進めます。

環境

  • Xcode 12.4
  • Swift 5
  • nodejs 12

Fire Storage

デフォルトバケットを使用

クライアント

Capability

TARGETS > CapabilitiesよりBackground ModesとPush Notificationsを有効にします。
Background ModesはRemote notificationsにチェックを入れます。
(バックグラウンドで通知を受け取りたい場合はBackground fetchもチェック)

Swift

今回はTopic購読で実装します。

AppDelegate.swift
import Firebase

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        FirebaseApp.configure()

        // 1. 通知許可
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(
            options: authOptions,
            completionHandler: { (granted, error) in
                // todo
            })
        application.registerForRemoteNotifications()
        return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // 2. Topic購読
        Messaging.messaging().subscribe(toTopic: "hoge") { _ in
            // todo
        }
    }

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                     fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        // 3. 画像ファイルの名称を取り出す
        guard let imageName = userInfo["imageName"] as? String else {
            return
        }
        print("image name: \(imageName)")
    }
}

Cloud Functions

onFinalize関数でバケットに画像がアップロードされたことを検知します。

index.js
'use strict';

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

exports.sendSilentNotificationWithTopic = functions.storage.object().onFinalize((object) => {
  const topic = 'hoge';
  const payload = {
    data: {
      imageName: object.name, // 画像名をセット
    },
  };
  const options = {
    contentAvailable: true // サイレントプッシュはtrueにする
  };
  return admin.messaging().sendToTopic(topic, payload, options)
    .then(function(response) {
      return console.log("Successfully sent message:", response);
    })
    .catch(function(error) {
      return console.log("Error sending message:", error);
    });  
});

firebase deployを実行してデプロイします。

実行

  1. Xcodeに実機をつないでRun
  2. Fire Storageに画像ファイルを追加

Xcodeのコンソールログに画像名が表示されれば成功です。
自分はこれで成功したのですが、失敗したらすいません。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsでglTFモデルを圧縮してthree.jsで読む込む(DRACO/meshoptimizer)

概要

WebGL表現でハイポリ(30K ~ 140K Poly)のglTFモデルを複数使用した際に、転送量やローディングパフォーマンスの観点からできる限りファイルサイズを落としたくて、Node.jsでglTFファイルを圧縮する環境を作ったのでその紹介です。

リポジトリはこちら

glTFのファイルサイズを軽量化する方法はいくつかありますが、本記事ではDRACOmeshoptimizerによる2通りの圧縮方法を紹介します。

環境構築

yarnとNode.jsのバージョンは以下です。

yarn@v1.22.5
node@v12.21.0

プロジェクトディレクトリを作成、移動してpackage.jsonを生成します。

$ mkdir gltf-compressor
$ cd gltf-compressor
$ yarn init -y

srcディレクトリを作成しglTFを格納します。

$ mkdir src

今回はサンプルデータとして、mixamoからダウンロードしたアニメーション付きXBOTのモデルがどのくらい軽量化できるか比較してみます。

mixamo.gif

mixamoからダウンロードできるのは.fbx.dae形式なので、BlenderでglTFにエクスポートしておきます。

アニメーション マテリアル 頂点 ポリゴン
1 2 28,312 49,112
gltf-compressor
├── package.json
└── + src
    └── + Capoeira
        ├── + Capoeira.bin
        └── + Capoeira.gltf

圧縮前のglTFは2.3MBありました。

before.png

DRACO圧縮

DRACO圧縮にはgltf-pipelineパッケージを使用します。

gltf-pipeline fs-extra glob pathをインストールし、DRACO圧縮用のJSファイルcompress-draco.jsを作成します。

$ yarn add gltf-pipeline fs-extra glob path
$ touch compress-draco.js
gltf-compressor
├── + compress-draco.js
├── node_modules
│   └── ...
├── package.json
├── src
│   └── ...
└── yarn.lock

yarn dracoコマンドを実行した際に対応するJSが実行されるように、package.jsonscriptsを追加します。

package.json
{
  "name": "gltf-compressor",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "draco": "node compress-draco.js"
  },
  "dependencies": {
    "fs-extra": "^9.1.0",
    "glob": "^7.1.6",
    "gltf-pipeline": "^3.0.2",
    "path": "^0.12.7"
  }
}

Node.jsでDRACO圧縮をするコードです。

compress-draco.js
const glob = require('glob');
const fs = require('fs-extra');
const path = require('path');
const gltfPipeline = require('gltf-pipeline');

const srcDir = 'src';
const distDir = 'dist';

/**
 * glTFをDRACO圧縮
 * @param {string | string[]} globs 
 */
const compressGltfWithDraco = (globs) => {
  glob(globs, async (err, files) => {
    if (err) return;

    for (const file of files) {
      const filePath = path.resolve(file);
      const gltf = fs.readJsonSync(filePath);// gltfのJSONを読み込む
      const options = {
        resourceDirectory: path.dirname(filePath),// gltfのリソースディレクトリ(親フォルダ)
        dracoOptions: { compressionLevel: 10 }// DRACO圧縮率MAX
      };
      const { glb } = await gltfPipeline.gltfToGlb(gltf, options);// gltf -> glb
      const outFilePath = filePath.replace('.gltf', '-draco.glb').replace(srcDir, distDir);// 出力先
      await fs.mkdirp(path.dirname(outFilePath));// distディレクトリがなかったら作成
      await fs.writeFileSync(outFilePath, glb);// glbファイル出力
      console.log(`[draco] ${ outFilePath }`);
    }
  });
};

compressGltfWithDraco(`./${ srcDir }/**/*.gltf`);

globsrcフォルダ内にある.gltfファイルの配列を取得し、.gltfJSONとして読み込ませたものとオプションをgltfPipeline.gltfToGlb()に渡して圧縮します。

glTFを圧縮しているのは以下の部分です。

const options = {
  resourceDirectory: path.dirname(filePath),// gltfのリソースディレクトリ(親フォルダ)
  dracoOptions: { compressionLevel: 10 }// DRACO圧縮率MAX
};
const { glb } = await gltfToGlb(gltf, options);// gltf -> glb

用意しておいたコマンドを実行してDRACO圧縮をかけます。

$ yarn draco

distディレクトリにDRACO圧縮後の.glbファイルが出力されます。

gltf-compressor
├── compress-draco.js
├── + dist
│   └── + Capoeira
│       └── + Capoeira-draco.glb
├── node_modules
│   └── ...
├── package.json
├── src
│   └── Capoeira
│       ├── Capoeira.bin
│       └── Capoeira.gltf
└── yarn.lock
Before After 圧縮率
2.3MB 804KB -64.79%

after-draco.png

three.jsで読み込む

GLTFLoader.setDracoLoader()DRACOLoaderのインスタンスを渡して読み込みます。
DRACO圧縮のデコードに必要なdraco_decoder.js draco_decoder.wasmthree/examples/js/libs/dracoに用意されているので、自分のサーバーにコピーしてDRACOLoader.setDecoderPath()でパスを指定する必要があります。

参考:https://threejs.org/docs/index.html#examples/en/loaders/GLTFLoader

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/path/to/draco_decoder/');

const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.load('Capoeira-draco.glb', (gltf) => {
  scene.add(gltf.scene);
});

meshopt圧縮

meshopt圧縮にはgltfpackパッケージを使用します。

gltfpackをインストールして、meshopt圧縮用のJSファイルcompress-meshopt.jsを作成します。

$ yarn add gltf-pack
$ touch compress-meshopt.js
gltf-compressor
├── compress-draco.js
├── + compress-meshopt.js
├── dist
│   └── ...
├── node_modules
│   └── ...
├── package.json
├── src
│   └── ...
└── yarn.lock

yarn meshoptコマンドを実行した際に対応するJSが実行されるように、package.jsonscriptsを追加します。

package.json
{
  "name": "gltf-compresser",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "draco": "node compress-draco.js",
    "meshopt": "node compress-meshopt.js"
  },
  "dependencies": {
    "fs-extra": "^9.1.0",
    "glob": "^7.1.6",
    "gltf-pipeline": "^3.0.2",
    "gltfpack": "^0.15.2",
    "path": "^0.12.7"
  }
}

Node.jsでmeshopt圧縮をするコードです。

compress-meshopt.js
const cp = require('child_process');
const glob = require('glob');
const gltfPack = require('gltfpack');
const fs = require('fs-extra');
const path = require('path');

const srcDir = 'src';
const distDir = 'dist';

const paths = {
  'basisu': process.env['BASISU_PATH'],
  'toktx': process.env['TOKTX_PATH']
};

/**
 * gltfpack用インターフェース
 */
const gltfPackInterface = {
  read: (path) => {
    return fs.readFileSync(path);
  },
  write: (path, data) => {
    fs.writeFileSync(path, data);
  },
  execute: (command) => {
    // perform substitution of command executable with environment-specific paths
    const pkv = Object.entries(paths);
    for (const [k, v] of pkv) {
      if (command.startsWith(k + ' ')) {
        command = v + command.substr(k.length);
        break;
      }
    }
    const ret = cp.spawnSync(command, [], { shell: true });
    return ret.status == null ? 256 : ret.status;
  },
  unlink: (path) => {
    fs.unlinkSync(path);
  }
};

/**
 * compress gltf -> glb
 * @param {string} inputPath 
 * @param {string} outputPath 
 * @return {Promise<string>}
 */
const packGltf = (inputPath, outputPath) => {
  const output = outputPath || inputPath.replace('.gltf', '.glb');
  const command = `-i ${ inputPath } -o ${ output } -cc`;// コマンドライン引数(必要に応じてオプションを追加)
  const args = command.split(/\s/g);// コマンドライン引数の配列
  return gltfPack.pack(args, gltfPackInterface).catch(err => { console.log(err.message); });
};

/**
 * glTFをmeshoptimizer圧縮
 * @param {string | string[]} globs 
 */
const compressGltfWithMeshopt = (globs) => {
  glob(globs, async (err, files) => {
    if (err) return;

    for (const file of files) {
      const filePath = path.resolve(file);
      const outFilePath = filePath.replace('.gltf', '-meshopt.glb').replace(srcDir, distDir);// 保存先
      await fs.mkdirp(path.dirname(outFilePath));// distディレクトリがなかったら作成
      await packGltf(filePath, outFilePath);// gltf -> glb
      console.log(`[meshopt] ${ outFilePath }`);
    }
  });
};

compressGltfWithMeshopt(`./${ srcDir }/**/*.gltf`);

glTFひとつを圧縮するpackGltf関数を用意して、globで取得したファイルリストに対して回しています。

gltfPackInterfaceの部分はgltfpack/cli.jsの実装から引っ張ってきたので、なぜこういう実装になっているかわかりません。(拡張性をもたせるため?)

const packGltf = (inputPath, outputPath) => {
  const output = outputPath || inputPath.replace('.gltf', '.glb');
  const command = `-i ${ inputPath } -o ${ output } -cc`;// コマンドライン引数(必要に応じてオプションを追加)
  const args = command.split(/\s/g);// コマンドライン引数の配列
  return gltfPack.pack(args, gltfPackInterface).catch(err => { console.log(err.message); });
};

用意しておいたコマンドを実行してmeshopt圧縮をかけます。

$ yarn meshopt

distディレクトリにmeshopt圧縮後の.glbファイルが出力されます。

gltf-compressor
├── compress-draco.js
├── compress-meshopt.js
├── dist
│   └── Capoeira
│       ├── Capoeira-draco.glb
│       └── + Capoeira-meshopt.glb
├── node_modules
│   └── ...
├── package.json
├── src
│   └── Capoeira
│       ├── Capoeira.bin
│       └── Capoeira.gltf
└── yarn.lock
Before After 圧縮率
2.3MB 247KB -89.16%

after-meshopt.png

three.jsで読み込む

GLTFLoaderのDocumentationには記載されていませんが、GLTFLoader.setMeshoptDecoder()というメンバ関数が用意されていて、デコーダーもthree.js内に含まれています。

こちらはGLTFLoader.setMeshoptDecoder()MeshoptDecoderをそのまま渡します。

参考:https://threejs.org/examples/#webgl_loader_gltf_compressed

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';

const loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load('Capoeira-meshopt.glb', (gltf) => {
  scene.add(gltf.scene);
});

比較

圧縮形式 Before After 圧縮率
DRACO 2.3MB 804KB -64.79%
meshoptimizer 2.3MB 247KB -89.16%

今回のサンプルモデルだけで比較するとDRACOよりmeshoptimizerのほうが圧縮率が高かったです。

元ファイルのサイズが2.3MBもあったのに247KBまで落とせるというのはWebパフォーマンス的にもかなりありがたいですね。

いくつかモデルを圧縮して比較した印象では、モデルの作り方やジオメトリの構造によってはDRACO圧縮のほうがサイズを落とせる場合があったり、meshopt圧縮をすると一部のメッシュが破綻することがあったので、どちらか一択という訳にはいきませんでした。
使用するモデルの中身に応じて圧縮形式を適宜使い分ける必要がありそうです。

Links

gltf-pipeline
gltfpack
gltf-transform(試してないが多機能そうなnpmパッケージ)
RapidCompact(ポリゴン数やテクスチャサイズなどを自動で最適化してくれるオンラインサービス)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む