This commit is contained in:
Lucas Dower 2021-08-07 16:20:31 +01:00
parent fd892fd4c0
commit dd5683d728
6 changed files with 667 additions and 15 deletions

View File

@ -22,16 +22,12 @@
},
"homepage": "https://github.com/LucasDower/ObjToSchematic#readme",
"devDependencies": {
"@types/jquery": "^3.5.6",
"@types/pngjs": "^6.0.1",
"electron": "^13.1.4",
"electron-packager": "^15.2.0",
"expand-vertex-data": "^1.1.2",
"pngjs": "^6.0.0",
"prismarine-nbt": "^1.6.0",
"ts-node": "^10.1.0",
"twgl.js": "^4.19.1",
"typescript": "^4.3.5",
"wavefront-obj-parser": "^2.0.1"
"typescript": "^4.3.5"
},
"dependencies": {
"twgl.js": "^4.19.1",

View File

@ -1,9 +1,9 @@
const { Renderer } = require('./renderer.js');
const { Mesh } = require('./mesh.js');
const { VoxelManager } = require('./voxel_manager.js');
const { Vector3 } = require('./vector.js');
const { Schematic } = require('./schematic.js');
const dialog = require('electron').remote.dialog;
import { Renderer } from "./renderer";
import { Mesh } from "./mesh.js";
import { VoxelManager } from "./voxel_manager.js";
import { Vector3 } from "./vector.js";
import { Schematic } from "./schematic.js";
//const dialog = from 'electron').remote.dialog;
class AppContext {

View File

@ -1,3 +1,5 @@
import { AppContext } from "./dist/app_context.js";
const { AppContext } = require('./dist/app_context.js');
const context = new AppContext();

310
src/mesh.ts Normal file
View File

@ -0,0 +1,310 @@
import * as twgl from "twgl.js";
import * as fs from "fs";
import * as path from "path";
import {expandVertexData} from "expand-vertex-data";
import { Triangle } from "./triangle";
import { Vector3 } from "./vector";
import { RGB } from "./util";
interface objData {
vertexNormals: Array<number>;
vertexUVs: Array<number>;
vertexPositions: Array<number>;
vertexNormalIndices: Array<number>;
vertexUVIndices: Array<number>;
vertexPositionIndices: Array<number>;
vertexMaterial: Array<string>;
};
interface Materials {
[materialName: string]: (FillMaterial | TextureMaterial)
}
interface FillMaterial {
readonly type: MaterialType.Fill
diffuseColour: RGB
}
interface TextureMaterial {
readonly type: MaterialType.Texture
texturePath: string,
texture?: WebGLTexture
}
export enum MaterialType {
Texture,
Fill
}
interface MaterialTriangles {
material: (FillMaterial | TextureMaterial);
triangles: Array<Triangle>
}
export class Mesh {
public materialTriangles: {[materialName: string]: Array<MaterialTriangles>};
private _gl: WebGLRenderingContext;
private objPath: path.ParsedPath;
private mtlPath?: string;
//private _materials: Materials;
private _data: {
position: Float32Array;
texcoord: Float32Array;
indices: Uint16Array;
materialNames: Array<string>;
}
private _materialIndices: {[materialName: string]: Array<number>}
constructor(objPathString: string, gl: WebGLRenderingContext) {
this.objPath = path.parse(objPathString);
this.materialTriangles = {};
this._gl = gl;
// Parse .obj
const wavefrontString = fs.readFileSync(objPathString).toString('utf8');
const parsedJSON = this._parseWavefrontObj(wavefrontString);
if (this.mtlPath) {
if (!path.isAbsolute(this.mtlPath)) {
this.mtlPath = path.join(this.objPath.dir, this.mtlPath);
}
} else {
throw Error("No .mtl file found.");
}
// Parse .mtl
const materialString = fs.readFileSync(this.mtlPath).toString('utf8');
const materials = this._parseMaterial(materialString);
// FIXME: Fix quad faces
const expanded = expandVertexData(parsedJSON, {facesToTriangles: true});
this._data = {
position: expanded.positions,
texcoord: expanded.uvs,
indices: expanded.positionIndices,
materialNames: parsedJSON.vertexMaterial
};
this._materialIndices = {};
for (let i = 0; i < parsedJSON.vertexMaterial.length; ++i) {
const materialName = parsedJSON.vertexMaterial[i];
const index = expanded.positionIndices[i];
if (this._materialIndices[materialName]) {
this._materialIndices[materialName].push(index);
} else {
this._materialIndices[materialName] = [index];
}
}
this._parseTriangles(materials);
this._loadTextures(materials);
}
private _addMaterial(materialsJSON: Materials, materialName: string, materialDiffuseColour: RGB, materialDiffuseTexturePath: string) {
if (materialDiffuseTexturePath !== "") {
materialsJSON[materialName] = {
texturePath: materialDiffuseTexturePath,
type: MaterialType.Texture
};
} else if (materialName !== "") {
materialsJSON[materialName] = {
diffuseColour: materialDiffuseColour,
type: MaterialType.Fill
};
}
}
private _parseMaterial(materialString: string): Materials {
var materialsJSON: Materials = {};
const lines = materialString.split('\n');
let materialName: string = "";
let materialDiffuseColour: RGB = {r: 1.0, g: 1.0, b: 1.0};
let materialDiffuseTexturePath!: string;
for (let i = 0; i < lines.length; ++i) {
const line = lines[i];
const lineTokens = line.trim().split(/\s+/);
switch (lineTokens[0]) {
case "newmtl":
this._addMaterial(materialsJSON, materialName, materialDiffuseColour, materialDiffuseTexturePath);
materialName = lineTokens[1];
break;
case "Kd":
const diffuseColour = lineTokens.slice(1).map(x => parseFloat(x))
if (!diffuseColour || diffuseColour.length != 3) {
throw Error(`Could not parse .mtl file. (Line ${i+1})`);
}
if (diffuseColour.some(x => Number.isNaN(x))) {
throw Error(`Could not parse .mtl file. (Line ${i+1})`);
}
materialDiffuseColour = {
r: diffuseColour[0], g: diffuseColour[1], b: diffuseColour[2]
};
break;
case "map_Kd":
if (!lineTokens[1]) {
throw Error(`No valid path to texture in .mtl file. (Line ${i+1})`);
}
let texturePath = lineTokens[1];
if (!path.isAbsolute(texturePath)) {
texturePath = path.join(this.objPath.dir, texturePath);
}
if (!fs.existsSync(texturePath)) {
console.error(texturePath);
throw Error(`Cannot load texture ${texturePath}`);
}
const _path = path.parse(texturePath);
if (_path.ext.toLowerCase() != ".png") {
throw Error(`Can only load .png textures`);
}
materialDiffuseTexturePath = texturePath;
break;
}
}
this._addMaterial(materialsJSON, materialName, materialDiffuseColour, materialDiffuseTexturePath);
return materialsJSON;
}
/*
DISCLAIMER: This is a modified version of wavefront-obj-parser
to include .mtl data (https://www.npmjs.com/package/wavefront-obj-parser)
*/
// TODO: Just re-write this whole thing, wtf have I done, these types tho
_parseWavefrontObj(wavefrontString: string): objData {
const vertexInfoNameMap: {[key: string]: string} = {v: 'vertexPositions', vt: 'vertexUVs', vn: 'vertexNormals'};
var parsedJSON: {[key: string]: Array<number>} = {
vertexNormals: [],
vertexUVs: [],
vertexPositions: [],
vertexNormalIndices: [],
vertexUVIndices: [],
vertexPositionIndices: [],
};
var vertexMaterial: Array<string> = [];
var linesInWavefrontObj = wavefrontString.split('\n');
var currentMaterial: string = "";
// Loop through and parse every line in our .obj file
for (let i = 0; i < linesInWavefrontObj.length; i++) {
const currentLine = linesInWavefrontObj[i];
// Tokenize our current line
const currentLineTokens = currentLine.trim().split(/\s+/);
// vertex position, vertex texture, or vertex normal
const vertexInfoType = vertexInfoNameMap[currentLineTokens[0]];
if (vertexInfoType) {
for (let k = 1; k < currentLineTokens.length; k++) {
parsedJSON[vertexInfoType].push(parseFloat(currentLineTokens[k]));
}
continue;
}
switch (currentLineTokens[0]) {
case "mtllib":
this.mtlPath = currentLineTokens[1];
break;
case "usemtl":
currentMaterial = currentLineTokens[1];
break;
case "f":
// Get our 4 sets of vertex, uv, and normal indices for this face
for (let k = 1; k < 5; k++) {
// If there is no fourth face entry then this is specifying a triangle
// in this case we push `-1`
// Consumers of this module should check for `-1` before expanding face data
if (k === 4 && !currentLineTokens[4]) {
parsedJSON.vertexPositionIndices.push(-1);
parsedJSON.vertexUVIndices.push(-1);
parsedJSON.vertexNormalIndices.push(-1);
//parsedJSON.vertexMaterial.push(currentMaterial);
} else {
var indices = currentLineTokens[k].split('/');
parsedJSON.vertexPositionIndices.push(parseInt(indices[0], 10) - 1); // We zero index
parsedJSON.vertexUVIndices.push(parseInt(indices[1], 10) - 1); // our face indices
parsedJSON.vertexNormalIndices.push(parseInt(indices[2], 10) - 1); // by subtracting 1
vertexMaterial.push(currentMaterial);
}
}
}
}
return {
vertexNormals: parsedJSON["vertexNormals"],
vertexUVs: parsedJSON["vertexUVs"],
vertexPositions: parsedJSON["vertexPositions"],
vertexNormalIndices: parsedJSON["vertexNormalIndices"],
vertexUVIndices: parsedJSON["vertexUVIndices"],
vertexPositionIndices: parsedJSON["vertexPositionIndices"],
vertexMaterial: vertexMaterial
}
}
_parseTriangles(materials: Materials) {
this.materialTriangles = {};
for (const materialName in this._materialIndices) {
let triangles = [];
const indices = this._materialIndices[materialName];
for (let i = 0; i < indices.length; i += 3) {
const i0 = indices[i];
const i1 = indices[i + 1];
const i2 = indices[i + 2];
const v0 = this._data.position.slice(3 * i0, 3 * i0 + 3);
const v1 = this._data.position.slice(3 * i1, 3 * i1 + 3);
const v2 = this._data.position.slice(3 * i2, 3 * i2 + 3);
const uv0 = this._data.texcoord.slice(2 * i0, 2 * i0 + 2);
const uv1 = this._data.texcoord.slice(2 * i1, 2 * i1 + 2);
const uv2 = this._data.texcoord.slice(2 * i2, 2 * i2 + 2);
const v0_ = new Vector3(v0[0], v0[1], v0[2]);
const v1_ = new Vector3(v1[0], v1[1], v1[2]);
const v2_ = new Vector3(v2[0], v2[1], v2[2]);
triangles.push(new Triangle(v0_, v1_, v2_, {u: uv0[0], v: uv0[1]}, {u: uv1[0], v: uv1[1]}, {u: uv2[0], v: uv2[1]}));
}
return {
material: materials[materialName],
triangles: triangles
}
}
}
_loadTextures(materials: Materials) {
for (const materialName in materials) {
if (materials[materialName].type == MaterialType.Texture) {
const material = <TextureMaterial> materials[materialName];
material.texture = twgl.createTexture(this._gl, {
src: material.texturePath,
mag: this._gl.LINEAR
});
materials[materialName] = material;
}
}
}
}
module.exports.Mesh = Mesh;

View File

@ -8,11 +8,9 @@ const { SegmentedBuffer, BottomlessBuffer } = require('./buffer.js');
const { GeometryTemplates } = require('./geometry.js');
class Renderer {
export class Renderer {
constructor(fov, backgroundColour) {
console.log("Renderer constructor");
this._backgroundColour = backgroundColour;
this._strokeColour = new Vector3(1.0, 1.0, 1.0);

346
src/voxel_manager.ts Normal file
View File

@ -0,0 +1,346 @@
import { AABB, CubeAABB } from "./aabb";
import { Vector3 } from "./vector.js";
import { HashSet, HashMap } from "./hash_map";
import { Texture } from "./texture";
import { BlockAtlas } from "./block_atlas";
import { UV, RGB } from "./util";
import { Triangle } from "./triangle";
import { Mesh, MaterialTypw } from "./mesh";
interface TriangleCubeAABBs {
triangle: Triangle;
AABBs: Array<CubeAABB>;
}
export class VoxelManager {
public voxels: Array<Vector3>;
public voxelTexcoords: Array<UV>;
public triangleAABBs: Array<TriangleCubeAABBs>;
private voxelsHash: HashSet<Vector3>;
private blockAtlas: BlockAtlas;
private _voxelSize: number;
private _blockMode!: MaterialTypw;
private _currentTexture!: Texture;
private _currentColour!: RGB;
public minX = Infinity; public maxX = -Infinity;
public minY = Infinity; public maxY = -Infinity;
public minZ = Infinity; public maxZ = -Infinity;
constructor(voxelSize: number) {
this._voxelSize = voxelSize;
this.voxels = [];
this.voxelTexcoords = [];
this.triangleAABBs = [];
this.voxelsHash = new HashSet(2048);
this.blockAtlas = new BlockAtlas();
}
public setVoxelSize(voxelSize: number) {
this._voxelSize = voxelSize;
}
private _clearVoxels() {
this.voxels = [];
this.voxelTexcoords = [];
this.minX = Infinity;
this.minY = Infinity;
this.minZ = Infinity;
this.maxX = -Infinity;
this.maxY = -Infinity;
this.maxZ = -Infinity;
this.voxelsHash = new HashSet(2048);
}
public clear() {
this.triangleAABBs = [];
this._clearVoxels();
}
private _snapToVoxelGrid(vec: Vector3) {
return vec.copy().divScalar(this._voxelSize).round().mulScalar(this._voxelSize);
}
private _getTriangleCubeAABB(triangle: Triangle) {
const triangleAABB = triangle.getAABB();
const gridSnappedCentre = this._snapToVoxelGrid(triangleAABB.centre);
let cubeAABB = new CubeAABB(gridSnappedCentre, this._voxelSize);
while (!triangle.insideAABB(cubeAABB)) {
cubeAABB = new CubeAABB(cubeAABB.centre, cubeAABB.width * 2.0);
}
return cubeAABB;
}
private _toGridPosition(vec: Vector3) {
return new Vector3(
Math.round(vec.x / this._voxelSize),
Math.round(vec.y / this._voxelSize),
Math.round(vec.z / this._voxelSize)
);
}
private _toModelPosition(vec: Vector3) {
return new Vector3(
vec.x * this._voxelSize,
vec.y * this._voxelSize,
vec.z * this._voxelSize
);
}
public isVoxelAt(pos: Vector3) {
return this.voxelsHash.contains(pos);
}
addVoxel(vec: Vector3, blockTexcoord: UV) {
// (0.5, 0.5, 0.5) -> (0, 0, 0);
vec = Vector3.subScalar(vec, this._voxelSize / 2);
// [HACK] FIXME: Fix misaligned voxels
// Some vec data is not not grid-snapped to voxelSize-spacing
const test = Vector3.divScalar(vec, this._voxelSize);
if ((test.x % 1 < 0.9 && test.x % 1 > 0.1) || (test.y % 1 < 0.9 && test.y % 1 > 0.1) || (test.z % 1 < 0.9 && test.z % 1 > 0.1)) {
console.warn("Misaligned voxel, skipping...");
return;
}
// Convert to
const pos = this._toGridPosition(vec);
if (this.voxelsHash.contains(pos)) {
return;
}
this.voxels.push(pos);
this.voxelTexcoords.push(blockTexcoord);
this.voxelsHash.add(pos);
this.minX = Math.min(this.minX, vec.x);
this.minY = Math.min(this.minY, vec.y);
this.minZ = Math.min(this.minZ, vec.z);
this.maxX = Math.max(this.maxX, vec.x);
this.maxY = Math.max(this.maxY, vec.y);
this.maxZ = Math.max(this.maxZ, vec.z);
}
// FIXME: Fix voxel meshing for AO and textures
/*
_findXExtent(pos) {
let xEnd = pos.x + 1;
while (this.voxelsHash.contains(new Vector3(xEnd, pos.y, pos.z)) && !this.seen.contains(new Vector3(xEnd, pos.y, pos.z))) {
//console.log("Marking:", new Vector3(xEnd, y, z));
this.seen.add(new Vector3(xEnd, pos.y, pos.z));
++xEnd;
}
return xEnd - 1;
}
_findZExtent(pos, xEnd) {
let canMerge = true;
let zEnd = pos.z + 1;
do {
//console.log("zEnd:", z, zEnd);
for (let i = pos.x; i <= xEnd; ++i) {
const here = new Vector3(i, pos.y, zEnd);
if (!this.voxelsHash.contains(here) || this.seen.contains(here)) {
canMerge = false;
break;
}
}
if (canMerge) {
// Mark all as seen
for (let i = pos.x; i <= xEnd; ++i) {
const here = new Vector3(i, pos.y, zEnd);
//console.log("Marking:", new Vector3(xEnd, y, z));
this.seen.add(here);
}
++zEnd;
}
} while (canMerge);
return zEnd - 1;
}
_findYExtent(pos, xEnd, zEnd) {
let canMerge = true;
let yEnd = pos.y + 1;
do {
for (let i = pos.x; i <= xEnd; ++i) {
for (let j = pos.z; j <= zEnd; ++j) {
const here = new Vector3(i, yEnd, j);
if (!this.voxelsHash.contains(here) || this.seen.contains(here)) {
canMerge = false;
break;
}
}
}
if (canMerge) {
// Mark all as seen
for (let i = pos.x; i <= xEnd; ++i) {
for (let j = pos.z; j <= zEnd; ++j) {
const here = new Vector3(i, yEnd, j);
this.seen.add(here);
}
}
++yEnd;
}
} while (canMerge);
return yEnd - 1;
}
buildMesh() {
this.mesh = [];
this.seen = new HashSet(2048);
//console.log(this.voxelsHash);
const minPos = this._toGridPosition(new Vector3(this.minX, this.minY, this.minZ));
const maxPos = this._toGridPosition(new Vector3(this.maxX, this.maxY, this.maxZ));
for (let y = minPos.y; y <= maxPos.y; ++y) {
for (let z = minPos.z; z <= maxPos.z; ++z) {
for (let x = minPos.x; x <= maxPos.x; ++x) {
const pos = new Vector3(x, y, z);
if (this.seen.contains(pos) || !this.voxelsHash.contains(pos)) {
continue;
}
let xEnd = this._findXExtent(pos);
let zEnd = this._findZExtent(pos, xEnd);
let yEnd = this._findYExtent(pos, xEnd, zEnd);
let centre = new Vector3((xEnd + x)/2, (yEnd + y)/2, (zEnd + z)/2);
let size = new Vector3(xEnd - x + 1, yEnd - y + 1, zEnd - z + 1);
this.mesh.push({
centre: this._toModelPosition(centre),
size: this._toModelPosition(size)
});
}
}
}
//console.log("Mesh:", this.mesh);
return this.mesh;
}
*/
public splitVoxels() {
this._voxelSize /= 2;
this._clearVoxels();
const newTriangleAABBs = [];
for (const {triangle, AABBs} of this.triangleAABBs) {
const triangleAABBs = [];
for (const AABB of AABBs) {
for (const sub of AABB.subdivide()) {
if (triangle.intersectAABB(sub)) {
const voxelColour = this._getVoxelColour(triangle, sub.centre);
const blockTexcoord = this.blockAtlas.getTexcoord(voxelColour);
this.addVoxel(sub.centre, blockTexcoord);
triangleAABBs.push(sub);
}
}
}
newTriangleAABBs.push({triangle: triangle, AABBs: triangleAABBs});
}
this.triangleAABBs = newTriangleAABBs;
}
_getVoxelColour(triangle: Triangle, centre: Vector3): RGB {
const p1 = triangle.v0;
const p2 = triangle.v1;
const p3 = triangle.v2;
const uv1 = triangle.uv0;
const uv2 = triangle.uv1;
const uv3 = triangle.uv2;
const f1 = Vector3.sub(p1, centre);
const f2 = Vector3.sub(p2, centre);
const f3 = Vector3.sub(p3, centre);
const a = Vector3.cross(Vector3.sub(p1, p2), Vector3.sub(p1, p3)).magnitude();
const a1 = Vector3.cross(f2, f3).magnitude() / a;
const a2 = Vector3.cross(f3, f1).magnitude() / a;
const a3 = Vector3.cross(f1, f2).magnitude() / a;
const uv = {
u: uv1.u * a1 + uv2.u * a2 + uv3.u * a3,
v: uv1.v * a1 + uv2.v * a2 + uv3.v * a3
}
if (this._blockMode === MaterialTypw.Texture) {
return this._currentTexture.getRGBA(uv);
} else {
return this._currentColour;
}
}
voxeliseTriangle(triangle: Triangle) {
const cubeAABB = this._getTriangleCubeAABB(triangle);
const triangleAABBs = [];
let queue = [cubeAABB];
while (true) {
const aabb = queue.shift();
if (!aabb) {
break;
}
if (triangle.intersectAABB(aabb)) {
if (aabb.width > this._voxelSize) {
// Continue to subdivide
queue.push(...aabb.subdivide());
} else {
// We've reached the voxel level, stop
const voxelColour = this._getVoxelColour(triangle, aabb.centre);
const blockTexcoord = this.blockAtlas.getTexcoord(voxelColour);
this.addVoxel(aabb.centre, blockTexcoord);
triangleAABBs.push(aabb);
}
}
}
this.triangleAABBs.push({triangle: triangle, AABBs: triangleAABBs});
}
voxeliseMesh(mesh: Mesh) {
for (const materialName in mesh.materialTriangles) {
const material = mesh.materialTriangles[materialName];
material.
if ("diffuseTexturePath" in mesh._materials[material]) {
this._currentTexture = new Texture(mesh._materials[material].diffuseTexturePath);
this._blockMode = "TEXTURE";
} else {
this._currentColour = mesh._materials[material].diffuseColour;
this._blockMode = "FILL";
}
for (const triangle of mesh.materialTriangles[material]) {
this.voxeliseTriangle(triangle);
}
}
}
}
module.exports.VoxelManager = VoxelManager;