Commit c2a60661 authored by Vadym Gidulian's avatar Vadym Gidulian

1.0.0

parent 5e381194
/dist/
/.nyc_output/
# Dependency directories
node_modules/
# Logs
logs/
*.log
npm-debug.log*
# Optional npm cache directory
.npm/
# Optional REPL history
.node_repl_history/
*
!/src/**/*
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2019-11-17
### Added
- Implemented basic data types (`v`, `vt`, `vn`, `g`, `o`, `s`, `f`)
# objb
A binary representation of obj file format.
## Usage
### `objToObjb(source[, options])`
Converts obj file to objb.
- `source` `string`
Obj file content.
- `options`
- `useFloat16` `[boolean]` Default: `false`
Use `float16` instead of `float32`. May result in decreased model accuracy.
Returns `ArrayBuffer` with objb file content.
### `objbToObj(buffer)`
Converts objb file to obj.
- `buffer` `ArrayBuffer`
Objb file content.
Returns `string` with obj file content.
## File format
- Header: 8 bytes
- Version: 2 bytes
- Major: `uint8`
- Minor: `uint8`
- Options: 6 bytes
- useFloat16: 0<sup>th</sup> bit of 0<sup>th</sup> byte
- Body
- &lt;Section&gt;*
#### Section
- Keyword: `uint8`
One of `v (0x1)`, `vn (0x2)`, `vt (0x3)`, `g (0x4)`, `o (0x5)`, `s (0x6)`, `f (0x7)`.
- Length: `uint32 (BE)`
Length of the section data.
- Data:
One of the following depending on the keyword:
- `v`: 3 &times; (`float32`/`float16`)
Section data length is a number of geometric vertices.
- `vt`: 3 &times; (`float32`/`float16`)
Section data length is a number of texture vertices.
- `vn`: 3 &times; (`float32`/`float16`)
Section data length is a number of vertex normals.
- `g`: &lt;string&gt;?
Section data length is a string length.
- `o`: &lt;string&gt;?
Section data length is a string length.
- `s`: &lt;string&gt;?
Section data length is a string length.
- `f`:
Section data length is a number of faces.
- `L`: `uint8`
Number of vertices in the face.
- `bits`: `uint8`
Number of bits used to reference vertex data (`8`, `16` or `32`).
- Data: `L` &times; (3 &times; `uint[bits]`)
`0` means that the reference is omitted.
#### String
- Data: `L` &times; `uint16`,
where `L` is a string length.
{
"name": "@gviagroup/objb",
"version": "0.1.0"
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"test": "nyc ava --verbose"
},
"dependencies": {
"@petamoriken/float16": "^1.1.1"
},
"devDependencies": {
"ava": "^2.4.0",
"nyc": "^14.1.1"
}
}
'use strict';
const {getFloat16, setFloat16} = require('@petamoriken/float16');
const KEYWORDS = {
v: 0x1,
vn: 0x2,
vt: 0x3,
g: 0x4,
o: 0x5,
s: 0x6,
f: 0x7
};
module.exports = {
objToObjb,
objbToObj
};
function objToObjb(source, {useFloat16 = false} = {}) {
const buffer = new ArrayBuffer(Math.max(1024, source.length));
const view = new DataView(buffer);
view.setUint16(0, 0x0100);
if (useFloat16) view.setUint8(2, 1);
let offset = 8;
const lines = source
.split(/\r\n|\r|\n/)
.map(line => line.trim())
.filter(Boolean);
let currentSection = null;
let currentSectionLength = 0;
let currentSectionOffset = null;
let vCount = 0;
let vtCount = 0;
let vnCount = 0;
for (const line of lines) {
const [keyword, ...args] = line.split(/\s+/);
writeSection(keyword);
switch (keyword) {
case 'f':
view.setUint8(offset++, args.length);
const bits = Math.max(8, 2**Math.ceil(Math.log2(Math.log2(Math.max(vCount, vtCount, vnCount)+1))));
view.setUint8(offset++, bits);
for (const arg of args) {
const nums = arg.split('/');
for (let i = 0; i < 3; i++) {
view[`setUint${bits}`](offset, +(nums[i] || 0));
offset += bits / 8;
}
}
currentSectionLength++;
break;
case 'g':
case 'o':
case 's':
writeString(args.join(' '));
break;
case 'v':
writeTriplet(args);
vCount++;
break;
case 'vn':
writeTriplet(args);
vnCount++;
break;
case 'vt':
writeTriplet(args);
vtCount++;
break;
}
}
writeSection(null);
return buffer.slice(0, offset);
function writeSection(keyword) {
if (currentSection === keyword) return;
if (currentSection) view.setUint32(currentSectionOffset+1, currentSectionLength);
if (!Object.keys(KEYWORDS).includes(keyword)) return;
currentSection = keyword;
currentSectionLength = 0;
currentSectionOffset = offset;
view.setUint8(offset, KEYWORDS[keyword]);
offset += 5;
}
function writeString(string) {
for (let i = 0; i < string.length; i++) {
view.setUint16(offset, string.charCodeAt(i));
offset += 2;
}
currentSectionLength += string.length;
}
function writeTriplet(args) {
for (let i = 0; i < 3; i++) {
if (useFloat16) {
setFloat16(view, offset, +args[i] || 0);
offset += 2;
} else {
view.setFloat32(offset, +args[i] || 0);
offset += 4;
}
}
currentSectionLength++;
}
}
function objbToObj(buffer) {
const view = new DataView(buffer);
let offset = 0;
let source = '';
try {
if (view.getUint16(0) !== 0x0100) throw new SyntaxError('Unknown version');
const useFloat16 = !!(view.getUint8(2) & 1);
offset += 8;
for ( ; offset < buffer.byteLength; ) {
switch (view.getUint8(offset++)) {
case KEYWORDS.v:
readTriplet('v');
break;
case KEYWORDS.vn:
readTriplet('vn');
break;
case KEYWORDS.vt:
readTriplet('vt');
break;
case KEYWORDS.g:
readString('g');
break;
case KEYWORDS.o:
readString('o');
break;
case KEYWORDS.s:
readString('s');
break;
case KEYWORDS.f:
const length = view.getUint32(offset);
offset += 4;
for (let i = 0; i < length; i++) {
source += `f`;
const L = view.getUint8(offset++);
const bits = view.getUint8(offset++);
for (let j = 0; j < L; j++) {
let triplet = [];
for (let k = 0; k < 3; k++) {
triplet.push(view[`getUint${bits}`](offset) || null);
offset += bits / 8;
}
source += ` ${triplet.join('/').replace(/\/+$/, '')}`;
}
source += '\n';
}
break;
default: throw new SyntaxError(`Invalid sequence at offset 0x${(offset-1).toString(16)}`);
}
}
return source;
function readString(keyword) {
const length = view.getUint32(offset);
offset += 4;
const codes = Array(length);
for (let i = 0; i < length; i++) {
codes[i] = view.getUint16(offset);
offset += 2;
}
source += `${keyword} ${String.fromCharCode.apply(null, codes)}\n`;
}
function readTriplet(keyword) {
const length = view.getUint32(offset);
offset += 4;
for (let i = 0; i < length * 3; i++) {
if (!(i % 3)) source += keyword;
if (useFloat16) {
source += ` ${getFloat16(view, offset)}`;
offset += 2;
} else {
source += ` ${view.getFloat32(offset)}`;
offset += 4;
}
if (i && (i % 3) === 2) source += '\n';
}
}
} catch (e) {
throw new SyntaxError(`File was not parsed correctly. Probably invalid data structure. Reason: ${e.message}`);
}
}
'use strict';
module.exports = require('../src/index');
This diff is collapsed.
'use strict';
const fs = require('fs');
const path = require('path');
const test = require('ava');
const {objToObjb, objbToObj} = require('./_lib');
const HEADER = [0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0];
test('Empty', t => {
t.deepEqual(objToObjb(''), new Uint8Array(HEADER).buffer);
});
test('Ignore comments', t => {
const source = `
# Comment
`;
t.deepEqual(objToObjb(source), new Uint8Array(HEADER).buffer);
});
test('v[1]', t => {
t.deepEqual(objbToObj(objToObjb('v 1 2 3')), `v 1 2 3
`);
});
test('v[2]', t => {
t.deepEqual(objbToObj(objToObjb(`
v 1 2 3
v 4 5 6
`)), `v 1 2 3
v 4 5 6
`);
});
test('v empty', t => {
t.deepEqual(objbToObj(objToObjb(`v`)), `v 0 0 0\n`);
});
test('v(5)', t => {
t.deepEqual(objbToObj(objToObjb(`v 1 2 3 4 5`)), `v 1 2 3\n`);
});
test('vn[1]', t => {
t.deepEqual(objbToObj(objToObjb(`vn 1 2 3`)), `vn 1 2 3
`);
});
test('vn[2]', t => {
t.deepEqual(objbToObj(objToObjb(`vn 1 2 3
vn 4 5 6
`)), `vn 1 2 3
vn 4 5 6
`);
});
test('vn empty', t => {
t.deepEqual(objbToObj(objToObjb(`vn`)), `vn 0 0 0\n`);
});
test('vn(5)', t => {
t.deepEqual(objbToObj(objToObjb(`vn 1 2 3 4 5`)), `vn 1 2 3\n`);
});
test('vt(u,v,w)[1]', t => {
t.deepEqual(objbToObj(objToObjb(`vt 1 2 3`)), `vt 1 2 3
`);
});
test('vt(u,v,w)[2]', t => {
t.deepEqual(objbToObj(objToObjb(`vt 1 2 3
vt 4 5 6
`)), `vt 1 2 3
vt 4 5 6
`);
});
test('vt(u,v)[1]', t => {
t.deepEqual(objbToObj(objToObjb(`vt 1 2`)), `vt 1 2 0
`);
});
test('vt(u,v)[2]', t => {
t.deepEqual(objbToObj(objToObjb(`vt 1 2
vt 3 4
`)), `vt 1 2 0
vt 3 4 0
`);
});
test('vt(u)[1]', t => {
t.deepEqual(objbToObj(objToObjb(`vt 1`)), `vt 1 0 0
`);
});
test('vt(u)[2]', t => {
t.deepEqual(objbToObj(objToObjb(`vt 1
vt 2
`)), `vt 1 0 0
vt 2 0 0
`);
});
test('vt empty', t => {
t.deepEqual(objbToObj(objToObjb(`vt`)), `vt 0 0 0\n`);
});
test('vt(5)', t => {
t.deepEqual(objbToObj(objToObjb(`vt 1 2 3 4 5`)), `vt 1 2 3\n`);
});
test('g', t => {
t.deepEqual(objbToObj(objToObjb(`g group`)), `g group
`);
});
test('g[2]', t => {
t.deepEqual(objbToObj(objToObjb(`g group1 group2`)), `g group1 group2
`);
});
test('g empty', t => {
t.deepEqual(objbToObj(objToObjb(`g`)), `g \n`);
});
test('o', t => {
t.deepEqual(objbToObj(objToObjb(`o name`)), `o name
`);
});
test('o[2]', t => {
t.deepEqual(objbToObj(objToObjb(`o name1 name2`)), `o name1 name2
`);
});
test('o empty', t => {
t.deepEqual(objbToObj(objToObjb(`o`)), `o \n`);
});
test('s <number>', t => {
t.deepEqual(objbToObj(objToObjb(`s 1`)), `s 1
`);
});
test('s off', t => {
t.deepEqual(objbToObj(objToObjb(`s off`)), `s off
`);
});
test('s empty', t => {
t.deepEqual(objbToObj(objToObjb(`s`)), `s \n`);
});
test('f(3)(v,vt,vn)[1]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1/2/3 4/5/6 7/8/9`)), `f 1/2/3 4/5/6 7/8/9
`);
});
test('f(3)(v,vt)[1]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1/2 3/4 5/6`)), `f 1/2 3/4 5/6
`);
});
test('f(3)(v,vn)[1]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1//2 3//4 5//6`)), `f 1//2 3//4 5//6
`);
});
test('f(3)(v)[1]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1 2 3`)), `f 1 2 3
`);
});
test('f(4)(v,vt,vn)[1]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1/2/3 4/5/6 7/8/9 10/11/12`)), `f 1/2/3 4/5/6 7/8/9 10/11/12
`);
});
test('f(4)(v,vt)[1]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1/2 3/4 5/6 7/8`)), `f 1/2 3/4 5/6 7/8
`);
});
test('f(4)(v,vn)[1]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1//2 3//4 5//6 7//8`)), `f 1//2 3//4 5//6 7//8
`);
});
test('f(4)(v)[1]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1 2 3 4`)), `f 1 2 3 4
`);
});
test('f(3)(v,vt,vn)[2]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1/2/3 4/5/6 7/8/9
f 10/11/12 13/14/15 16/17/18
`)), `f 1/2/3 4/5/6 7/8/9
f 10/11/12 13/14/15 16/17/18
`);
});
test('f(3)(v,vt)[2]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1/2 3/4 5/6
f 7/8 9/10 11/12
`)), `f 1/2 3/4 5/6
f 7/8 9/10 11/12
`);
});
test('f(3)(v,vn)[2]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1//2 3//4 5//6
f 7//8 9//10 11//12
`)), `f 1//2 3//4 5//6
f 7//8 9//10 11//12
`);
});
test('f(3)(v)[2]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1 2 3
f 4 5 6
`)), `f 1 2 3
f 4 5 6
`);
});
test('f(4)(v,vt,vn)[2]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1/2/3 4/5/6 7/8/9 10/11/12
f 13/14/15 16/17/18 19/20/21 22/23/24
`)), `f 1/2/3 4/5/6 7/8/9 10/11/12
f 13/14/15 16/17/18 19/20/21 22/23/24
`);
});
test('f(4)(v,vt)[2]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1/2 3/4 5/6 7/8
f 9/10 11/12 13/14 15/16
`)), `f 1/2 3/4 5/6 7/8
f 9/10 11/12 13/14 15/16
`);
});
test('f(4)(v,vn)[2]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1//2 3//4 5//6 7//8
f 9//10 11//12 13//14 15//16
`)), `f 1//2 3//4 5//6 7//8
f 9//10 11//12 13//14 15//16
`);
});
test('f(4)(v)[2]', t => {
t.deepEqual(objbToObj(objToObjb(`f 1 2 3 4
f 5 6 7 8
`)), `f 1 2 3 4
f 5 6 7 8
`);
});
test('Unknown file version', t => {
t.throws(() => {
objbToObj(new Uint8Array([2, 0, 0, 0, 0, 0, 0, 0]).buffer);
}, SyntaxError);
});
test('Invalid data structure', t => {
t.throws(() => {
objbToObj(new Uint8Array([1, 0]).buffer);
}, SyntaxError);
});
test('Should not stuck', t => {
t.timeout(1000);
t.throws(() => {
objbToObj(new Uint8Array([...HEADER, 42]).buffer);
}, SyntaxError);
});
test('Teapot (float32)', t => {
const source = fs.readFileSync(path.join(__dirname, '_teapot.obj'), {encoding: 'utf8'});
const buffer = objToObjb(source);
objbToObj(buffer);
t.pass();
});
test('Teapot (float16)', t => {
const source = fs.readFileSync(path.join(__dirname, '_teapot.obj'), {encoding: 'utf8'});
const buffer = objToObjb(source, {useFloat16: true});
objbToObj(buffer);
t.pass();
});
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment