Refactored mesh into mesh sections

This commit is contained in:
Lucas Dower 2023-10-12 00:59:50 +01:00
parent 8232fe8098
commit 61b32b2b0d
11 changed files with 717 additions and 528 deletions

View File

@ -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,

View File

@ -1,5 +1,5 @@
import { OtS_Mesh } from '../ots_mesh';
export abstract class IImporter {
export abstract class OtS_Importer {
public abstract import(file: ReadableStream<Uint8Array>): Promise<OtS_Mesh>;
}

View File

@ -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<Uint8Array>): Promise<OtS_Mesh> {
// 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<string, Material> = new Map();
const meshMaterials: Map<string, { }> = 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;
}
}

View File

@ -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();
}
}
}

View File

@ -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<Uint8Array>): Promise<OtS_Mesh> {
const reader = file.getReader();
const decoder = new TextDecoder(); //utf8
let lastChunkEndedWithNewline = false;
let result: ReadableStreamReadResult<Uint8Array>;
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<string, boolean>();
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;
}
}

View File

@ -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);
}
}
}
// export function copyMaterial(material: Material): Material {
// if (material.type === 'solid') {
// return OtS_Util.copySolidMaterial(material);
// } else {
// return OtS_Util.copyTexturedMaterial(material);
// }
// }
// }

40
Core/src/ots_materials.ts Normal file
View File

@ -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<number, Material>;
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;
}
}
*/

View File

@ -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<T> = {
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<void, OtS_MeshError> {
// 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<OtS_Triangle> {
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<string, number>();
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,
);
}

View File

@ -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) {

View File

@ -17,4 +17,8 @@ export type WrappedInfo = {}
/** Wrapped simply wraps a payload with a list of warnings/info associated with it */
export type Wrapped<T> = { payload: T, warnings: WrappedWarnings[], info: WrappedInfo[] };
export type BlockPalette = Set<string>;
export type BlockPalette = Set<string>;
export type Result<T, E = Error> =
| { ok: true, value: T }
| { ok: false, error: { code: E, message?: string } };

View File

@ -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();