Begin glTF parser

This commit is contained in:
Szymon Nowakowski 2023-08-28 22:38:14 +02:00
parent c9f2b4edcb
commit 67089f3f33
3 changed files with 519 additions and 3 deletions

View File

@ -11,7 +11,7 @@ export type Camera = OrthographicCamera | PerspectiveCamera;
export interface OrthographicCameraProps {
readonly name?: string;
readonly verticalSize: number;
readonly halfVerticalSize: number;
readonly nearPlane: number;
readonly farPlane: number;
}
@ -39,13 +39,13 @@ export class OrthographicCamera {
constructor({
name = "",
verticalSize,
halfVerticalSize,
nearPlane,
farPlane,
}: OrthographicCameraProps) {
this._name = name;
this._halfVerticalSize = verticalSize;
this._halfVerticalSize = halfVerticalSize;
this._nearPlane = nearPlane;
this._farPlane = farPlane;

View File

@ -1,3 +1,9 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
export function degToRad(angleDeg: number): number {
return angleDeg * Math.PI / 180;
}

510
src/gltf.ts Normal file
View File

@ -0,0 +1,510 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import * as data from "./data";
/* INITIAL SUPPORT PLAN
*
* Basic properties:
* - extensionsRequired:
* - issues error when any extension not supported at least partially
* - extensionsUsed: ignored
* - accessors: used indirectly
* - read when converting mesh
* - sparse: no support
* - issues error
* - animations: no support
* - issues warning
* - no animations emitted
* - asset:
* - version: verified
* - rest: ignored
* - buffers: used indirectly
* - read when converting mesh
* - uri: no support
* - issues error
* - bufferViews: used indirectly
* - read when converting mesh
* - cameras:
* - orthographic:
* - xmag: ignored
* - ymag: converted to halfVerticalSize
* - perspective:
* - aspectRatio: ignored
* - issues warning when provided
* - images:
* - uri: no support
* - issues error
* - materials:
* - name: full support
* - pbrMetallicRoughness:
* - baseColorFactor: full support
* - baseColorTexture: partial support
* - forced texCoord 0
* - issues error when different provided
* - metallicFactor: full support
* - roughnessFactor: full support
* - metallicRoughnessTexture: partial support
* - forced texCoord 0
* - issues error when different provided
* - normalTexture: partial support
* - scale: full support
* - forced texCoord 0
* - issues error when different provided
* - occlusionTexture: partial support
* - strength: full support
* - forced texCoord 1
* - issues error when different provided
* - emissiveTexture: partial support
* - forced texCoord 0
* - issues error when different provided
* - emissiveFactor: full support
* - alphaMode:
* - OPAQUE: full support
* - MASK: no support
* - issues error
* - BLEND: partial support
* - decoded, but not implemented
* - doubleSided: prtial support
* - decoded, but not implemented
*
* Extensions:
* - KHR_lights_punctual
* - name: full support
* - color/intensity: full support
* - converted to color = color * intensity
* - type:
* - directional: full support
* - point: full support
* - spot: no support
* - issues error
* - range: no support
* - issues warning
* - always infite range
* - KHR_materials_emissive_strength: full support
* - converted to emissive = emissive * strength
* - KHR_materials_ior: full support
* - when not provided, glTF's default is used (1.5) intead of oktaeder's (1.45)
* - KHR_materials_ior: full support
* - probably
*/
export interface ParseResult {
readonly cameras: readonly data.Camera[];
readonly materials: readonly data.Material[];
readonly lights: readonly data.Light[];
readonly scenes: readonly data.Scene[];
readonly scene: data.Scene | null;
readonly warnings: readonly ParseError[];
readonly errors: readonly ParseError[];
}
export interface ParseErrorProps {
message: string;
position?: JsonPosition | undefined;
severity: ParseErrorSeverity;
options?: ErrorOptions | undefined;
}
export type ParseErrorSeverity =
| "warning"
| "error"
;
export class ParseError extends Error {
override message: string;
position: JsonPosition | undefined;
severity: ParseErrorSeverity;
constructor({
message,
position,
severity,
options,
}: ParseErrorProps) {
super(message, options);
this.message = message;
this.position = position;
this.severity = severity;
}
}
export interface JsonPosition {
readonly line: number;
readonly column: number;
readonly path: number;
}
export interface ParseOptions {
/**
* When `true`, the parser will throw with a `ParseError` on the first error
* encountered. This includes warnings when `treatWarningsAsErrors` is
* `true`. When `false`, the parser will always return `ParseResult` and is
* never expected to throw. Failures are then communicated with the
* `ParseResult.errors` array.
*
* When this option is `true`, `stopOnFirstError` has no effect.
* @default true
*/
readonly throwOnError?: boolean;
/**
* When `true`, the parser will stop processing on the first error
* encountered. This includes warnings when `treatWarningsAsErrors` is
* `true`. When `false`, the parser will continue processing when it
* encounters an error that it consideres recoverable.
*
* This option has no effect when `throwOnError` is `true`.
* @default true
*/
readonly stopOnFirstError?: boolean;
/**
* When `true`, the parser will treat any encountered warning as a failure
* for the purpose of the other options. Note that regardless of this
* option, the warnings will always be returned in the
* `ParseResult.warnings` array and they will always have their `severity`
* property equal to `"warning"`.
* @default false
*/
readonly treatWarningsAsErrors?: boolean;
}
export async function parse(gltf: ArrayBufferView, {
throwOnError = true,
stopOnFirstError = true,
treatWarningsAsErrors = false,
}: ParseOptions = {}): Promise<ParseResult> {
const cameras: data.Camera[] = [];
const materials: data.Material[] = [];
const lights: data.Light[] = [];
const scenes: data.Scene[] = [];
const scene: data.Scene | null = null;
const warnings: ParseError[] = [];
const errors: ParseError[] = [];
function makeParseResult(): ParseResult {
return Object.freeze({
cameras: Object.freeze(cameras),
materials: Object.freeze(materials),
lights: Object.freeze(lights),
scenes: Object.freeze(scenes),
scene,
warnings: Object.freeze(warnings),
errors: Object.freeze(errors),
});
}
let gltfDataView = new DataView(gltf.buffer, gltf.byteOffset, gltf.byteLength);
// --- GLB HEADER ----------------------------------------------------------
if (gltfDataView.byteLength < 12) {
const message = `glTF buffer view is too short to be a valid binary glTF container. Binary glTF begins with a 12-byte header, but the provided buffer view has byte length of ${gltf.byteLength}`;
const error = new ParseError({ message, severity: "error" });
if (throwOnError) {
throw error;
}
errors.push(error);
// unrecoverable error
return makeParseResult();
}
const magic = gltfDataView.getUint32(0, true);
const version = gltfDataView.getUint32(4, true);
let length = gltfDataView.getUint32(8, true);
if (magic !== 0x46546C67) {
const message = `glTF container has invalid magic bytes. The first four bytes must have a value of 0x46546C67 when read as little endian unsigned integer, but in the provided buffer view they have the value of ${u32toHexString(magic)}`;
const error = new ParseError({ message, severity: "error" });
if (throwOnError) {
throw error;
}
errors.push(error);
/* NOTE This error is considered unrecoverable, because it is very
* likely that when the magic bytes are wrong, the provided buffer
* view points to a completely different format or garbage data and
* it would be pointless to continue parsing in this case.
*/
return makeParseResult();
}
if (version !== 2) {
const message = `Unsupported binary glTF container format. The bytes 4-8 define the binary glTF conatiner format version when read as little endian unsigned integer. Only version 2 is supported, but in the provided buffer they have the value of ${version}`;
const error = new ParseError({ message, severity: "error" });
if (throwOnError) {
throw error;
}
errors.push(error);
// unrecoverable error
return makeParseResult();
}
if (length !== gltf.byteLength) {
const message = `Invalid glTF container length. The bytes 8-12 define the length in bytes of the entirety of the binary glTF container when read as little endian unsigned integer. The container byte length is defined as ${length}, but the provided buffer view has byte length of ${gltf.byteLength}`;
const error = new ParseError({ message, severity: "error" });
if (throwOnError) {
throw error;
}
errors.push(error);
// recovery: use the lower length value and pretend its the actual length
length = Math.min(length, gltf.byteLength);
gltfDataView = new DataView(gltf.buffer, gltf.byteOffset, length);
}
let rest = new DataView(gltf.buffer, gltf.byteOffset + 12, gltf.byteLength - 12);
// --- JSON CHUNK ----------------------------------------------------------
throw new Error("TODO");
// --- BIN CHUNK -----------------------------------------------------------
}
function u32toHexString(value: number) {
return "0x" + ("00000000" + value.toString(16)).slice(-8);
}
// --- GLTF DATA STRUCTURES ----------------------------------------------------
export interface Gltf {
extensionsRequired?: [string, ...string[]];
accessors?: [Accessor, ...Accessor[]];
asset: Asset;
buffers?: [Buffer, ...Buffer[]];
bufferViews?: [BufferView, ...BufferView[]];
cameras?: [Camera, ...Camera[]];
images?: [Image, ...Image[]];
materials?: [Material, ...Material[]];
meshes?: [Mesh, ...Mesh[]];
nodes?: [Node, ...Node[]];
samplers?: [Sampler, ...Sampler[]];
scene?: number;
scenes?: [number, ...number[]];
textures?: [Texture, ...Texture[]];
}
export interface Accessor {
bufferView?: number;
/** @default 0 */
byteOffset?: number;
componentType: ComponentType;
/** @default false */
normalized?: boolean;
count: number;
type: AccessorType;
}
export enum ComponentType {
Byte = 5120,
UnsignedByte = 5121,
Short = 5122,
UnsignedShort = 5123,
UnsignedInt = 5125,
Float = 5126,
}
export type AccessorType =
| "SCALAR"
| "VEC2"
| "VEC3"
| "VEC4"
| "MAT2"
| "MAT3"
| "MAT4"
;
export interface Asset {
version: `${string}.${string}`;
minVersion: `${string}.${string}`;
}
export interface Buffer {
uri?: string;
byteLength: number;
}
export interface BufferView {
buffer: number;
/** @default 0 */
byteOffset?: number;
byteLength: number;
byteStride?: number;
}
export type Camera =
| CameraOrthographic
| CameraPerspective
;
export interface CameraOrthographic {
orthographic: Orthographic;
type: "orthographic";
}
export interface CameraPerspective {
orthographic: Perspective;
type: "perspective";
}
export interface Orthographic {
xmag: number;
ymag: number;
zfar: number;
znear: number;
}
export interface Perspective {
aspectRatio?: number;
yfov: number;
zfar?: number;
znear: number;
}
export interface Image {
uri?: string;
mimeType?: ImageMimeType;
bufferView?: number;
name?: string;
}
export type ImageMimeType =
| "image/jpeg"
| "image/png"
;
export interface Material {
name?: string;
extensions?: MaterialExtensions;
pbrMetallicRoughness?: MaterialPbrMetallicRoughness;
normalTexture?: NormalTextureInfo;
occlusionTexture?: OcclusionTextureInfo;
emissiveTexture?: TextureInfo;
emissiveFactor?: [r: number, g: number, b: number];
alphaMode?: AlphaMode;
/** @default false */
doubleSided?: boolean;
}
export interface MaterialExtensions {
KHR_materials_emissive_strength?: KHR_materials_emissive_strength;
KHR_materials_ior?: KHR_materials_ior;
}
export interface KHR_materials_emissive_strength {
/** @default 1 */
emissiveStrength?: number;
}
export interface KHR_materials_ior {
/** @default 1.5 */
ior?: number;
}
export interface MaterialPbrMetallicRoughness {
/** @default [1, 1, 1, 1] */
baseColorFactor?: [r: number, b: number, g: number, partialCoverage: number];
baseColorTexture?: TextureInfo;
/** @default 1 */
metallicFactor?: number;
/** @default 1 */
roughnessFactor?: number;
metallicRoughnessTexture?: TextureInfo;
}
export interface TextureInfo {
index: number;
/** @default 0 */
texCoord?: number;
}
export interface NormalTextureInfo extends TextureInfo {
/** @default 1 */
scale?: number;
}
export interface OcclusionTextureInfo extends TextureInfo {
/** @default 1 */
strength?: number;
}
export type AlphaMode =
| "OPAQUE"
| "MASK"
| "BLEND"
;
export interface Mesh {
primitives: [Primitive, ...Primitive[]];
name?: string;
}
export interface Primitive {
attributes: {
POSITION?: number,
NORMAL?: number,
TANGENT?: number,
TEXCOORD_0?: number,
TEXCOORD_1?: number,
};
indices?: number;
material?: number;
/** @default PrimitiveMode.Triangles */
mode?: PrimitiveMode;
}
export enum PrimitiveMode {
Points = 0,
Lines = 1,
LineLoop = 2,
LineStrip = 3,
Triangles = 4,
TriangleStrip = 5,
TriangleFan = 6,
}
export interface Sampler {
magFilter?: MagFilter;
minFilter?: MinFilter;
/** @default WrappingMode.Repeat */
wrapS?: WrappingMode;
/** @default WrappingMode.Repeat */
wrapT?: WrappingMode;
}
export enum MagFilter {
Nearest = 9728,
Linear = 9729,
}
export enum MinFilter {
Nearest = 9728,
Linear = 9729,
NearestMipmapNearest = 9984,
LinearMipmapNearest = 9985,
NearestMipmapLinear = 9986,
LinearMipmapLinear = 9987,
}
export enum WrappingMode {
ClampToEdge = 33071,
MirroredRepeat = 33648,
Repeat = 10497,
}
export interface Texture {
sampler?: number;
source?: number;
name?: string;
}