はじめに
jsの小数をfloat32の形式でbase64文字列に変換します。そのあとで復元し、float32の形式で取得します。
コード全文
スコープが面倒なのでp5.jsの仕様をお借りしています。
function setup() {
createCanvas(400, 400);
const x = Math.PI;
const b64s = float32ToBase64(x);
console.log(b64s);
const f = base64ToFloat32(b64s);
console.log(f);
}
function float32ToBase64(x){
console.log(x); // 3.141592653589793
const buf = new ArrayBuffer(4);
const dataView = new DataView(buf);
dataView.setFloat32(0, x, true); // true:リトルエンディアン
// バイト文字列
let byteStrings = "";
for(let k=0; k<4; k++){
byteStrings += String.fromCharCode(dataView.getUint8(k));
}
console.log(byteStrings); // ÛI@
const b64s = btoa(byteStrings);
console.log(b64s); // 2w9JQA==
return b64s;
}
function base64ToFloat32(b64s){
console.log(b64s); // 2w9JQA==
const byteStrings = atob(b64s);
console.log(byteStrings); // ÛI@
const buf = new ArrayBuffer(byteStrings.length);
const dataView = new DataView(buf);
for(let k=0; k<4; k++){
dataView.setUint8(k, byteStrings.charCodeAt(k));
}
const f = dataView.getFloat32(0, true); // trueにしないとエンディアンがバグる
console.log(f); // 3.1415927410125732
return f;
}
float32からbase64への変換
まずfloat32は4バイトなので、4バイトの長さのarrayBufferを確保します。これは配列ではなく、ただの4バイトの長さの「領域」です。ここからDataViewクラスを作成します。これは読み書き用のモジュールです。紐付けられたバッファに自由に数を書き込み、また取り出すことができます
function float32ToBase64(x){
console.log(x); // 3.141592653589793
const buf = new ArrayBuffer(4);
const dataView = new DataView(buf);
dataView.setFloat32(0, x, true); // true:リトルエンディアン
// バイト文字列
let byteStrings = "";
for(let k=0; k<4; k++){
byteStrings += String.fromCharCode(dataView.getUint8(k));
}
console.log(byteStrings); // ÛI@
const b64s = btoa(byteStrings);
console.log(b64s); // 2w9JQA==
return b64s;
}
trueを指定しているのはリトルエンディアンで入れたいからです。Float32Arrayなどの仕様がデフォルトでリトルエンディアンなので基本的にそうしています。デフォルトはビッグエンディアンなので明示する必要があります。そのあと、バイトごとにfromCharCodeで文字列を取得し、つなげています。byteStringsができました。0,1,2,3のうち1番目は制御文字なのか表示されません。この謎の文字列にbtoaをかますと、
console.log(b64s); // 2w9JQA==
「2w9JQA==」が得られました。これがbase64形式のfloat32における円周率です。
base64からfloat32への変換
逆変換はこのプロセスを逆にたどるだけです。
function base64ToFloat32(b64s){
console.log(b64s); // 2w9JQA==
const byteStrings = atob(b64s);
console.log(byteStrings); // ÛI@
const buf = new ArrayBuffer(byteStrings.length);
const dataView = new DataView(buf);
for(let k=0; k<4; k++){
dataView.setUint8(k, byteStrings.charCodeAt(k));
}
const f = dataView.getFloat32(0, true); // trueにしないとエンディアンがバグる
console.log(f); // 3.1415927410125732
return f;
}
まずatobでbase64文字列をバイト文字列に変換します。この配列の長さがバイト長なので、同じ長さのバッファを確保します(buf)。dataViewを作り、書き込みます。書き込むにはバイト文字列の各文字を0~255の数値に変換し、uintの形式で入力します。ここからgetFloat32でfloat32形式の数値を出します。入れるときにリトルエンディアン(true)で入れたので出す時もリトルエンディアン(true)で出します。無事出てきました。32bitに補正されているので、最初にconsole.logで出した値とは異なりますが、glsl内部などではこの数値が使われているわけです。
おわりに
gltfの取り扱いでbase64の取り扱いが出てきたので自分用に整理したいと思いました。ここまでお読みただいてありがとうございました
参考にしたサイト
[JavaScript] ArrayBufferについて調べてみた
ArrayBuffer, binary arrays
ArrayBuffer mdnのサイト
DataView mdnのサイト
リトルエンディアン