diff --git a/Core/index.ts b/Core/index.ts index 1562c9b..36ac702 100644 --- a/Core/index.ts +++ b/Core/index.ts @@ -1,10 +1,10 @@ -import { ImporterFactory } from './src/importers/importers'; +import { OtS_ImporterFactory } from './src/importers/importers'; import { OtS_Texture } from './src/ots_texture'; import { OtS_VoxelMesh } from './src/ots_voxel_mesh'; import { OtS_VoxelMesh_Converter } from './src/ots_voxel_mesh_converter'; export default { - getImporter: ImporterFactory.GetImporter, + getImporter: OtS_ImporterFactory.GetImporter, texture: OtS_Texture, voxelMeshConverter: OtS_VoxelMesh_Converter, diff --git a/Core/src/importers/base_importer.ts b/Core/src/importers/base_importer.ts index 2c5475a..a94263e 100644 --- a/Core/src/importers/base_importer.ts +++ b/Core/src/importers/base_importer.ts @@ -1,5 +1,5 @@ import { OtS_Mesh } from '../ots_mesh'; -export abstract class IImporter { +export abstract class OtS_Importer { public abstract import(file: ReadableStream): Promise; } \ No newline at end of file diff --git a/Core/src/importers/gltf_loader.ts b/Core/src/importers/gltf_importer.ts similarity index 86% rename from Core/src/importers/gltf_loader.ts rename to Core/src/importers/gltf_importer.ts index aa9090e..45347b3 100644 --- a/Core/src/importers/gltf_loader.ts +++ b/Core/src/importers/gltf_importer.ts @@ -4,25 +4,27 @@ import { GLTFLoader } from '@loaders.gl/gltf'; import { RGBAColours, RGBAUtil } from '../colour'; import { UV } from '../util'; import { Vector3 } from '../vector'; -import { IImporter } from './base_importer'; -import { OtS_Mesh, TEMP_CONVERT_MESH, Tri } from '../ots_mesh'; -import { Material } from '../materials'; +import { OtS_Mesh } from '../ots_mesh'; import { OtS_Texture } from '../ots_texture'; +import { OtS_Importer } from './base_importer'; -export type TGltfImporterError = +export type OtS_GltfImporterError = | { type: 'failed-to-parse' } | { type: 'unsupported-image-format' }; -export class GltfImporterError extends Error { - public error: TGltfImporterError; +interface VertexIndices { + v0: number; + v1: number; + v2: number; +}; - constructor(error: TGltfImporterError) { - super(); - this.error = error; - } -} +export interface Tri { + positionIndices: VertexIndices; + texcoordIndices?: VertexIndices; + material: string; +}; -export class GltfLoader extends IImporter { +export class OtS_Importer_Gltf extends OtS_Importer { public override async import(file: ReadableStream): Promise { // TODO: StatusRework //StatusHandler.warning(LOC('import.gltf_experimental')); @@ -31,7 +33,8 @@ export class GltfLoader extends IImporter { try { gltf = await parse(file, GLTFLoader, { loadImages: true }); } catch (err) { - throw new GltfImporterError({ type: 'failed-to-parse' }); + // TODO: + //throw new GltfImporterError({ type: 'failed-to-parse' }); } return this._handleGLTF(gltf); @@ -39,12 +42,11 @@ export class GltfLoader extends IImporter { private _handleGLTF(gltf: any): OtS_Mesh { const meshVertices: Vector3[] = []; - const meshNormals: Vector3[] = []; + //const meshNormals: Vector3[] = []; const meshTexcoords: UV[] = []; const meshTriangles: Tri[] = []; - const meshMaterials: Map = new Map(); + const meshMaterials: Map = new Map(); meshMaterials.set('NONE', { - name: 'NONE', type: 'solid', colour: RGBAUtil.copy(RGBAColours.WHITE), canBeTextured: false, @@ -64,6 +66,7 @@ export class GltfLoader extends IImporter { )); } } + /* if (attributes.NORMAL !== undefined) { const normals = attributes.NORMAL.value as Float32Array; for (let i = 0; i < normals.length; i += 3) { @@ -74,6 +77,7 @@ export class GltfLoader extends IImporter { )); } } + */ if (attributes.TEXCOORD_0 !== undefined) { const texcoords = attributes.TEXCOORD_0.value as Float32Array; for (let i = 0; i < texcoords.length; i += 2) { @@ -102,7 +106,7 @@ export class GltfLoader extends IImporter { if (mimeType !== 'image/png' && mimeType !== 'image/jpeg') { // TODO: StatusRework //StatusHandler.warning(LOC('import.unsupported_image_type', { file_name: diffuseTexture.texture.source.id, file_type: mimeType })); - throw new GltfImporterError({ type: 'unsupported-image-format' }) + //throw new GltfImporterError({ type: 'unsupported-image-format' }) } const base64 = btoa( @@ -178,14 +182,14 @@ export class GltfLoader extends IImporter { meshTriangles.push({ material: materialNameToUse, positionIndices: { - x: maxIndex + indices[i * 3 + 0], - y: maxIndex + indices[i * 3 + 1], - z: maxIndex + indices[i * 3 + 2], + v0: maxIndex + indices[i * 3 + 0], + v1: maxIndex + indices[i * 3 + 1], + v2: maxIndex + indices[i * 3 + 2], }, texcoordIndices: { - x: maxIndex + indices[i * 3 + 0], - y: maxIndex + indices[i * 3 + 1], - z: maxIndex + indices[i * 3 + 2], + v0: maxIndex + indices[i * 3 + 0], + v1: maxIndex + indices[i * 3 + 1], + v2: maxIndex + indices[i * 3 + 2], }, }); } @@ -200,6 +204,10 @@ export class GltfLoader extends IImporter { }); }); - return TEMP_CONVERT_MESH(meshVertices, meshTexcoords, meshTriangles); + const mesh = OtS_Mesh.create(); + + // TODO: Fix + + return mesh; } } diff --git a/Core/src/importers/importers.ts b/Core/src/importers/importers.ts index ab6d65b..666849c 100644 --- a/Core/src/importers/importers.ts +++ b/Core/src/importers/importers.ts @@ -1,20 +1,16 @@ -import { ASSERT } from '../util/error_util'; -import { IImporter } from './base_importer'; -import { GltfLoader } from './gltf_loader'; -import { ObjImporter } from './obj_importer'; +import { OtS_Importer } from './base_importer'; +import { OtS_Importer_Gltf } from './gltf_importer'; +import { OtS_Importer_Obj } from './obj_importer'; -export type TImporters = 'obj' | 'gltf'; +export type OtS_Importers = 'obj' | 'gltf'; - -export class ImporterFactory { - public static GetImporter(importer: TImporters): IImporter { +export class OtS_ImporterFactory { + public static GetImporter(importer: OtS_Importers): OtS_Importer { switch (importer) { case 'obj': - return new ObjImporter(); + return new OtS_Importer_Obj(); case 'gltf': - return new GltfLoader(); - default: - ASSERT(false); + return new OtS_Importer_Gltf(); } } } diff --git a/Core/src/importers/obj_importer.ts b/Core/src/importers/obj_importer.ts index 4a97b35..e752c4e 100644 --- a/Core/src/importers/obj_importer.ts +++ b/Core/src/importers/obj_importer.ts @@ -1,238 +1,80 @@ import { RGBAColours, RGBAUtil } from '../colour'; -import { anyNaN } from '../math'; -import { OtS_Mesh, TEMP_CONVERT_MESH, Tri } from '../ots_mesh'; +import { OtS_Mesh } from '../ots_mesh'; +import { OtS_Texture } from '../ots_texture'; import { UV } from '../util'; import { ASSERT } from '../util/error_util'; import { RegExpBuilder } from '../util/regex_util'; import { REGEX_NZ_ANY } from '../util/regex_util'; import { REGEX_NUMBER } from '../util/regex_util'; import { Vector3 } from '../vector'; -import { IImporter } from './base_importer'; +import { OtS_Importer } from './base_importer'; -type TObjImporterParser = { - regex: RegExp, - delegate: (match: { [key: string]: string }) => (null | TObjImporterError), -} - -export type TObjImporterError = +export type OtS_ObjImporterError = | { type: 'invalid-encoding' } | { type: 'invalid-material-name', name: string } | { type: 'invalid-data' } | { type: 'failed-to-parse', line: string } - | { type: 'failed-to-parse-essential-token', line: string } + | { type: 'failed-to-parse-essential-token', line: string }; -export class ObjImporterError extends Error { - public error: TObjImporterError; - - constructor(error: TObjImporterError) { - super(); - this.error = error; - } +interface VertexIndices { + v0: number; + v1: number; + v2: number; } -export class ObjImporter extends IImporter { +export interface Tri { + positionIndices: VertexIndices; + texcoordIndices?: VertexIndices; + material: string; +} + +export class OtS_Importer_Obj extends OtS_Importer { private _vertices: Vector3[] = []; - private _normals: Vector3[] = []; private _uvs: UV[] = []; private _tris: Tri[] = []; private _currentMaterialName: string = 'DEFAULT_UNASSIGNED'; - - private _objParsers: TObjImporterParser[] = [ - { - // e.g. 'usemtl my_material' - regex: new RegExpBuilder().add(/^usemtl/).add(/ /).add(REGEX_NZ_ANY, 'name').toRegExp(), - delegate: (match: { [key: string]: string }) => { - this._currentMaterialName = match.name.trim(); - - if (this._currentMaterialName.length === 0) { - const err: TObjImporterError = { type: 'invalid-material-name', name: match.name }; - return err; - } - - return null; - }, - }, - { - // e.g. 'v 0.123 0.456 0.789' - regex: new RegExpBuilder() - .add(/^v/) - .addNonzeroWhitespace() - .add(REGEX_NUMBER, 'x') - .addNonzeroWhitespace() - .add(REGEX_NUMBER, 'y') - .addNonzeroWhitespace() - .add(REGEX_NUMBER, 'z') - .toRegExp(), - delegate: (match: { [key: string]: string }) => { - const x = parseFloat(match.x); - const y = parseFloat(match.y); - const z = parseFloat(match.z); - - if (anyNaN(x, y, z)) { - const err: TObjImporterError = { type: 'invalid-data' }; - return err; - } - - this._vertices.push(new Vector3(x, y, z)); - return null; - }, - }, - { - // e.g. 'vn 0.123 0.456 0.789' - regex: new RegExpBuilder() - .add(/^vn/) - .addNonzeroWhitespace() - .add(REGEX_NUMBER, 'x') - .addNonzeroWhitespace() - .add(REGEX_NUMBER, 'y') - .addNonzeroWhitespace() - .add(REGEX_NUMBER, 'z') - .toRegExp(), - delegate: (match: { [key: string]: string }) => { - const x = parseFloat(match.x); - const y = parseFloat(match.y); - const z = parseFloat(match.z); - - if (anyNaN(x, y, z)) { - const err: TObjImporterError = { type: 'invalid-data' }; - return err; - } - - this._normals.push(new Vector3(x, y, z)); - return null; - }, - }, - { - // e.g. 'vt 0.123 0.456' - regex: new RegExpBuilder() - .add(/^vt/) - .addNonzeroWhitespace() - .add(REGEX_NUMBER, 'u') - .addNonzeroWhitespace() - .add(REGEX_NUMBER, 'v') - .toRegExp(), - delegate: (match: { [key: string]: string }) => { - const u = parseFloat(match.u); - const v = parseFloat(match.v); - - if (anyNaN(u, v)) { - const err: TObjImporterError = { type: 'invalid-data' }; - return err; - } - - this._uvs.push({ u: u, v: v }); - return null; - }, - }, - { - // e.g. 'f 1/2/3 ...' or 'f 1/2 ...' or 'f 1 ...' - regex: new RegExpBuilder() - .add(/^f/) - .addNonzeroWhitespace() - .add(/.*/, 'line') - .toRegExp(), - delegate: (match: { [key: string]: string }) => { - const line = match.line.trim(); - - const vertices = line.split(' ').filter((x) => { - return x.length !== 0; - }); - - if (vertices.length < 3) { - const err: TObjImporterError = { type: 'invalid-data' }; - return err; - } - - const points: { - positionIndex: number; - texcoordIndex?: number; - normalIndex?: number; - }[] = []; - - for (const vertex of vertices) { - const vertexData = vertex.split('/'); - switch (vertexData.length) { - case 1: { - const index = parseInt(vertexData[0]); - points.push({ - positionIndex: index, - texcoordIndex: index, - normalIndex: index, - }); - break; - } - case 2: { - const positionIndex = parseInt(vertexData[0]); - const texcoordIndex = parseInt(vertexData[1]); - points.push({ - positionIndex: positionIndex, - texcoordIndex: texcoordIndex, - }); - break; - } - case 3: { - const positionIndex = parseInt(vertexData[0]); - const texcoordIndex = parseInt(vertexData[1]); - const normalIndex = parseInt(vertexData[2]); - points.push({ - positionIndex: positionIndex, - texcoordIndex: texcoordIndex, - normalIndex: normalIndex, - }); - break; - } - default: - const err: TObjImporterError = { type: 'invalid-data' }; - return err; - } - } - - const pointBase = points[0]; - for (let i = 1; i < points.length - 1; ++i) { - const pointA = points[i]; - const pointB = points[i + 1]; - const tri: Tri = { - positionIndices: { - x: pointBase.positionIndex - 1, - y: pointA.positionIndex - 1, - z: pointB.positionIndex - 1, - }, - material: this._currentMaterialName, - }; - if (pointBase.normalIndex || pointA.normalIndex || pointB.normalIndex) { - ASSERT(pointBase.normalIndex && pointA.normalIndex && pointB.normalIndex); - tri.normalIndices = { - x: pointBase.normalIndex - 1, - y: pointA.normalIndex - 1, - z: pointB.normalIndex - 1, - }; - } - if (pointBase.texcoordIndex || pointA.texcoordIndex || pointB.texcoordIndex) { - ASSERT(pointBase.texcoordIndex && pointA.texcoordIndex && pointB.texcoordIndex); - tri.texcoordIndices = { - x: pointBase.texcoordIndex - 1, - y: pointA.texcoordIndex - 1, - z: pointB.texcoordIndex - 1, - }; - } - this._tris.push(tri); - } - - return null; - }, - }, - ]; - + // Parser context private _linesToParse: string[] = []; + private static _REGEX_USEMTL = new RegExpBuilder() + .add(/^usemtl/) + .add(/ /) + .add(REGEX_NZ_ANY, 'name') + .toRegExp(); + + private static _REGEX_VERTEX = new RegExpBuilder() + .add(/^v/) + .addNonzeroWhitespace() + .add(REGEX_NUMBER, 'x') + .addNonzeroWhitespace() + .add(REGEX_NUMBER, 'y') + .addNonzeroWhitespace() + .add(REGEX_NUMBER, 'z') + .toRegExp(); + + private static _REGEX_TEXCOORD = new RegExpBuilder() + .add(/^vt/) + .addNonzeroWhitespace() + .add(REGEX_NUMBER, 'u') + .addNonzeroWhitespace() + .add(REGEX_NUMBER, 'v') + .toRegExp(); + + private static _REGEX_FACE = new RegExpBuilder() + .add(/^f/) + .addNonzeroWhitespace() + .add(/.*/, 'line') + .toRegExp(); + private _consumeLoadedLines(includeLast: boolean) { const size = includeLast ? this._linesToParse.length : this._linesToParse.length - 1; - + for (let i = 0; i < size; ++i) { const line = this._linesToParse[i]; const { err } = this.parseOBJLine(line); if (err !== null) { - throw new ObjImporterError(err); + // TODO: + //throw new ObjImporterError(err); } } @@ -242,7 +84,7 @@ export class ObjImporter extends IImporter { public override async import(file: ReadableStream): Promise { const reader = file.getReader(); const decoder = new TextDecoder(); //utf8 - + let lastChunkEndedWithNewline = false; let result: ReadableStreamReadResult; do { @@ -267,32 +109,232 @@ export class ObjImporter extends IImporter { lastChunkEndedWithNewline = lastChar === '\n' || lastChar === '\r\n'; } while (!result.done); - return TEMP_CONVERT_MESH(this._vertices, this._uvs, this._tris); + const materials = new Map(); + this._tris.forEach((tri) => { + if (!materials.has(tri.material)) { + materials.set(tri.material, tri.texcoordIndices !== undefined); + } + }); + + const mesh = OtS_Mesh.create(); + for (const [material, isTextureMaterial] of materials) { + if (isTextureMaterial) { + const positionData: number[] = []; + const texcoordData: number[] = []; + const indexData: number[] = []; + let ni = 0; + + this._tris.forEach((tri) => { + if (tri.material === material) { + const p0 = this._vertices[tri.positionIndices.v0]; + const p1 = this._vertices[tri.positionIndices.v1]; + const p2 = this._vertices[tri.positionIndices.v2]; + + ASSERT(tri.texcoordIndices !== undefined); + const t0 = this._uvs[tri.texcoordIndices.v0]; + const t1 = this._uvs[tri.texcoordIndices.v1]; + const t2 = this._uvs[tri.texcoordIndices.v2]; + + positionData.push(p0.x, p0.y, p0.z); + texcoordData.push(t0.u, t0.v); + indexData.push(ni++); + positionData.push(p1.x, p1.y, p1.z); + texcoordData.push(t1.u, t1.v); + indexData.push(ni++); + positionData.push(p2.x, p2.y, p2.z); + texcoordData.push(t2.u, t2.v); + indexData.push(ni++); + } + }); + + mesh.addSection({ + type: 'textured', + texture: OtS_Texture.CreateDebugTexture(), + positionData: Float32Array.from(positionData), + texcoordData: Float32Array.from(texcoordData), + indexData: Uint32Array.from(indexData), + }); + } else { + const positionData: number[] = []; + const indexData: number[] = []; + let ni = 0; + + this._tris.forEach((tri) => { + if (tri.material === material) { + const p0 = this._vertices[tri.positionIndices.v0]; + const p1 = this._vertices[tri.positionIndices.v1]; + const p2 = this._vertices[tri.positionIndices.v2]; + + positionData.push(p0.x, p0.y, p0.z); + indexData.push(ni++); + positionData.push(p1.x, p1.y, p1.z); + indexData.push(ni++); + positionData.push(p2.x, p2.y, p2.z); + indexData.push(ni++); + } + }); + + mesh.addSection({ + type: 'solid', + colour: RGBAUtil.copy(RGBAColours.WHITE), + positionData: Float32Array.from(positionData), + indexData: Uint32Array.from(indexData), + }); + } + } + + return mesh; } /** * Attempts to parse the given line of an OBJ file. * Potentially returns an error if failed to do so. */ - public parseOBJLine(line: string): { err: null | TObjImporterError} { - const essentialTokens = ['usemtl ', 'v ', 'vt ', 'f ', 'vn ']; - - for (const parser of this._objParsers) { - const match = parser.regex.exec(line); - if (match && match.groups) { - const err = parser.delegate(match.groups); - return { err: err }; - } - } - - const beginsWithEssentialToken = essentialTokens.some((token) => { - return line.startsWith(token); - }); - - if (beginsWithEssentialToken) { - return { err: { type: 'failed-to-parse-essential-token', line: line } } - } + public parseOBJLine(line: string): { err: null | OtS_ObjImporterError } { + false + || this._tryParseAsUsemtl(line) + || this._tryParseAsVertex(line) + || this._tryParseAsTexcoord(line) + || this._tryParseAsFace(line); return { err: null }; } + + // e.g. 'usemtl my_material' + private _tryParseAsUsemtl(line: string): boolean { + const match = OtS_Importer_Obj._REGEX_USEMTL.exec(line); + if (match === null) { + return false; + } + + const materialName = match.groups?.name.trim(); + + if (materialName === undefined || materialName.length === 0) { + throw 'Invalid material name'; // TODO: Error type + } + + this._currentMaterialName = materialName; + return true; + } + + // e.g. 'v 0.123 0.456 0.789' + private _tryParseAsVertex(line: string): boolean { + const match = OtS_Importer_Obj._REGEX_VERTEX.exec(line); + if (match === null) { + return false; + } + + const x = parseFloat(match.groups?.x ?? ''); + const y = parseFloat(match.groups?.y ?? ''); + const z = parseFloat(match.groups?.z ?? ''); + + if (isNaN(x) || isNaN(y) || isNaN(z)) { + throw 'Invalid data'; // TODO: Error type + } + + this._vertices.push(new Vector3(x, y, z)); + return true; + } + + // e.g. 'vt 0.123 0.456' + private _tryParseAsTexcoord(line: string): boolean { + const match = OtS_Importer_Obj._REGEX_TEXCOORD.exec(line); + if (match === null) { + return false; + } + + const u = parseFloat(match.groups?.u ?? ''); + const v = parseFloat(match.groups?.v ?? ''); + + if (isNaN(u) || isNaN(v)) { + throw 'Invalid data'; + } + + this._uvs.push({ u: u, v: v }); + return true; + } + + private _tryParseAsFace(line: string): boolean { + const match = OtS_Importer_Obj._REGEX_FACE.exec(line); + if (match === null) { + return false; + } + + const data = match.groups?.line.trim(); + if (data === undefined) { + throw 'Invalid data'; + } + + const vertices = data.split(' ').filter((x) => { + return x.length !== 0; + }); + + if (vertices.length < 3) { + throw 'Invalid data'; + } + + const points: { + positionIndex: number; + texcoordIndex?: number; + }[] = []; + + for (const vertex of vertices) { + const vertexData = vertex.split('/'); + switch (vertexData.length) { + case 1: { + const index = parseInt(vertexData[0]); + points.push({ + positionIndex: index, + texcoordIndex: index, + }); + break; + } + case 2: { + const positionIndex = parseInt(vertexData[0]); + const texcoordIndex = parseInt(vertexData[1]); + points.push({ + positionIndex: positionIndex, + texcoordIndex: texcoordIndex, + }); + break; + } + case 3: { + const positionIndex = parseInt(vertexData[0]); + const texcoordIndex = parseInt(vertexData[1]); + points.push({ + positionIndex: positionIndex, + texcoordIndex: texcoordIndex, + }); + break; + } + default: + throw 'Invalid data'; + } + } + + const pointBase = points[0]; + for (let i = 1; i < points.length - 1; ++i) { + const pointA = points[i]; + const pointB = points[i + 1]; + const tri: Tri = { + positionIndices: { + v0: pointBase.positionIndex - 1, + v1: pointA.positionIndex - 1, + v2: pointB.positionIndex - 1, + }, + material: this._currentMaterialName, + }; + if (pointBase.texcoordIndex || pointA.texcoordIndex || pointB.texcoordIndex) { + ASSERT(pointBase.texcoordIndex && pointA.texcoordIndex && pointB.texcoordIndex); + tri.texcoordIndices = { + v0: pointBase.texcoordIndex - 1, + v1: pointA.texcoordIndex - 1, + v2: pointB.texcoordIndex - 1, + }; + } + this._tris.push(tri); + } + + return true; + } } diff --git a/Core/src/materials.ts b/Core/src/materials.ts index 1ce4fdb..f1fc5bc 100644 --- a/Core/src/materials.ts +++ b/Core/src/materials.ts @@ -1,47 +1,43 @@ import { RGBA, RGBAUtil } from './colour'; import { OtS_Texture } from './ots_texture'; -export type OtS_MaterialType = 'solid' | 'textured'; +// export type OtS_MaterialType = 'solid' | 'textured'; -type BaseMaterial = { - name: string, -} +// type BaseMaterial = { +// //name: string, +// } -export type SolidMaterial = BaseMaterial & { - type: 'solid' - colour: RGBA, - canBeTextured: boolean, -} -export type TexturedMaterial = BaseMaterial & { - type: 'textured', - texture: OtS_Texture, -} +// export type SolidMaterial = BaseMaterial & { +// type: 'solid' +// //canBeTextured: boolean, +// } +// export type TexturedMaterial = BaseMaterial & { +// type: 'textured', +// texture: OtS_Texture, +// } -export type Material = SolidMaterial | TexturedMaterial; +// export type Material = SolidMaterial | TexturedMaterial; -export namespace OtS_Util { - export function copySolidMaterial(material: SolidMaterial): SolidMaterial { - return { - type: 'solid', - name: material.name, - colour: RGBAUtil.copy(material.colour), - canBeTextured: material.canBeTextured, - }; - } +// export namespace OtS_Util { +// export function copySolidMaterial(material: SolidMaterial): SolidMaterial { +// return { +// type: 'solid', +// colour: RGBAUtil.copy(material.colour), +// }; +// } - export function copyTexturedMaterial(material: TexturedMaterial): TexturedMaterial { - return { - type: 'textured', - name: material.name, - texture: material.texture.copy(), - } - } +// export function copyTexturedMaterial(material: TexturedMaterial): TexturedMaterial { +// return { +// type: 'textured', +// texture: material.texture.copy(), +// } +// } - export function copyMaterial(material: Material): Material { - if (material.type === 'solid') { - return OtS_Util.copySolidMaterial(material); - } else { - return OtS_Util.copyTexturedMaterial(material); - } - } -} \ No newline at end of file +// export function copyMaterial(material: Material): Material { +// if (material.type === 'solid') { +// return OtS_Util.copySolidMaterial(material); +// } else { +// return OtS_Util.copyTexturedMaterial(material); +// } +// } +// } \ No newline at end of file diff --git a/Core/src/ots_materials.ts b/Core/src/ots_materials.ts new file mode 100644 index 0000000..f8433ca --- /dev/null +++ b/Core/src/ots_materials.ts @@ -0,0 +1,40 @@ +import { RGBA } from "./colour"; +//import { Material, OtS_Util } from "./materials"; +import { OtS_Texture } from "./ots_texture"; + +export type OtS_MeshSection = { positionData: Float32Array, indexData: Uint32Array } & ( + | { type: 'solid', colour: RGBA } + | { type: 'colour', colourData: Float32Array } + | { type: 'textured', texcoordData: Float32Array, texture: OtS_Texture }); + +/* +export class OtS_MaterialSlots { + private _slots: Map; + + private constructor() { + this._slots = new Map(); + } + + public static create() { + return new OtS_MaterialSlots(); + } + + public setSlot(index: number, material: Material) { + this._slots.set(index, material); + } + + public getSlot(index: number) { + return this._slots.get(index); + } + + public copy() { + const clone = OtS_MaterialSlots.create(); + + this._slots.forEach((value, key) => { + clone.setSlot(key, OtS_Util.copyMaterial(value)); + }); + + return clone; + } +} +*/ \ No newline at end of file diff --git a/Core/src/ots_mesh.ts b/Core/src/ots_mesh.ts index b289d27..522bed9 100644 --- a/Core/src/ots_mesh.ts +++ b/Core/src/ots_mesh.ts @@ -1,70 +1,105 @@ import { Bounds } from './bounds'; -import { RGBAColours, RGBAUtil } from './colour'; -import { Material, OtS_Util, SolidMaterial } from './materials'; +import { RGBA } from './colour'; import { degreesToRadians } from './math'; +import { OtS_MeshSection } from './ots_materials'; +import { OtS_Texture } from './ots_texture'; import { UV } from "./util"; +import { ASSERT } from './util/error_util'; +import { Result } from './util/type_util'; import { Vector3 } from "./vector"; -// TODO: Nuke -interface VertexIndices { - x: number; - y: number; - z: number; +type OtS_VertexData = { + v0: T, + v1: T, + v2: T, } -// TODO: Nuke -export interface Tri { - positionIndices: VertexIndices; - texcoordIndices?: VertexIndices; - normalIndices?: VertexIndices; - material: string; -} +export type OtS_Triangle = + | { type: 'solid', colour: RGBA, data: OtS_VertexData<{ position: Vector3 }> } + | { type: 'coloured', data: OtS_VertexData<{ position: Vector3, colour: RGBA }> } + | { type: 'textured', texture: OtS_Texture, data: OtS_VertexData<{ position: Vector3, texcoord: UV }> } -export type OtS_Vertex = { - position: Vector3, - texcoord: UV, -}; - -export type OtS_Triangle = { - v0: OtS_Vertex, - v1: OtS_Vertex, - v2: OtS_Vertex, - material: Material, -}; +type OtS_MeshError = 'bad-height' | 'bad-material-match' | 'bad-geometry'; export class OtS_Mesh { - private _materials: Material[]; + private _sections: OtS_MeshSection[]; + //private _geometry: OtS_Geometry; + //private _materials: OtS_MaterialSlots; // [p0, p1, p2] - private _positionData: Float32Array; + //private _positionData: Float32Array; private static _POSITION_STRIDE = 3; // [u, v] - private _texcoordData: Float32Array; + //private _texcoordData: Float32Array; private static _TEXCOORD_STRIDE = 2; + private static _COLOUR_STRIDE = 2; + + //private static _MATERIAL_STRIDE = 4; // [position_index * 3, texcoord_index * 3, material_index] - private _triangleData: Uint32Array; + //private _triangleData: Uint32Array; private static _TRIANGLE_STRIDE = 7; - public constructor(positionData: Float32Array, texcoordData: Float32Array, triangleData: Uint32Array, materials: Material[]) { - this._materials = materials; - this._positionData = positionData; - this._texcoordData = texcoordData; - this._triangleData = triangleData; + private constructor() { + this._sections = []; + } + + public static create() { + return new this(); + /* + // TODO: Check non-zero height + if (false) { + return { ok: false, error: { + code: 'bad-height', + message: 'Geometry should have a non-zero height, consider rotating the mesh' + }}; + } + + // TODO: Check geometry using materials slots is valid, i.e. a triangle that uses a + // textured material must have texcoords + if (false) { + return { ok: false, error: { + code: 'bad-material-match', + message: 'Material \'x\' uses a textured material but has no texcoords' + }}; + } + + // TODO: Check material slots used by geometry are defined + + if (!geometry.hasAttribute('position')) { + return { ok: false, error: { + code: 'bad-geometry', + message: 'Missing position data' + }}; + } + + const mesh = new this(geometry, materials); + return { ok: true, value: mesh }; + */ + } + + public addSection(section: OtS_MeshSection): Result { + // TODO: Validation + this._sections.push(section); + return { ok: true, value: undefined }; } public translate(x: number, y: number, z: number) { - for (let i = 0; i < this._positionData.length; i += 3) { - this._positionData[i + 0] += x; - this._positionData[i + 1] += y; - this._positionData[i + 2] += z; - } + this._sections.forEach((section) => { + for (let i = 0; i < section.positionData.length; i += 3) { + section.positionData[i + 0] += x; + section.positionData[i + 1] += y; + section.positionData[i + 2] += z; + } + }); } public scale(s: number) { - for (let i = 0; i < this._positionData.length; i += 3) { - this._positionData[i + 0] *= s; - this._positionData[i + 1] *= s; - this._positionData[i + 2] *= s; - } + this._sections.forEach((section) => { + for (let i = 0; i < section.positionData.length; i += 3) { + section.positionData[i + 0] *= s; + section.positionData[i + 1] *= s; + section.positionData[i + 2] *= s; + } + }); } public centre() { @@ -72,16 +107,17 @@ export class OtS_Mesh { this.translate(-centre.x, -centre.y, -centre.z); } - public normalise() { + public normalise(): boolean { const bounds = this.calcBounds(); const size = Vector3.sub(bounds.max, bounds.min); const scaleFactor = 1.0 / size.y; if (isNaN(scaleFactor) || !isFinite(scaleFactor)) { - throw 'Could not normalize'; + return false; } this.scale(scaleFactor); + return true; } public rotate(pitch: number, roll: number, yaw: number) { @@ -106,174 +142,203 @@ export class OtS_Mesh { const Azy = cosb*sinc; const Azz = cosb*cosc; - for (let i = 0; i < this._positionData.length; i += 3) { - const px = this._positionData[i + 0]; - const py = this._positionData[i + 1]; - const pz = this._positionData[i + 2]; - - this._positionData[i + 0] = Axx * px + Axy * py + Axz * pz; - this._positionData[i + 1] = Ayx * px + Ayy * py + Ayz * pz; - this._positionData[i + 2] = Azx * px + Azy * py + Azz * pz; - } - } - - public setMaterial(newMaterial: Material): boolean { - for (let i = 0; i < this._materials.length; ++i) { - const oldMaterial = this._materials[i]; - if (oldMaterial.name === newMaterial.name) { - if (oldMaterial.type === 'solid' && newMaterial.type === 'textured' && !oldMaterial.canBeTextured) { - return false; // Assigning a texture material to a non-textureable material - } - - // TODO: Check newMaterial is valid - - this._materials[i] = newMaterial; - return true; + this._sections.forEach((section) => { + for (let i = 0; i < section.positionData.length; i += 3) { + const px = section.positionData[i + 0]; + const py = section.positionData[i + 1]; + const pz = section.positionData[i + 2]; + + section.positionData[i + 0] = Axx * px + Axy * py + Axz * pz; + section.positionData[i + 1] = Ayx * px + Ayy * py + Ayz * pz; + section.positionData[i + 2] = Azx * px + Azy * py + Azz * pz; } - } - - return false; // No material found under that name + }); } + /** + * @note Returns a reference to the underlying materials, modifying these is dangerous + */ public getTriangles(): IterableIterator { - const triangleCount = this._triangleData.length / OtS_Mesh._TRIANGLE_STRIDE; + const sectionCount = this._sections.length; + ASSERT(sectionCount > 0); // TODO: Don't assert, + + let sectionIndex = 0; + let triangleCount = this._sections[0].positionData.length / 3; let triangleIndex = 0; - const getVertex = (positionIndex: number, texcoordIndex: number) => { - return { - position: new Vector3( - this._positionData[positionIndex * OtS_Mesh._POSITION_STRIDE + 0], - this._positionData[positionIndex * OtS_Mesh._POSITION_STRIDE + 1], - this._positionData[positionIndex * OtS_Mesh._POSITION_STRIDE + 2], - ), - texcoord: { - u: this._texcoordData[texcoordIndex * OtS_Mesh._TEXCOORD_STRIDE + 0], - v: this._texcoordData[texcoordIndex * OtS_Mesh._TEXCOORD_STRIDE + 1], - }, - } - } - return { [Symbol.iterator]: function () { return this; }, next: () => { + if (triangleIndex >= triangleCount && sectionIndex < sectionCount) { + ++sectionIndex; + triangleCount = this._sections[sectionIndex].positionData.length / 3; + } + if (triangleIndex < triangleCount) { - const dataIndex = triangleIndex * OtS_Mesh._TRIANGLE_STRIDE; + const section = this._sections[sectionIndex]; - const positionIndex0 = this._triangleData[dataIndex + 0]; - const positionIndex1 = this._triangleData[dataIndex + 1]; - const positionIndex2 = this._triangleData[dataIndex + 2]; - const texcoordIndex0 = this._triangleData[dataIndex + 3]; - const texcoordIndex1 = this._triangleData[dataIndex + 4]; - const texcoordIndex2 = this._triangleData[dataIndex + 5]; - const materialIndex = this._triangleData[dataIndex + 6]; - - const triangle: OtS_Triangle = { - material: this._materials[materialIndex], - v0: getVertex(positionIndex0, texcoordIndex0), - v1: getVertex(positionIndex1, texcoordIndex1), - v2: getVertex(positionIndex2, texcoordIndex2), - }; + const index0 = section.indexData[triangleIndex * 3 + 0]; + const index1 = section.indexData[triangleIndex * 3 + 1]; + const index2 = section.indexData[triangleIndex * 3 + 2]; ++triangleIndex; - return { done: false, value: triangle }; - } else { - return { done: true, value: undefined }; + switch (section.type) { + case 'solid': { + const triangle: OtS_Triangle = { + type: 'solid', + colour: section.colour, + data: { + v0: { + position: new Vector3( + section.positionData[index0 * 3 + 0], + section.positionData[index0 * 3 + 1], + section.positionData[index0 * 3 + 2], + ), + }, + v1: { + position: new Vector3( + section.positionData[index1 * 3 + 0], + section.positionData[index1 * 3 + 1], + section.positionData[index1 * 3 + 2], + ), + }, + v2: { + position: new Vector3( + section.positionData[index2 * 3 + 0], + section.positionData[index2 * 3 + 1], + section.positionData[index2 * 3 + 2], + ), + }, + }, + } + return { done: false, value: triangle }; + } + case 'colour': { + const triangle: OtS_Triangle = { + type: 'coloured', + data: { + v0: { + position: new Vector3( + section.positionData[index0 * 3 + 0], + section.positionData[index0 * 3 + 1], + section.positionData[index0 * 3 + 2], + ), + colour: { + r: section.colourData[index0 * 4 + 0], + g: section.colourData[index0 * 4 + 1], + b: section.colourData[index0 * 4 + 2], + a: section.colourData[index0 * 4 + 3], + }, + }, + v1: { + position: new Vector3( + section.positionData[index1 * 3 + 0], + section.positionData[index1 * 3 + 1], + section.positionData[index1 * 3 + 2], + ), + colour: { + r: section.colourData[index1 * 4 + 0], + g: section.colourData[index1 * 4 + 1], + b: section.colourData[index1 * 4 + 2], + a: section.colourData[index1 * 4 + 3], + }, + }, + v2: { + position: new Vector3( + section.positionData[index2 * 3 + 0], + section.positionData[index2 * 3 + 1], + section.positionData[index2 * 3 + 2], + ), + colour: { + r: section.colourData[index2 * 4 + 0], + g: section.colourData[index2 * 4 + 1], + b: section.colourData[index2 * 4 + 2], + a: section.colourData[index2 * 4 + 3], + }, + }, + }, + } + return { done: false, value: triangle }; + } + case 'textured': { + const triangle: OtS_Triangle = { + type: 'textured', + texture: section.texture, + data: { + v0: { + position: new Vector3( + section.positionData[index0 * 3 + 0], + section.positionData[index0 * 3 + 1], + section.positionData[index0 * 3 + 2], + ), + texcoord: { + u: section.texcoordData[index0 * 2 + 0], + v: section.texcoordData[index0 * 2 + 1], + }, + }, + v1: { + position: new Vector3( + section.positionData[index1 * 3 + 0], + section.positionData[index1 * 3 + 1], + section.positionData[index1 * 3 + 2], + ), + texcoord: { + u: section.texcoordData[index1 * 2 + 0], + v: section.texcoordData[index1 * 2 + 1], + }, + }, + v2: { + position: new Vector3( + section.positionData[index2 * 3 + 0], + section.positionData[index2 * 3 + 1], + section.positionData[index2 * 3 + 2], + ), + texcoord: { + u: section.texcoordData[index2 * 2 + 0], + v: section.texcoordData[index2 * 2 + 1], + }, + }, + }, + } + return { done: false, value: triangle }; + } + } } + + return { done: true, value: undefined }; }, }; } public copy() { - return new OtS_Mesh( - this._positionData.slice(0), - this._texcoordData.slice(0), - this._triangleData.slice(0), - this._materials.map((material) => { - return OtS_Util.copyMaterial(material); - }), - ); + const clone = OtS_Mesh.create(); + + for (const section of this._sections) { + const success = clone.addSection(section).ok; + ASSERT(success); + } + + return clone; } public calcBounds() { const bounds = Bounds.getEmptyBounds(); - + const vec = new Vector3(0, 0, 0); - for (let i = 0; i < this._positionData.length; i += 3) { - vec.set( - this._positionData[i + 0], - this._positionData[i + 1], - this._positionData[i + 2], - ); - bounds.extendByPoint(vec); - } + this._sections.forEach((section) => { + for (let i = 0; i < section.positionData.length; i += 3) { + vec.set( + section.positionData[i + 0], + section.positionData[i + 1], + section.positionData[i + 2], + ); + bounds.extendByPoint(vec); + } + }); return bounds; } - - public getMaterials() { - return this._materials.map((material) => { - return OtS_Util.copyMaterial(material); - }); - } - - public getTriangleCount() { - return this._triangleData.length / OtS_Mesh._TRIANGLE_STRIDE; - } -} - -export function TEMP_CONVERT_MESH(vertices: Vector3[], uvs: UV[], tris: Tri[]): OtS_Mesh { - const positionData = new Float32Array(3 * vertices.length); - vertices.forEach((vertex, index) => { - positionData[index * 3 + 0] = vertex.x; - positionData[index * 3 + 1] = vertex.y; - positionData[index * 3 + 2] = vertex.z; - }); - - const texcoordData = new Float32Array(2 * uvs.length); - uvs.forEach((uv, index) => { - texcoordData[index * 2 + 0] = uv.u; - texcoordData[index * 2 + 1] = uv.v; - }); - - const materialNameToIndex = new Map(); - const materials: Material[] = []; - - const triangleData = new Uint32Array(7 * tris.length); - tris.forEach((tri, index) => { - const materialName = tri.material; - let materialIndex = materialNameToIndex.get(materialName); - if (materialIndex === undefined) { - materialIndex = materialNameToIndex.size; - materialNameToIndex.set(materialName, materialIndex); - - const hasTexcoords = tri.texcoordIndices !== undefined; - - const material: SolidMaterial = { - name: materialName, - canBeTextured: hasTexcoords, - colour: RGBAUtil.copy(RGBAColours.WHITE), - type: 'solid', - } - - materials.push(material); - } - - triangleData[index * 7 + 0] = tri.positionIndices.x; - triangleData[index * 7 + 1] = tri.positionIndices.y; - triangleData[index * 7 + 2] = tri.positionIndices.z; - triangleData[index * 7 + 3] = tri.texcoordIndices?.x ?? 0; - triangleData[index * 7 + 4] = tri.texcoordIndices?.y ?? 0; - triangleData[index * 7 + 5] = tri.texcoordIndices?.z ?? 0; - triangleData[index * 7 + 6] = materialIndex; - }); - - return new OtS_Mesh( - positionData, - texcoordData, - triangleData, - materials, - ); } \ No newline at end of file diff --git a/Core/src/ots_voxel_mesh_converter.ts b/Core/src/ots_voxel_mesh_converter.ts index 9371990..fcf17bc 100644 --- a/Core/src/ots_voxel_mesh_converter.ts +++ b/Core/src/ots_voxel_mesh_converter.ts @@ -8,6 +8,8 @@ import { Bounds } from './bounds'; import { RGBA, RGBAColours, RGBAUtil } from './colour'; import { OtS_Mesh, OtS_Triangle } from './ots_mesh'; import { ASSERT } from './util/error_util'; +import { OtS_Texture } from './ots_texture'; +import { UV } from './util'; export type OtS_VoxelMesh_ConverterConfig = { constraintAxis: TAxis, @@ -70,14 +72,14 @@ export class OtS_VoxelMesh_Converter { private _voxeliseTri(mesh: OtS_Mesh, voxelMesh: OtS_VoxelMesh, triangle: OtS_Triangle) { this._rays.reset(); - this._generateRays(triangle.v0.position, triangle.v1.position, triangle.v2.position); + this._generateRays(triangle.data.v0.position, triangle.data.v1.position, triangle.data.v2.position); const voxelPosition = new Vector3(0, 0, 0); const size = this._rays.size(); for (let i = 0; i < size; ++i) { const ray = this._rays.get(i)!; - const intersection = rayIntersectTriangle(ray, triangle.v0.position, triangle.v1.position, triangle.v2.position); + const intersection = rayIntersectTriangle(ray, triangle.data.v0.position, triangle.data.v1.position, triangle.data.v2.position); if (intersection) { switch (ray.axis) { case Axes.x: @@ -122,31 +124,38 @@ export class OtS_VoxelMesh_Converter { } private _getVoxelColour(mesh: OtS_Mesh, triangle: OtS_Triangle, location: Vector3): RGBA { - if (triangle.material.type === 'solid') { - return RGBAUtil.copy(triangle.material.colour); + if (triangle.type === 'solid') { + return triangle.colour; } - const area01 = Triangle.CalcArea(triangle.v0.position, triangle.v1.position, location); - const area12 = Triangle.CalcArea(triangle.v1.position, triangle.v2.position, location); - const area20 = Triangle.CalcArea(triangle.v2.position, triangle.v0.position, location); + const area01 = Triangle.CalcArea(triangle.data.v0.position, triangle.data.v1.position, location); + const area12 = Triangle.CalcArea(triangle.data.v1.position, triangle.data.v2.position, location); + const area20 = Triangle.CalcArea(triangle.data.v2.position, triangle.data.v0.position, location); const total = area01 + area12 + area20; const w0 = area12 / total; const w1 = area20 / total; const w2 = area01 / total; - const uv = { - u: triangle.v0.texcoord.u * w0 + triangle.v1.texcoord.u * w1 + triangle.v2.texcoord.u * w2, - v: triangle.v0.texcoord.v * w0 + triangle.v1.texcoord.v * w1 + triangle.v2.texcoord.v * w2, + if (triangle.type === 'coloured') { + return { + r: triangle.data.v0.colour.r * w0 + triangle.data.v1.colour.r * w1 * triangle.data.v2.colour.r * w2, + g: triangle.data.v0.colour.g * w0 + triangle.data.v1.colour.g * w1 * triangle.data.v2.colour.g * w2, + b: triangle.data.v0.colour.b * w0 + triangle.data.v1.colour.b * w1 * triangle.data.v2.colour.b * w2, + a: triangle.data.v0.colour.a * w0 + triangle.data.v1.colour.a * w1 * triangle.data.v2.colour.a * w2, + }; + } + + const texcoord: UV = { + u: triangle.data.v0.texcoord.u * w0 + triangle.data.v1.texcoord.u * w1 + triangle.data.v2.texcoord.u * w2, + v: triangle.data.v0.texcoord.v * w0 + triangle.data.v1.texcoord.v * w1 + triangle.data.v2.texcoord.v * w2, }; - if (isNaN(uv.u) || isNaN(uv.v)) { + if (isNaN(texcoord.u) || isNaN(texcoord.v)) { RGBAUtil.copy(RGBAColours.MAGENTA); } - ASSERT(triangle.material.type === 'textured'); - const texture = triangle.material.texture; - return texture.sample(uv.u, uv.v); + return triangle.texture.sample(texcoord.u, texcoord.v); } private _generateRays(v0: Vector3, v1: Vector3, v2: Vector3) { diff --git a/Core/src/util/type_util.ts b/Core/src/util/type_util.ts index 716a735..becb412 100644 --- a/Core/src/util/type_util.ts +++ b/Core/src/util/type_util.ts @@ -17,4 +17,8 @@ export type WrappedInfo = {} /** Wrapped simply wraps a payload with a list of warnings/info associated with it */ export type Wrapped = { payload: T, warnings: WrappedWarnings[], info: WrappedInfo[] }; -export type BlockPalette = Set; \ No newline at end of file +export type BlockPalette = Set; + +export type Result = + | { ok: true, value: T } + | { ok: false, error: { code: E, message?: string } }; \ No newline at end of file diff --git a/Sandbox/index.ts b/Sandbox/index.ts index 025ef29..305713e 100644 --- a/Sandbox/index.ts +++ b/Sandbox/index.ts @@ -1,29 +1,58 @@ -import { strict as assert } from 'node:assert'; -import path from 'node:path'; -import OTS from 'ots-core'; - -import { createOtSTexture, createReadableStream } from './src/util'; +import { RGBAColours, RGBAUtil } from 'ots-core/src/colour'; +import { OtS_Mesh } from 'ots-core/src/ots_mesh'; +import { OtS_Texture } from 'ots-core/src/ots_texture'; (async () => { - // 1. Import a mesh - const pathModel = path.join(__dirname, '../res/samples/skull.obj'); - const readableStream = createReadableStream(pathModel); + const mesh = OtS_Mesh.create(); + { + mesh.addSection({ + type: 'solid', + colour: RGBAUtil.copy(RGBAColours.WHITE), + positionData: Float32Array.from([ + 0.0, 0.0, 0.0, + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + ]), + indexData: Uint32Array.from([ + 0, 1, 2 + ]), + }); - const importer = OTS.getImporter('obj'); - const mesh = await importer.import(readableStream); + mesh.addSection({ + type: 'colour', + positionData: Float32Array.from([ + 0.0, 10.0, 0.0, + 1.0, 12.0, 3.0, + 4.0, 15.0, 6.0, + ]), + colourData: Float32Array.from([ + 1.0, 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, 1.0, + ]), + indexData: Uint32Array.from([ + 0, 1, 2 + ]), + }); - // 2. Assign materials - const pathTexture = path.join(__dirname, '../res/samples/skull.jpg'); - const texture = createOtSTexture(pathTexture); - assert(texture !== undefined, `Could not parse ${pathTexture}`); - - // Update the 'skull' material - const success = mesh.setMaterial({ - type: 'textured', - name: 'skull', - texture: texture, - }); - assert(success, 'Could not update skull material'); + mesh.addSection({ + type: 'textured', + texture: new OtS_Texture(new Uint8ClampedArray(), 0, 0, 'nearest', 'repeat'), + positionData: Float32Array.from([ + 0.0, 20.0, 0.0, + 1.0, 22.0, 3.0, + 4.0, 25.0, 6.0, + ]), + texcoordData: Float32Array.from([ + 0.0, 0.0, + 1.0, 0.0, + 0.0, 1.0, + ]), + indexData: Uint32Array.from([ + 0, 1, 2 + ]), + }); + } // 3. Construct a voxel mesh from the mesh const converter = new OTS.voxelMeshConverter();