@webxr-jp/texture-compression 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # @webxr-jp/texture-compression
2
+
3
+ avatar-optimizer向けのテクスチャ圧縮ユーティリティパッケージ。Basis Universal WASMを使用してKTX2形式(UASTC)でテクスチャを圧縮します。
4
+
5
+ ## 特徴
6
+
7
+ - **KTX2形式出力**: GPU対応の圧縮テクスチャフォーマット
8
+ - **UASTC圧縮**: 高品質なユニバーサルテクスチャ圧縮
9
+ - **Zstandard超圧縮**: オプションでさらなるファイルサイズ削減
10
+ - **ブラウザ専用**: WebAssemblyベースで高速処理
11
+
12
+ ## インストール
13
+
14
+ ```bash
15
+ pnpm add @webxr-jp/texture-compression
16
+ ```
17
+
18
+ ## 使用方法
19
+
20
+ ### 基本的な圧縮
21
+
22
+ ```typescript
23
+ import { compressToKtx2, initBasisEncoder } from '@webxr-jp/texture-compression'
24
+
25
+ // WASMモジュールを初期化(初回のみ)
26
+ const initResult = await initBasisEncoder()
27
+ if (initResult.isErr()) {
28
+ console.error('初期化エラー:', initResult.error)
29
+ return
30
+ }
31
+
32
+ // RGBAピクセルデータを圧縮
33
+ const imageData = new Uint8Array(width * height * 4) // RGBA
34
+ const result = await compressToKtx2(imageData, width, height)
35
+
36
+ if (result.isOk()) {
37
+ const ktx2Data = result.value.data // Uint8Array
38
+ console.log(
39
+ `圧縮完了: ${result.value.originalSize} → ${result.value.compressedSize} bytes`,
40
+ )
41
+ }
42
+ ```
43
+
44
+ ### オプション設定
45
+
46
+ ```typescript
47
+ import { compressToKtx2, UastcQuality } from '@webxr-jp/texture-compression'
48
+
49
+ const result = await compressToKtx2(imageData, width, height, {
50
+ quality: UastcQuality.Default, // 品質レベル (0-4)
51
+ compressionLevel: 3, // 圧縮レベル (0-5)
52
+ generateMipmaps: false, // ミップマップ生成
53
+ supercompression: true, // Zstandard超圧縮
54
+ })
55
+ ```
56
+
57
+ ### Y軸反転
58
+
59
+ WebGLテクスチャ座標系(左下原点)からKTX2座標系(左上原点)への変換が必要な場合:
60
+
61
+ ```typescript
62
+ import { flipImageY, compressToKtx2 } from '@webxr-jp/texture-compression'
63
+
64
+ const flippedData = flipImageY(imageData, width, height)
65
+ const result = await compressToKtx2(flippedData, width, height)
66
+ ```
67
+
68
+ ### エンコーダー管理
69
+
70
+ ```typescript
71
+ import {
72
+ initBasisEncoder,
73
+ disposeBasisEncoder,
74
+ isBasisEncoderReady,
75
+ } from '@webxr-jp/texture-compression'
76
+
77
+ // 初期化確認
78
+ if (!isBasisEncoderReady()) {
79
+ await initBasisEncoder()
80
+ }
81
+
82
+ // 使用後にリソース解放(オプション)
83
+ disposeBasisEncoder()
84
+ ```
85
+
86
+ ## API
87
+
88
+ ### `compressToKtx2(imageData, width, height, options?)`
89
+
90
+ RGBAピクセルデータをKTX2形式に圧縮します。
91
+
92
+ - `imageData`: `Uint8Array` - RGBAピクセルデータ(width × height × 4 bytes)
93
+ - `width`: `number` - 画像の幅
94
+ - `height`: `number` - 画像の高さ
95
+ - `options`: `Ktx2CompressionOptions` - 圧縮オプション(省略可)
96
+
97
+ 戻り値: `ResultAsync<Ktx2CompressionResult, CompressionError>`
98
+
99
+ ### `flipImageY(imageData, width, height)`
100
+
101
+ Y軸を反転したピクセルデータを生成します。
102
+
103
+ ### `initBasisEncoder(wasmDir?)`
104
+
105
+ BasisEncoder WASMモジュールを初期化します。
106
+
107
+ - `wasmDir`: `string` - WASMファイルのディレクトリURL(省略時はパッケージ内のwasmディレクトリ)
108
+
109
+ ### `disposeBasisEncoder()`
110
+
111
+ キャッシュされたモジュールを解放します。
112
+
113
+ ### `isBasisEncoderReady()`
114
+
115
+ WASMモジュールが初期化済みかどうかを返します。
116
+
117
+ ## 型定義
118
+
119
+ ### `Ktx2CompressionOptions`
120
+
121
+ ```typescript
122
+ interface Ktx2CompressionOptions {
123
+ quality?: UastcQuality // UASTC品質レベル (デフォルト: Default)
124
+ compressionLevel?: number // 圧縮レベル 0-5 (デフォルト: 3)
125
+ generateMipmaps?: boolean // ミップマップ生成 (デフォルト: false)
126
+ supercompression?: boolean // Zstandard超圧縮 (デフォルト: true)
127
+ }
128
+ ```
129
+
130
+ ### `UastcQuality`
131
+
132
+ ```typescript
133
+ enum UastcQuality {
134
+ Fastest = 0, // 最速、最低品質
135
+ Faster = 1, // 高速
136
+ Default = 2, // デフォルト
137
+ Slower = 3, // 低速、高品質
138
+ VerySlow = 4, // 最高品質
139
+ }
140
+ ```
141
+
142
+ ### `Ktx2CompressionResult`
143
+
144
+ ```typescript
145
+ interface Ktx2CompressionResult {
146
+ data: Uint8Array // 圧縮後のKTX2バイナリデータ
147
+ originalSize: number // 元のサイズ (bytes)
148
+ compressedSize: number // 圧縮後のサイズ (bytes)
149
+ width: number // 画像の幅
150
+ height: number // 画像の高さ
151
+ }
152
+ ```
153
+
154
+ ## 注意事項
155
+
156
+ - ブラウザ環境専用です(Node.js非対応)
157
+ - 2のべき乗でないサイズの画像は警告が出ますが処理は継続します
158
+ - 大きなテクスチャの場合、出力バッファは最大24MBに制限されています
package/dist/index.cjs ADDED
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ UastcQuality: () => UastcQuality,
34
+ compressToKtx2: () => compressToKtx2,
35
+ disposeBasisEncoder: () => disposeBasisEncoder,
36
+ flipImageY: () => flipImageY,
37
+ getCachedBasisEncoder: () => getCachedBasisEncoder,
38
+ initBasisEncoder: () => initBasisEncoder,
39
+ isBasisEncoderReady: () => isBasisEncoderReady
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+
43
+ // src/types.ts
44
+ var UastcQuality = /* @__PURE__ */ ((UastcQuality2) => {
45
+ UastcQuality2[UastcQuality2["Fastest"] = 0] = "Fastest";
46
+ UastcQuality2[UastcQuality2["Faster"] = 1] = "Faster";
47
+ UastcQuality2[UastcQuality2["Default"] = 2] = "Default";
48
+ UastcQuality2[UastcQuality2["Slower"] = 3] = "Slower";
49
+ UastcQuality2[UastcQuality2["VerySlow"] = 4] = "VerySlow";
50
+ return UastcQuality2;
51
+ })(UastcQuality || {});
52
+
53
+ // src/compress.ts
54
+ var import_neverthrow2 = require("neverthrow");
55
+
56
+ // src/encoder.ts
57
+ var import_neverthrow = require("neverthrow");
58
+ var import_basis_encoder = __toESM(require("../wasm/basis_encoder.js"), 1);
59
+ var import_basis_encoder2 = __toESM(require("../wasm/basis_encoder.wasm?url"), 1);
60
+ var cachedModule = null;
61
+ function initBasisEncoder() {
62
+ if (cachedModule) {
63
+ return import_neverthrow.ResultAsync.fromSafePromise(Promise.resolve(cachedModule));
64
+ }
65
+ return import_neverthrow.ResultAsync.fromPromise(loadBasisModule(), (error) => ({
66
+ type: "WASM_LOAD_ERROR",
67
+ message: `Basis WASM \u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557: ${error instanceof Error ? error.message : String(error)}`
68
+ })).map((module2) => {
69
+ cachedModule = module2;
70
+ return module2;
71
+ });
72
+ }
73
+ function disposeBasisEncoder() {
74
+ cachedModule = null;
75
+ }
76
+ function getCachedBasisEncoder() {
77
+ return cachedModule;
78
+ }
79
+ function isBasisEncoderReady() {
80
+ return cachedModule !== null;
81
+ }
82
+ async function loadBasisModule() {
83
+ const moduleObj = await (0, import_basis_encoder.default)({
84
+ locateFile: () => import_basis_encoder2.default
85
+ });
86
+ if (moduleObj.initializeBasis) {
87
+ moduleObj.initializeBasis();
88
+ }
89
+ if (!moduleObj.BasisEncoder) {
90
+ throw new Error("BasisEncoder class not found in module after init");
91
+ }
92
+ return moduleObj;
93
+ }
94
+
95
+ // src/compress.ts
96
+ var DEFAULT_OPTIONS = {
97
+ quality: 2 /* Default */,
98
+ compressionLevel: 3,
99
+ generateMipmaps: true,
100
+ supercompression: true
101
+ };
102
+ var MAX_OUTPUT_SIZE = 1024 * 1024 * 24;
103
+ function compressToKtx2(imageData, width, height, options) {
104
+ const validationError = validateInput(imageData, width, height);
105
+ if (validationError) {
106
+ return (0, import_neverthrow2.errAsync)(validationError);
107
+ }
108
+ const opts = { ...DEFAULT_OPTIONS, ...options };
109
+ return initBasisEncoder().andThen(
110
+ (module2) => import_neverthrow2.ResultAsync.fromPromise(
111
+ encodeWithBasis(module2, imageData, width, height, opts),
112
+ (error) => ({
113
+ type: "COMPRESSION_ERROR",
114
+ message: `KTX2\u30A8\u30F3\u30B3\u30FC\u30C9\u306B\u5931\u6557: ${error instanceof Error ? error.message : String(error)}`
115
+ })
116
+ )
117
+ );
118
+ }
119
+ function validateInput(imageData, width, height) {
120
+ if (width <= 0 || height <= 0) {
121
+ return {
122
+ type: "INVALID_INPUT",
123
+ message: `\u5E45\u3068\u9AD8\u3055\u306F\u6B63\u306E\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059: width=${width}, height=${height}`
124
+ };
125
+ }
126
+ const expectedSize = width * height * 4;
127
+ if (imageData.length !== expectedSize) {
128
+ return {
129
+ type: "INVALID_INPUT",
130
+ message: `\u753B\u50CF\u30C7\u30FC\u30BF\u30B5\u30A4\u30BA\u304C\u4E0D\u6B63\u3067\u3059: expected=${expectedSize}, actual=${imageData.length}`
131
+ };
132
+ }
133
+ if (!isPowerOfTwo(width) || !isPowerOfTwo(height)) {
134
+ console.warn(
135
+ `[texture-compression] \u5E45\u30FB\u9AD8\u3055\u304C2\u306E\u3079\u304D\u4E57\u3067\u306A\u3044\u5834\u5408\u3001\u4E00\u90E8\u306EGPU\u3067\u554F\u984C\u304C\u767A\u751F\u3059\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059: ${width}x${height}`
136
+ );
137
+ }
138
+ return null;
139
+ }
140
+ function isPowerOfTwo(n) {
141
+ return n > 0 && (n & n - 1) === 0;
142
+ }
143
+ async function encodeWithBasis(module2, imageData, width, height, options) {
144
+ const encoder = new module2.BasisEncoder();
145
+ try {
146
+ encoder.setCreateKTX2File(true);
147
+ encoder.setUASTC(true);
148
+ encoder.setKTX2UASTCSupercompression(options.supercompression);
149
+ encoder.setPackUASTCFlags(options.quality);
150
+ encoder.setCompressionLevel(options.compressionLevel);
151
+ encoder.setMipGen(options.generateMipmaps);
152
+ const success = encoder.setSliceSourceImage(
153
+ 0,
154
+ imageData,
155
+ width,
156
+ height,
157
+ module2.ldr_image_type.cRGBA32.value
158
+ );
159
+ if (!success) {
160
+ throw new Error("\u30BD\u30FC\u30B9\u753B\u50CF\u306E\u8A2D\u5B9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
161
+ }
162
+ const outputBuffer = new Uint8Array(MAX_OUTPUT_SIZE);
163
+ const outputSize = encoder.encode(outputBuffer);
164
+ if (outputSize === 0) {
165
+ throw new Error("\u30A8\u30F3\u30B3\u30FC\u30C9\u7D50\u679C\u304C\u7A7A\u3067\u3059");
166
+ }
167
+ const ktx2Data = new Uint8Array(outputBuffer.buffer, 0, outputSize);
168
+ return {
169
+ data: ktx2Data.slice(),
170
+ // コピーを返す
171
+ originalSize: imageData.length,
172
+ compressedSize: outputSize,
173
+ width,
174
+ height
175
+ };
176
+ } finally {
177
+ encoder.delete();
178
+ }
179
+ }
180
+ function flipImageY(imageData, width, height) {
181
+ const rowSize = width * 4;
182
+ const flipped = new Uint8Array(imageData.length);
183
+ for (let y = 0; y < height; y++) {
184
+ const srcOffset = y * rowSize;
185
+ const dstOffset = (height - 1 - y) * rowSize;
186
+ flipped.set(imageData.subarray(srcOffset, srcOffset + rowSize), dstOffset);
187
+ }
188
+ return flipped;
189
+ }
190
+ // Annotate the CommonJS export names for ESM import in node:
191
+ 0 && (module.exports = {
192
+ UastcQuality,
193
+ compressToKtx2,
194
+ disposeBasisEncoder,
195
+ flipImageY,
196
+ getCachedBasisEncoder,
197
+ initBasisEncoder,
198
+ isBasisEncoderReady
199
+ });
200
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/compress.ts","../src/encoder.ts"],"sourcesContent":["/**\n * @webxr-jp/texture-compression\n *\n * テクスチャ圧縮ユーティリティパッケージ\n * avatar-optimizerで利用するためのWASMベースのKTX2圧縮機能を提供\n */\n\n// 型定義\nexport type {\n CompressionError,\n Ktx2CompressionOptions,\n Ktx2CompressionResult,\n} from './types'\nexport { UastcQuality } from './types'\n\n// 圧縮API\nexport { compressToKtx2, flipImageY } from './compress'\n\n// エンコーダー管理\nexport {\n disposeBasisEncoder,\n getCachedBasisEncoder,\n initBasisEncoder,\n isBasisEncoderReady,\n} from './encoder'\n","/**\n * テクスチャ圧縮関連の型定義\n */\n\n/** UASTC品質レベル */\nexport enum UastcQuality {\n /** 最速、最低品質 */\n Fastest = 0,\n /** 高速 */\n Faster = 1,\n /** デフォルト */\n Default = 2,\n /** 低速、高品質 */\n Slower = 3,\n /** 最高品質 */\n VerySlow = 4,\n}\n\n/** KTX2圧縮オプション */\nexport interface Ktx2CompressionOptions {\n /** UASTC品質レベル (0-4, デフォルト: 2) */\n quality?: UastcQuality\n /** 圧縮レベル (0-5, デフォルト: 3) */\n compressionLevel?: number\n /** ミップマップ生成 (デフォルト: false) */\n generateMipmaps?: boolean\n /** Zstandard超圧縮を使用 (デフォルト: true) */\n supercompression?: boolean\n}\n\n/** KTX2圧縮結果 */\nexport interface Ktx2CompressionResult {\n /** 圧縮後のKTX2バイナリデータ */\n data: Uint8Array\n /** 元のサイズ (bytes) */\n originalSize: number\n /** 圧縮後のサイズ (bytes) */\n compressedSize: number\n /** 画像の幅 */\n width: number\n /** 画像の高さ */\n height: number\n}\n\n/** エラー型 */\nexport type CompressionError =\n | { type: 'WASM_LOAD_ERROR'; message: string }\n | { type: 'COMPRESSION_ERROR'; message: string }\n | { type: 'INVALID_INPUT'; message: string }\n\n/**\n * BasisEncoderモジュールの型定義\n * Emscriptenでビルドされたbasis_encoder.jsが公開するAPI\n */\nexport interface BasisEncoderModule {\n /** BasisEncoderクラス */\n BasisEncoder: new () => BasisEncoder\n /** LDR画像タイプ列挙(生ピクセルデータにはcRGBA32を使用) */\n ldr_image_type: {\n cRGBA32: { value: number }\n cPNGImage: { value: number }\n cJPGImage: { value: number }\n }\n}\n\n/**\n * BasisEncoderインスタンスの型定義\n */\nexport interface BasisEncoder {\n /** KTX2ファイル生成を設定 */\n setCreateKTX2File(create: boolean): void\n /** UASTC超圧縮(Zstandard)を設定 */\n setKTX2UASTCSupercompression(enable: boolean): void\n /** UASTCモードを有効化 */\n setUASTC(enable: boolean): void\n /** スライスソース画像を設定(LDR) */\n setSliceSourceImage(\n sliceIndex: number,\n imageData: Uint8Array,\n width: number,\n height: number,\n imageType: number,\n ): boolean\n /** UASTC品質フラグを設定 */\n setPackUASTCFlags(flags: number): void\n /** 圧縮レベルを設定 */\n setCompressionLevel(level: number): void\n /** ミップマップ生成を設定 */\n setMipGen(generate: boolean): void\n /** エンコードを実行 */\n encode(outputBuffer: Uint8Array): number\n /** リソースを解放 */\n delete(): void\n}\n\n/** BASIS WASM ファクトリ関数の型 */\nexport type BasisModuleFactory = (\n moduleOverrides?: Record<string, unknown>,\n) => Promise<BasisEncoderModule>\n","/**\n * KTX2テクスチャ圧縮ロジック\n */\n\nimport { errAsync, ResultAsync } from 'neverthrow'\nimport { initBasisEncoder } from './encoder'\nimport {\n BasisEncoderModule,\n CompressionError,\n Ktx2CompressionOptions,\n Ktx2CompressionResult,\n UastcQuality,\n} from './types'\n\n/** デフォルトの圧縮オプション */\nconst DEFAULT_OPTIONS: Required<Ktx2CompressionOptions> = {\n quality: UastcQuality.Default,\n compressionLevel: 3,\n generateMipmaps: true,\n supercompression: true,\n}\n\n/** 出力バッファの最大サイズ(24MB) */\nconst MAX_OUTPUT_SIZE = 1024 * 1024 * 24\n\n/**\n * RGBAピクセルデータをKTX2形式に圧縮\n *\n * @param imageData - RGBAピクセルデータ(width * height * 4 bytes)\n * @param width - 画像の幅\n * @param height - 画像の高さ\n * @param options - 圧縮オプション\n * @returns 圧縮結果\n */\nexport function compressToKtx2(\n imageData: Uint8Array,\n width: number,\n height: number,\n options?: Ktx2CompressionOptions,\n): ResultAsync<Ktx2CompressionResult, CompressionError> {\n // 入力検証\n const validationError = validateInput(imageData, width, height)\n if (validationError) {\n return errAsync(validationError)\n }\n\n const opts = { ...DEFAULT_OPTIONS, ...options }\n\n return initBasisEncoder().andThen((module) =>\n ResultAsync.fromPromise(\n encodeWithBasis(module, imageData, width, height, opts),\n (error) => ({\n type: 'COMPRESSION_ERROR' as const,\n message: `KTX2エンコードに失敗: ${error instanceof Error ? error.message : String(error)}`,\n }),\n ),\n )\n}\n\n/**\n * 入力データを検証\n * @returns エラーがあればCompressionError、なければnull\n */\nfunction validateInput(\n imageData: Uint8Array,\n width: number,\n height: number,\n): CompressionError | null {\n if (width <= 0 || height <= 0) {\n return {\n type: 'INVALID_INPUT',\n message: `幅と高さは正の値である必要があります: width=${width}, height=${height}`,\n }\n }\n\n const expectedSize = width * height * 4\n if (imageData.length !== expectedSize) {\n return {\n type: 'INVALID_INPUT',\n message: `画像データサイズが不正です: expected=${expectedSize}, actual=${imageData.length}`,\n }\n }\n\n // 2のべき乗チェック(推奨だが必須ではない)\n if (!isPowerOfTwo(width) || !isPowerOfTwo(height)) {\n console.warn(\n `[texture-compression] 幅・高さが2のべき乗でない場合、一部のGPUで問題が発生する可能性があります: ${width}x${height}`,\n )\n }\n\n return null\n}\n\n/**\n * 2のべき乗かチェック\n */\nfunction isPowerOfTwo(n: number): boolean {\n return n > 0 && (n & (n - 1)) === 0\n}\n\n/**\n * BasisEncoderを使用してエンコード\n */\nasync function encodeWithBasis(\n module: BasisEncoderModule,\n imageData: Uint8Array,\n width: number,\n height: number,\n options: Required<Ktx2CompressionOptions>,\n): Promise<Ktx2CompressionResult> {\n const encoder = new module.BasisEncoder()\n\n try {\n // KTX2出力を有効化\n encoder.setCreateKTX2File(true)\n\n // UASTCモードを有効化\n encoder.setUASTC(true)\n\n // 超圧縮(Zstandard)を設定\n encoder.setKTX2UASTCSupercompression(options.supercompression)\n\n // 品質設定\n encoder.setPackUASTCFlags(options.quality)\n\n // 圧縮レベル\n encoder.setCompressionLevel(options.compressionLevel)\n\n // ミップマップ生成\n encoder.setMipGen(options.generateMipmaps)\n\n // ソース画像を設定(RGBA32生データ)\n const success = encoder.setSliceSourceImage(\n 0,\n imageData,\n width,\n height,\n module.ldr_image_type.cRGBA32.value,\n )\n\n if (!success) {\n throw new Error('ソース画像の設定に失敗しました')\n }\n\n // 出力バッファを確保\n const outputBuffer = new Uint8Array(MAX_OUTPUT_SIZE)\n\n // エンコード実行\n const outputSize = encoder.encode(outputBuffer)\n\n if (outputSize === 0) {\n throw new Error('エンコード結果が空です')\n }\n\n // 結果を切り出し\n const ktx2Data = new Uint8Array(outputBuffer.buffer, 0, outputSize)\n\n return {\n data: ktx2Data.slice(), // コピーを返す\n originalSize: imageData.length,\n compressedSize: outputSize,\n width,\n height,\n }\n } finally {\n // リソース解放\n encoder.delete()\n }\n}\n\n/**\n * Y軸を反転したピクセルデータを生成\n * WebGLテクスチャ座標系(左下原点)からKTX2座標系(左上原点)への変換\n *\n * @param imageData - 元のRGBAピクセルデータ\n * @param width - 画像の幅\n * @param height - 画像の高さ\n * @returns Y軸反転されたピクセルデータ\n */\nexport function flipImageY(\n imageData: Uint8Array,\n width: number,\n height: number,\n): Uint8Array {\n const rowSize = width * 4\n const flipped = new Uint8Array(imageData.length)\n\n for (let y = 0; y < height; y++) {\n const srcOffset = y * rowSize\n const dstOffset = (height - 1 - y) * rowSize\n flipped.set(imageData.subarray(srcOffset, srcOffset + rowSize), dstOffset)\n }\n\n return flipped\n}\n","/**\n * BasisEncoder WASM モジュールのロードと管理\n * ブラウザ環境専用\n */\n\nimport { ResultAsync } from 'neverthrow'\n// @ts-expect-error - Emscripten module\nimport BASIS from '../wasm/basis_encoder.js'\n// @ts-expect-error - Vite ?url import\nimport wasmUrl from '../wasm/basis_encoder.wasm?url'\nimport { BasisEncoderModule, CompressionError } from './types'\n\n/** モジュールキャッシュ */\nlet cachedModule: BasisEncoderModule | null = null\n\n/**\n * BasisEncoder WASMモジュールを初期化(ブラウザ環境専用)\n *\n * @returns 初期化されたBasisEncoderModule\n */\nexport function initBasisEncoder(): ResultAsync<\n BasisEncoderModule,\n CompressionError\n> {\n // キャッシュがあれば返す\n if (cachedModule) {\n return ResultAsync.fromSafePromise(Promise.resolve(cachedModule))\n }\n\n return ResultAsync.fromPromise(loadBasisModule(), (error) => ({\n type: 'WASM_LOAD_ERROR' as const,\n message: `Basis WASM モジュールの読み込みに失敗: ${error instanceof Error ? error.message : String(error)}`,\n })).map((module) => {\n cachedModule = module\n return module\n })\n}\n\n/**\n * BasisEncoderモジュールを解放\n */\nexport function disposeBasisEncoder(): void {\n cachedModule = null\n}\n\n/**\n * キャッシュされたモジュールを取得(初期化済みの場合のみ)\n */\nexport function getCachedBasisEncoder(): BasisEncoderModule | null {\n return cachedModule\n}\n\n/**\n * WASMがすでにロード済みかチェック\n */\nexport function isBasisEncoderReady(): boolean {\n return cachedModule !== null\n}\n\n/** 拡張されたモジュール型(initializeBasisを含む) */\ninterface BasisEncoderModuleWithInit extends BasisEncoderModule {\n initializeBasis?: () => void\n}\n\n/**\n * BASISモジュールをロード\n * locateFileでWASMのURLを指定\n */\nasync function loadBasisModule(): Promise<BasisEncoderModule> {\n const moduleObj = (await BASIS({\n locateFile: () => wasmUrl,\n })) as BasisEncoderModuleWithInit\n\n // Basisエンコーダーの初期化(必須)\n if (moduleObj.initializeBasis) {\n moduleObj.initializeBasis()\n }\n\n // BasisEncoderクラスが存在するか確認\n if (!moduleObj.BasisEncoder) {\n throw new Error('BasisEncoder class not found in module after init')\n }\n\n return moduleObj as BasisEncoderModule\n}\n"],"mappings":"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","names":["UastcQuality","import_neverthrow","import_basis_encoder","module","BASIS","wasmUrl","module"]}
@@ -0,0 +1,147 @@
1
+ import { ResultAsync } from 'neverthrow';
2
+
3
+ /**
4
+ * テクスチャ圧縮関連の型定義
5
+ */
6
+ /** UASTC品質レベル */
7
+ declare enum UastcQuality {
8
+ /** 最速、最低品質 */
9
+ Fastest = 0,
10
+ /** 高速 */
11
+ Faster = 1,
12
+ /** デフォルト */
13
+ Default = 2,
14
+ /** 低速、高品質 */
15
+ Slower = 3,
16
+ /** 最高品質 */
17
+ VerySlow = 4
18
+ }
19
+ /** KTX2圧縮オプション */
20
+ interface Ktx2CompressionOptions {
21
+ /** UASTC品質レベル (0-4, デフォルト: 2) */
22
+ quality?: UastcQuality;
23
+ /** 圧縮レベル (0-5, デフォルト: 3) */
24
+ compressionLevel?: number;
25
+ /** ミップマップ生成 (デフォルト: false) */
26
+ generateMipmaps?: boolean;
27
+ /** Zstandard超圧縮を使用 (デフォルト: true) */
28
+ supercompression?: boolean;
29
+ }
30
+ /** KTX2圧縮結果 */
31
+ interface Ktx2CompressionResult {
32
+ /** 圧縮後のKTX2バイナリデータ */
33
+ data: Uint8Array;
34
+ /** 元のサイズ (bytes) */
35
+ originalSize: number;
36
+ /** 圧縮後のサイズ (bytes) */
37
+ compressedSize: number;
38
+ /** 画像の幅 */
39
+ width: number;
40
+ /** 画像の高さ */
41
+ height: number;
42
+ }
43
+ /** エラー型 */
44
+ type CompressionError = {
45
+ type: 'WASM_LOAD_ERROR';
46
+ message: string;
47
+ } | {
48
+ type: 'COMPRESSION_ERROR';
49
+ message: string;
50
+ } | {
51
+ type: 'INVALID_INPUT';
52
+ message: string;
53
+ };
54
+ /**
55
+ * BasisEncoderモジュールの型定義
56
+ * Emscriptenでビルドされたbasis_encoder.jsが公開するAPI
57
+ */
58
+ interface BasisEncoderModule {
59
+ /** BasisEncoderクラス */
60
+ BasisEncoder: new () => BasisEncoder;
61
+ /** LDR画像タイプ列挙(生ピクセルデータにはcRGBA32を使用) */
62
+ ldr_image_type: {
63
+ cRGBA32: {
64
+ value: number;
65
+ };
66
+ cPNGImage: {
67
+ value: number;
68
+ };
69
+ cJPGImage: {
70
+ value: number;
71
+ };
72
+ };
73
+ }
74
+ /**
75
+ * BasisEncoderインスタンスの型定義
76
+ */
77
+ interface BasisEncoder {
78
+ /** KTX2ファイル生成を設定 */
79
+ setCreateKTX2File(create: boolean): void;
80
+ /** UASTC超圧縮(Zstandard)を設定 */
81
+ setKTX2UASTCSupercompression(enable: boolean): void;
82
+ /** UASTCモードを有効化 */
83
+ setUASTC(enable: boolean): void;
84
+ /** スライスソース画像を設定(LDR) */
85
+ setSliceSourceImage(sliceIndex: number, imageData: Uint8Array, width: number, height: number, imageType: number): boolean;
86
+ /** UASTC品質フラグを設定 */
87
+ setPackUASTCFlags(flags: number): void;
88
+ /** 圧縮レベルを設定 */
89
+ setCompressionLevel(level: number): void;
90
+ /** ミップマップ生成を設定 */
91
+ setMipGen(generate: boolean): void;
92
+ /** エンコードを実行 */
93
+ encode(outputBuffer: Uint8Array): number;
94
+ /** リソースを解放 */
95
+ delete(): void;
96
+ }
97
+
98
+ /**
99
+ * KTX2テクスチャ圧縮ロジック
100
+ */
101
+
102
+ /**
103
+ * RGBAピクセルデータをKTX2形式に圧縮
104
+ *
105
+ * @param imageData - RGBAピクセルデータ(width * height * 4 bytes)
106
+ * @param width - 画像の幅
107
+ * @param height - 画像の高さ
108
+ * @param options - 圧縮オプション
109
+ * @returns 圧縮結果
110
+ */
111
+ declare function compressToKtx2(imageData: Uint8Array, width: number, height: number, options?: Ktx2CompressionOptions): ResultAsync<Ktx2CompressionResult, CompressionError>;
112
+ /**
113
+ * Y軸を反転したピクセルデータを生成
114
+ * WebGLテクスチャ座標系(左下原点)からKTX2座標系(左上原点)への変換
115
+ *
116
+ * @param imageData - 元のRGBAピクセルデータ
117
+ * @param width - 画像の幅
118
+ * @param height - 画像の高さ
119
+ * @returns Y軸反転されたピクセルデータ
120
+ */
121
+ declare function flipImageY(imageData: Uint8Array, width: number, height: number): Uint8Array;
122
+
123
+ /**
124
+ * BasisEncoder WASM モジュールのロードと管理
125
+ * ブラウザ環境専用
126
+ */
127
+
128
+ /**
129
+ * BasisEncoder WASMモジュールを初期化(ブラウザ環境専用)
130
+ *
131
+ * @returns 初期化されたBasisEncoderModule
132
+ */
133
+ declare function initBasisEncoder(): ResultAsync<BasisEncoderModule, CompressionError>;
134
+ /**
135
+ * BasisEncoderモジュールを解放
136
+ */
137
+ declare function disposeBasisEncoder(): void;
138
+ /**
139
+ * キャッシュされたモジュールを取得(初期化済みの場合のみ)
140
+ */
141
+ declare function getCachedBasisEncoder(): BasisEncoderModule | null;
142
+ /**
143
+ * WASMがすでにロード済みかチェック
144
+ */
145
+ declare function isBasisEncoderReady(): boolean;
146
+
147
+ export { type CompressionError, type Ktx2CompressionOptions, type Ktx2CompressionResult, UastcQuality, compressToKtx2, disposeBasisEncoder, flipImageY, getCachedBasisEncoder, initBasisEncoder, isBasisEncoderReady };
@@ -0,0 +1,147 @@
1
+ import { ResultAsync } from 'neverthrow';
2
+
3
+ /**
4
+ * テクスチャ圧縮関連の型定義
5
+ */
6
+ /** UASTC品質レベル */
7
+ declare enum UastcQuality {
8
+ /** 最速、最低品質 */
9
+ Fastest = 0,
10
+ /** 高速 */
11
+ Faster = 1,
12
+ /** デフォルト */
13
+ Default = 2,
14
+ /** 低速、高品質 */
15
+ Slower = 3,
16
+ /** 最高品質 */
17
+ VerySlow = 4
18
+ }
19
+ /** KTX2圧縮オプション */
20
+ interface Ktx2CompressionOptions {
21
+ /** UASTC品質レベル (0-4, デフォルト: 2) */
22
+ quality?: UastcQuality;
23
+ /** 圧縮レベル (0-5, デフォルト: 3) */
24
+ compressionLevel?: number;
25
+ /** ミップマップ生成 (デフォルト: false) */
26
+ generateMipmaps?: boolean;
27
+ /** Zstandard超圧縮を使用 (デフォルト: true) */
28
+ supercompression?: boolean;
29
+ }
30
+ /** KTX2圧縮結果 */
31
+ interface Ktx2CompressionResult {
32
+ /** 圧縮後のKTX2バイナリデータ */
33
+ data: Uint8Array;
34
+ /** 元のサイズ (bytes) */
35
+ originalSize: number;
36
+ /** 圧縮後のサイズ (bytes) */
37
+ compressedSize: number;
38
+ /** 画像の幅 */
39
+ width: number;
40
+ /** 画像の高さ */
41
+ height: number;
42
+ }
43
+ /** エラー型 */
44
+ type CompressionError = {
45
+ type: 'WASM_LOAD_ERROR';
46
+ message: string;
47
+ } | {
48
+ type: 'COMPRESSION_ERROR';
49
+ message: string;
50
+ } | {
51
+ type: 'INVALID_INPUT';
52
+ message: string;
53
+ };
54
+ /**
55
+ * BasisEncoderモジュールの型定義
56
+ * Emscriptenでビルドされたbasis_encoder.jsが公開するAPI
57
+ */
58
+ interface BasisEncoderModule {
59
+ /** BasisEncoderクラス */
60
+ BasisEncoder: new () => BasisEncoder;
61
+ /** LDR画像タイプ列挙(生ピクセルデータにはcRGBA32を使用) */
62
+ ldr_image_type: {
63
+ cRGBA32: {
64
+ value: number;
65
+ };
66
+ cPNGImage: {
67
+ value: number;
68
+ };
69
+ cJPGImage: {
70
+ value: number;
71
+ };
72
+ };
73
+ }
74
+ /**
75
+ * BasisEncoderインスタンスの型定義
76
+ */
77
+ interface BasisEncoder {
78
+ /** KTX2ファイル生成を設定 */
79
+ setCreateKTX2File(create: boolean): void;
80
+ /** UASTC超圧縮(Zstandard)を設定 */
81
+ setKTX2UASTCSupercompression(enable: boolean): void;
82
+ /** UASTCモードを有効化 */
83
+ setUASTC(enable: boolean): void;
84
+ /** スライスソース画像を設定(LDR) */
85
+ setSliceSourceImage(sliceIndex: number, imageData: Uint8Array, width: number, height: number, imageType: number): boolean;
86
+ /** UASTC品質フラグを設定 */
87
+ setPackUASTCFlags(flags: number): void;
88
+ /** 圧縮レベルを設定 */
89
+ setCompressionLevel(level: number): void;
90
+ /** ミップマップ生成を設定 */
91
+ setMipGen(generate: boolean): void;
92
+ /** エンコードを実行 */
93
+ encode(outputBuffer: Uint8Array): number;
94
+ /** リソースを解放 */
95
+ delete(): void;
96
+ }
97
+
98
+ /**
99
+ * KTX2テクスチャ圧縮ロジック
100
+ */
101
+
102
+ /**
103
+ * RGBAピクセルデータをKTX2形式に圧縮
104
+ *
105
+ * @param imageData - RGBAピクセルデータ(width * height * 4 bytes)
106
+ * @param width - 画像の幅
107
+ * @param height - 画像の高さ
108
+ * @param options - 圧縮オプション
109
+ * @returns 圧縮結果
110
+ */
111
+ declare function compressToKtx2(imageData: Uint8Array, width: number, height: number, options?: Ktx2CompressionOptions): ResultAsync<Ktx2CompressionResult, CompressionError>;
112
+ /**
113
+ * Y軸を反転したピクセルデータを生成
114
+ * WebGLテクスチャ座標系(左下原点)からKTX2座標系(左上原点)への変換
115
+ *
116
+ * @param imageData - 元のRGBAピクセルデータ
117
+ * @param width - 画像の幅
118
+ * @param height - 画像の高さ
119
+ * @returns Y軸反転されたピクセルデータ
120
+ */
121
+ declare function flipImageY(imageData: Uint8Array, width: number, height: number): Uint8Array;
122
+
123
+ /**
124
+ * BasisEncoder WASM モジュールのロードと管理
125
+ * ブラウザ環境専用
126
+ */
127
+
128
+ /**
129
+ * BasisEncoder WASMモジュールを初期化(ブラウザ環境専用)
130
+ *
131
+ * @returns 初期化されたBasisEncoderModule
132
+ */
133
+ declare function initBasisEncoder(): ResultAsync<BasisEncoderModule, CompressionError>;
134
+ /**
135
+ * BasisEncoderモジュールを解放
136
+ */
137
+ declare function disposeBasisEncoder(): void;
138
+ /**
139
+ * キャッシュされたモジュールを取得(初期化済みの場合のみ)
140
+ */
141
+ declare function getCachedBasisEncoder(): BasisEncoderModule | null;
142
+ /**
143
+ * WASMがすでにロード済みかチェック
144
+ */
145
+ declare function isBasisEncoderReady(): boolean;
146
+
147
+ export { type CompressionError, type Ktx2CompressionOptions, type Ktx2CompressionResult, UastcQuality, compressToKtx2, disposeBasisEncoder, flipImageY, getCachedBasisEncoder, initBasisEncoder, isBasisEncoderReady };